Какие ошибки чаще всего ломают кастомизации Битрикс24 после перехода на PHP 8
Обновление платформы почти всегда выявляет скрытые проблемы в кастомном коде. Чаще всего портал падает не из-за самой новой версии PHP или ORM, а из-за старых привычек разработки: неявных типов, неаккуратных выборок, тяжелых запросов и кода, который годами жил на допущениях.
В этой статье мы собрали практические сценарии, которые регулярно встречаются в проектах на коробочной Битрикс24. Материал будет полезен тем, кто поддерживает CRM-порталы, дорабатывает компоненты, пишет свои таблицы ORM и отвечает за стабильность сайта после обновлений.
Почему проблемы проявляются именно после обновления
Когда проект долго работает на одной версии ядра и PHP, многие слабые места остаются незаметными. PHP 8 стал строже относиться к типам данных, а ORM — к структуре запросов и связей между сущностями. В результате код, который раньше «терпел» некорректные данные, теперь начинает падать фатально.
На практике это означает простую вещь: обновление не ломает корректную архитектуру, но очень быстро вскрывает легаси-ошибки. Поэтому миграцию нужно воспринимать не как техническую формальность, а как ревизию качества кода и архитектуры данных.
ORM: что чаще всего идет не так
Новый ORM в Битрикс24 удобнее старых процедурных методов, но и требования к нему выше. Разработчики часто переносят старые привычки в новый API, а именно здесь и начинаются проблемы: лишние поля, неправильные join-условия, попытки вставить SQL-выражения в массивы и обработка огромных выборок в память.
1. Слишком широкая выборка
Самая частая ошибка — не задавать явный список полей. В результате ORM возвращает больше данных, чем реально нужно, а это особенно дорого для таблиц с текстовыми полями, JSON, служебными колонками и множеством связей.
// Плохо: вытягиваем все поля
$result = \Bitrix\Main\UserTable::getList([
'filter' => ['ACTIVE' => 'Y']
]);
// Лучше: берем только то, что используется
$result = \Bitrix\Main\UserTable::query()
->setSelect(['ID', 'LOGIN', 'NAME', 'LAST_NAME'])
->where('ACTIVE', 'Y')
->exec();
На небольшом списке разница почти незаметна, но на реальных порталах она быстро превращается в лишнюю нагрузку на БД и память PHP. Если запрос используется в списке, карточке или AJAX-обработчике, сначала подумайте, какие поля действительно нужны.
2. Перегрузка памяти через fetchAll
Еще одна типовая ошибка — сразу забирать весь результат в массив через fetchAll(). Это удобно, но опасно: при больших выборках память процесса расходуется очень быстро, а потом появляются ошибки вида Allowed memory size exhausted.
// Плохо: все строки сразу в память
$rows = \Bitrix\Crm\DealTable::getList([
'select' => ['ID', 'TITLE', 'OPPORTUNITY'],
'filter' => ['=STAGE_ID' => 'NEW']
])->fetchAll();
// Лучше: построчная обработка
$result = \Bitrix\Crm\DealTable::query()
->setSelect(['ID', 'TITLE', 'OPPORTUNITY'])
->where('STAGE_ID', 'NEW')
->exec();
while ($row = $result->fetch()) {
processDeal($row);
}
Если данных очень много, лучше читать их батчами: по 500–1000 строк, с сортировкой и контролем последнего ID. Такой подход стабильнее и проще для поддержки.
3. Неправильные JOIN и runtime-поля
Сложные связи между таблицами часто ломаются из-за неявных условий. ORM требует точного описания join-логики, а попытка «прикрутить» константу прямо в reference-условие нередко заканчивается ошибкой или неверным SQL.
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\DB\SqlExpression;
$query = \Bitrix\Iblock\ElementTable::query();
$query->registerRuntimeField(
new ReferenceField(
'PROP_BRAND',
\Bitrix\Iblock\ElementPropertyTable::class,
Join::on('this.ID', 'ref.IBLOCK_ELEMENT_ID')
->where('ref.IBLOCK_PROPERTY_ID', new SqlExpression('?i', 42))
)
);
Такой вариант предсказуемее, чем попытки встроить числовые значения в описание связи напрямую. Если запрос стал слишком сложным, полезно посмотреть итоговый SQL через методы диагностики и проверить, как ORM реально интерпретировал условия.
4. Удаление не тем способом
В простых таблицах можно удалить запись по одному ID, но в системных сущностях это работает не всегда. Причина в том, что часть таблиц использует составной первичный ключ, и тогда простой вызов delete($id) уже не подходит.
// Для таблиц с одиночным PK
SomeTable::delete(123);
// Для составного ключа
\Bitrix\Main\TaskOperationTable::delete([
'TASK_ID' => 456,
'OPERATION_ID' => 7,
]);
Если нужна выборка и удаление по условию, используйте удаление по фильтру, но только с максимально точными критериями. Ошибка в фильтре в таких сценариях может стоить дороже, чем час отладки.
PHP 8: где чаще всего рвется старый код
После перехода на PHP 8 типовые проблемы обычно повторяются из проекта в проект. Это не «каприз версии», а следствие того, что язык перестал молча закрывать глаза на невалидные значения и несовместимые сигнатуры.
1. Функции массива на null
Одна из самых массовых причин падения — передача null в функции, которые ожидают массив. Раньше это часто ограничивалось warning, а теперь легко превращается в фатальную ошибку.
// Потенциальная проблема
$key = array_search('active', $arParams['STATUS_LIST']);
// Безопасный вариант
$statusList = $arParams['STATUS_LIST'] ?? [];
$key = is_array($statusList) ? array_search('active', $statusList) : false;
Аналогично стоит проверять count(), in_array(), usort(), foreach и любые другие операции, завязанные на массив. Если переменная приходит извне, нужно считать, что она может быть пустой, null или иметь неожиданный формат.
2. foreach по не-массиву
Очень часто шаблон компонента работает с данными, которые на одном объекте — массив, а на другом — null. В PHP 8 это уже не «мелкое отклонение», а повод для ошибки.
$items = (array)($arResult['ITEMS'] ?? []);
foreach ($items as $item) {
$photos = (array)($item['PROPERTIES']['PHOTOS']['VALUE'] ?? []);
$tags = (array)($item['PROPERTIES']['TAGS']['VALUE'] ?? []);
}
Приведение к массиву — не идеальная архитектура, но на этапе миграции это часто лучший способ быстро стабилизировать код. Важно не просто «погасить» ошибку, а потом вернуться к нормальной валидации данных на входе.
3. Несовпадение сигнатур
Еще одна зона риска — наследование классов и реализация интерфейсов. Старые библиотеки нередко написаны без строгих типов, а PHP 8 требует аккуратного совпадения аргументов, возвращаемых значений и nullable-типов.
class MyIterator implements \SeekableIterator
{
public function seek(int $offset): void
{
// ...
}
}
Если в старом модуле сигнатуры не совпадают с требованиями интерфейса или родительского класса, ошибка может проявиться только после обновления. Поэтому при миграции нужно отдельно проверять все переопределенные методы, особенно в библиотеках, написанных несколько лет назад.
4. Нестатический вызов как статический
В legacy-коде встречается вызов обычного метода через двойное двоеточие. Раньше это могло работать или давать предупреждение, но в новой версии PHP такие конструкции лучше считать ошибкой проектирования.
// Плохо
CMyLegacyModule::doSomething();
// Лучше
$module = new CMyLegacyModule();
$module->doSomething();
Если метод не должен быть статическим, не пытайтесь подгонять вызов под старый стиль. Гораздо безопаснее привести код к нормальной объектной модели, чем наращивать еще один слой совместимости.
Практика безопасной миграции
Чтобы обновление не превратилось в серию аварийных исправлений, миграцию нужно проводить поэтапно. Сначала выявляются самые опасные места: шаблоны, AJAX-обработчики, ORM-запросы, старые модули и участки с массивами, которые приходят извне.
Затем код переводится в более строгий режим: явные select-поля, защитные проверки is_array(), осторожная работа с fetchAll(), контроль сигнатур и отказ от неочевидных допущений. Такой подход дает не только совместимость, но и более предсказуемую поддержку проекта в будущем.
Полезные приемы
- Используйте явные поля в ORM-запросах, а не выборку «всего подряд».
- Обрабатывайте большие наборы данных потоково, а не одним массивом.
- Проверяйте все входные данные, которые могут оказаться null или не массивом.
- Сверяйте сигнатуры методов при наследовании и реализации интерфейсов.
- Перед обновлением PHP обязательно прогоняйте критичные сценарии на тестовой копии портала.
Что стоит проверить отдельно
Особенно внимательно нужно смотреть на кастомные компоненты, которые работают с каталогом, CRM, инфоблоками и пользовательскими свойствами. Именно там обычно накапливается больше всего «тихих» технических долгов: неявные массивы, устаревшие методы и выборки, рассчитанные на старую версию ядра.
Если в проекте есть свой ORM-слой, отдельные события, кеширование коллекций или модули интеграции с внешними сервисами, их тоже стоит проверить до запуска обновления. В таких местах проблемы часто проявляются не сразу, а через нагрузку, очереди и фоновые агенты.
Вывод для команды
Переход на ORM и PHP 8 — это не просто техническое обновление, а проверка качества всего кода проекта. Чем раньше вы начнете относиться к данным строго, тем меньше будет аварий после деплоя.
Для поддержки живого портала важнее не «красивый код» сам по себе, а код, который одинаково стабилен на реальных данных, под нагрузкой и после очередного обновления платформы.
