Recognizing Anger Through Inside Out – What About Drinks?
Recognizing Anger Through Inside Out – What About Drinks?

Пятница, 18:00. Вы обновляете PHP на сервере с 7.4 на 8.0 — что может пойти не так? Деплой проходит, страница обновляется, и вместо привычного портала вы видите белый экран с TypeError: array_search(): Argument #2 ($haystack) must be of type array, null given. Мессенджер разрывается от сообщений: «Ничего не работает». Знакомо?

Привет! Меня зовут Дмитрий Черкашин, я разработчик в Битрикс24. Проанализировав девчаты разработчиков и опираясь на личный опыт, я собрал самые распространённые ошибки в кастомизациях коробочной Битрикс24 в две главы:

  • Первая — про ловушки ORM, в которые попадаешь, когда пытаешься написать код «по-современному».

  • Вторая — про боли миграции. Это о том, как в PHP 8+ безжалостно выстреливает легаси-код, который годами работал «на честном слове».

Внутри — конкретные ошибки с кодом, цифры производительности и интерактивные задачки. Проверьте, сколько из этих грабель вы уже собрали.

1 — Ловушки ORM: производительность, память и сложные запросы

ORM был создан как более современный и безопасный способ работы с базой данных в Битрикс24. В отличие от старого ядра, где работа велась в процедурном стиле через статические методы вроде CIBlockElement::GetList(), новое ядро основано на объектно-ориентированном подходе. Такой подход делает код более читаемым и удобным для поддержки.

Хотя ORM помогает писать более структурированный и поддерживаемый код, легко споткнуться о некоторые особенности поведения. Даже опытные разработчики сталкиваются с ситуациями, когда «красивый» код на ORM внезапно падает с исключением или возвращает неверные данные.

Общая рекомендация: Используйте ::query() вместо GetList и старого стиля ORM. Новый API позволяет избежать неявных UPPER и LIKE, которые ломают индексы БД.

Как ORM строит запросы

1.1. ORM больше не разрешает использовать SQL-выражения прямо в select

Современное ядро ужесточило требования к безопасности запросов. В старых версиях Битрикс24 разработчики часто смешивали ORM и фрагменты SQL. Например, можно было прямо внутри массива select написать SUM(PRICE) или COUNT(*), и ORM спокойно подставлял это в итоговый SQL-запрос.

В новых версиях ядра это считается небезопасным. Попытка прокинуть массив с выражениями напрямую в секцию select метода getList теперь приводит к фатальному исключению Bitrix\Main\ArgumentException. Система выдает ошибку: «Expression as an array in select section is no more supported due to security reason».

Как делать не надо:

$result = SomeTable::getList([
    'select' => [
        'ID',
        'TOTAL' => 'SUM(AMOUNT)',  // sql выражение больше нельзя!
    ],
    'group' => ['DEAL_ID'],
]);

Как надо: для вычисляемых полей необходимо использовать объекты ExpressionField, метод Query->registerRuntimeField или передавать готовый объект поля вместо массива.

use Bitrix\Main\Entity\ExpressionField;

$query = SomeTable::query()
    ->registerRuntimeField(
        new ExpressionField('TOTAL', 'SUM(%s)', ['AMOUNT'])
    )
    ->setSelect(['DEAL_ID', 'TOTAL'])
    ->addGroup('DEAL_ID')
;
foreach ($query->exec() as $row)
{
}

1.2. Почему сложные JOIN в ORM часто ломаются: ошибка "Unknown reference value"

Простые ORM-запросы обычно работают без проблем: выбрать список сделок, получить пользователей или найти элементы инфоблока. Но при работе с несколькими таблицами появляются запутанные ошибки ORM, который требует строгого описания связей.

Простая подстановка значений в ReferenceField больше не работает. При попытке построить сложные связи между таблицами (например, джойн свойств инфоблока) разработчики часто получают ошибку «Unknown reference value».

Пример: когда в описании связи Reference пытаются использовать числовые значения напрямую (например, ID свойства), ORM не понимает, как это интерпретировать в контексте маппинга полей.

Решение: необходимо использовать Runtime поля и корректно описывать условия связи через Join::on. Советую заглядывать в метод getQuery(), чтобы увидеть реальный SQL-запрос, который строит ORM, и понять, где именно отваливается логика джойна.

Как делать не надо:

use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\Entity\ReferenceField;

// числовое значение напрямую вызовет "Unknown reference value"
$query = \Bitrix\Iblock\ElementTable::query();

$query->registerRuntimeField(
    new ReferenceField(
        'PROP_BRAND',
        \Bitrix\Iblock\ElementPropertyTable::class,
        [
            '=this.ID' => 'ref.IBLOCK_ELEMENT_ID',
            'ref.IBLOCK_PROPERTY_ID' => 42,  // ОШИБКА!
        ]
    ),
);

Как надо:

use Bitrix\Main\ORM\Query\Join;

use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\DB\SqlExpression;

// используем 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))
    )
);

1.3. Почему delete($id) работает не для всех таблиц

Во многих ORM-системах любую запись можно удалить через простой вызов delete($id). Это работает, когда у таблицы есть обычный первичный ключ — например, поле ID.

Некоторые системные таблицы Битрикс24 используют составной первичный ключ вместо обычного ID. Из-за этого стандартное удаление через delete($id) перестаёт работать, и требуется передавать весь составной ключ целиком.

Если нужно удалить набор строк не по primary key, а по условию, используйте deleteByFilter(). Но с ним нужно быть осторожным: фильтр должен быть максимально точным, иначе можно удалить больше данных, чем планировалось. Кроме того, перед использованием нужно подключить трейт Bitrix\Main\ORM\Data\Internal\DeleteByFilterTrait.

Пример: таблица прав \Bitrix\Main\TaskOperationTable использует составной первичный ключ (TASK_ID + OPERATION_ID).

Как бороться: в таких случаях метод удаления ожидает не просто числовой ID, а массив, полностью идентифицирующий конкретную строку:

// стандартное удаление - работает для таблиц с одиночным PK
SomeTable::delete($id);  // $id = 123

// составной первичный ключ - передаём массив
\Bitrix\Main\TaskOperationTable::delete([
    'TASK_ID'      => 456,
    'OPERATION_ID' => 7,
]);

// удаление по условию
SomeTable::deleteByFilter([
    '=ENTITY_ID' => $entityId,
]);

ORM — мощный инструмент, но он требует понимания внутренних механизмов.

Почему ORM начинает тормозить и перегружать память

1.4. Опасность SELECT * и полей типа TEXT

При использовании ORM легко забыть указать конкретные поля в выборке — и получить серьёзную деградацию. Если не указать select, запрос начинает вытягивать все поля таблицы. Особенно опасно это для полей TEXT и BLOB, которые резко увеличивают потребление памяти и время выполнения запросов.

На небольших проектах это может быть незаметно. Но в CRM Битрикс24 таблицы часто содержат много служебных и тяжёлых полей: описания, JSON-структуры, комментарии, служебные данные. В результате даже простой запрос может создавать серьёзную нагрузку и замедлять страницу.

Визуально код выглядит нормально, потому что разработчик просто забыл указать select. Но внутри ORM это фактически аналог SELECT *.

В чём проблема: поля типа TEXT и BLOB увеличивают объём данных, который база должна прочитать и передать в PHP. Затем эти данные попадают в память процесса и могут увеличивать размер кеша.

Тестовые замеры: в тестах выборка логина и имени заняла 0.04с, а всех полей — 0.83с с потреблением 498 МБ памяти.

Как делать не надо:

// тянем все поля, включая TEXT/BLOB
$users = \Bitrix\Main\UserTable::getList([
    'filter' => ['ACTIVE' => 'Y'],
])->fetchAll();
// нет select → ORM выберет ВСЕ поля → 0.83с, 498 МБ

Как надо:

// только нужные поля
$result = \Bitrix\Main\UserTable::query()
    ->setSelect(['ID', 'LOGIN', 'NAME', 'LAST_NAME'])
    ->where('ACTIVE', 'Y')
    ->exec()
;

while ($user = $result->fetch())
{
    // работа с $user
}
// 0.04с, минимум памяти

Конкретные цифры зависят от таблицы, объёма данных и окружения, но порядок разницы хорошо показывает цену SELECT *.

Всегда указывайте конкретные поля в select. Никогда не полагайтесь на «выберется всё, что нужно».

1.5. Как fetchAll() может положить сервер по памяти

После выполнения ORM-запроса данные нужно обработать в PHP. 

Многие разработчики по привычке используют fetchAll(), который загружает весь результат в память сразу, что при больших объёмах мгновенно приводит к Fatal error: Allowed memory size exhausted.

Проблема в том, что fetchAll() загружает в память абсолютно все строки выборки одновременно. Если таблица маленькая — всё нормально. Но в CRM, логах, задачах или инфоблоках количество строк может исчисляться десятками и сотнями тысяч. 

Первый шаг — обрабатывать результат через fetch(), не складывая всё в массив. Но если выборка большая, одного fetch() мало: данные лучше читать батчами, например по 500-1000 строк, с явной сортировкой по ID.

Как делать не надо:

// загружает ВСЕ строки в массив в RAM
$rows = SomeTable::getList([
    'select' => ['ID', 'NAME', 'PRICE'],
    'filter' => ['ACTIVE' => 'Y'],
])->fetchAll();
// 100 000 строк приведет к memory_limit exceeded

Как надо:

// потоковая обработка, по одной строке
$result = SomeTable::query()
    ->setSelect(['ID', 'NAME', 'PRICE'])
    ->where('ACTIVE', 'Y')
    ->exec()
;

while ($row = $result->fetch())
{
    processRow($row);
    // каждая итерация - минимальное потребление памяти
}

// для больших выборок: читаем данные батчами
$lastId = 0;
$batchSize = 1000;

do

{
    $result = SomeTable::query()
        ->setSelect(['ID', 'NAME', 'PRICE'])
        ->where('ACTIVE', 'Y')
        ->where('ID', '>', $lastId)
        ->setOrder(['ID' => 'ASC'])
        ->setLimit($batchSize)
        ->exec()
    ;

    $count = 0;
    while ($row = $result->fetch())
    {
        $lastId = (int)$row['ID'];
        $count++;
        processRow($row);
    }
}

while ($count === $batchSize);

1.6. Кешируйте ORM-запрос, а не коллекцию

После перехода на коллекции многие пытаются положить ORM-объекты в кеш и сталкиваются с ошибками сериализации. Например, кешировать результат выборки ORM (объект EO_Collection) через стандартный механизм endDataCache(). Это сразу вызывает ошибку Serialization of 'Closure' is not allowed.

Почему так: объекты ORM (Entity Object и Collections) хранят ссылки на Entity-метаданные, которые содержат замыкания (Closure) — валидаторы полей, дефолтные значения, колбэки. PHP не умеет сериализовать Closure.

Что делать: используйте кеширование самого запроса, а не результирующей коллекции.

Как делать не надо:

use Bitrix\Main\Data\Cache;

if ($cache->initCache($cacheTime, $cacheId, $cacheDir))
{
    // достаём из кеша уже обычный массив
    $items = $cache->getVars()['items'];
}
elseif ($cache->startDataCache())
{
    $collection = \Bitrix\Crm\DealTable::getList([
        'select' => ['ID', 'TITLE', 'OPPORTUNITY'],
        'filter' => $filter,
    ])->fetchCollection();

    // упадёт с "Serialization of 'Closure' is not allowed"
    $cache->endDataCache(['items' => $collection]);
}

Как надо:

$collection = \Bitrix\Crm\DealTable::query()
    ->setSelect(['ID', 'TITLE', 'OPPORTUNITY'])
    ->setFilter($filter)
    ->setCacheTtl($cacheTtl)
    ->exec()
    ->fetchCollection()
;

2. Боли миграции на новую версию PHP

Переход на PHP 8.0 и выше стал для многих «холодным душем». В 2023 году минимальной требуемой версией PHP для работы Битрикс24 стала 8.0, поэтому многим разработчикам пришлось мигрировать свои кастомные наработки. В PHP 8.x язык стал строже относиться к типам, сигнатурам методов и некорректным вызовам.

Если PHP 7.4 прощал небрежность в типах данных, то новая версия превратила старые предупреждения в фатальные ошибки, роняющие порталы.

Особенно болезненно это проявилось в старых кастомизациях Битрикс24, которые пишут наши партнёры для дополнительных возможностей в своих бизнес-порталах. Во многих шаблонах, компонентах и модулях данные годами использовались без проверок: массив оказывался null, метод вызывался не тем способом, а сигнатуры интерфейсов не совпадали полностью. Раньше это либо игнорировалось, либо вызывало предупреждение. Теперь такие места роняют страницы, AJAX-запросы и фоновые процессы.

2.1. Фатальные ошибки типизации и «хрупкие» стандарты

Одна из проблем после перехода на PHP 8 связана с тем, что язык стал гораздо строже проверять типы данных. В PHP 7 многие встроенные функции спокойно принимали неправильные значения, в PHP 8 — нет. 

Самая частая «боль» — это использование функций array_search(), usort(), in_array() и count() с переменными, которые неожиданно оказываются null.

// было (PHP 7.4 прощал Warning)
$key = array_search('active', $arParams['STATUS_LIST']);

// стало (PHP 8.0+ фатальный TypeError)
// TypeError: array_search(): Argument #2 ($haystack) must be of type array
//null given

Ловушка array-функций: начиная с PHP 8.0 все функции семейства array_* (array_search, usort, in_array, array_keys и др.) стали бросать TypeError при получении null вместо массива. Раньше это оставалось предупреждением E_WARNING, теперь — фатальная ошибка. Особенно критично там, где данные приходят неструктурированными, часто незаполненными, и null — не исключение, а норма.

Лайфхак: используйте empty() вместо count() > 0. Это не просто защита от TypeError, это ещё и микрооптимизация. 

empty() — языковая конструкция (как isset), а не вызов функции. Она не создаёт стекового фрейма, не выполняет lookup в таблице функций и не бросает TypeError на null. В высоконагруженных шаблонах, которые рендерятся тысячи раз в день, эта разница складывается.

Строгий array_search(): Аналогично, попытка найти значение в массиве, который пришел как null (например, из параметров компонента или неверно полученного свойства), теперь считается критической ошибкой.

// типичная ошибка в шаблоне компонента $arParams не гарантирует массив
// PHP 7.4: вернет false без ошибки
// PHP 8+: TypeError
$key = array_search('active', $arParams['STATUS_LIST']);

// защитный вариант
$statusList = $arParams['STATUS_LIST'] ?? [];
$key = is_array($statusList) ? array_search('active', $statusList) : false;

2.2. Старые классы и библиотеки перестают соответствовать интерфейсам PHP 8

После перехода на PHP 8 в старом коде начали всплывать ошибки несовпадения сигнатур методов. Особенно это касается кастомных библиотек, модулей и классов, написанных ещё под старые версии PHP, а также сторонних модулей, использующих устаревшие реализации интерфейсов PSR.

Раньше язык довольно спокойно относился к расхождениям между сигнатурой метода и интерфейсом: метод мог принимать аргументы без указания типа, хотя интерфейс ожидал int, или возвращать значение без указанного return type.

В PHP 8 сигнатуры методов должны совпадать практически полностью: типы аргументов, возвращаемые значения и иногда даже nullable-типизация. Поэтому старые библиотеки и кастомные модули начинают выдавать фатальные ошибки сразу после обновления.

Как делать не надо:

// старая сигнатура метода без типов
class MyIterator implements \SeekableIterator
{
    public function seek($offset) { /* ... */ }      // PHP 8+: return type
}

Как надо:

// строгая сигнатура, соответствующая интерфейсу
class MyIterator implements \SeekableIterator
{
    public function seek(int $offset): void { /* ... */ }
}

2.3. Нестатические методы, вызываемые как статические

В старом PHP многие ошибки объектной модели игнорировались. Например, разработчик мог случайно вызвать обычный метод класса как статический через ClassName::method(), и PHP либо показывал warning, либо продолжал выполнять код.

Метод vs Статика: PHP 8.0 перестал игнорировать вызов динамических методов как статических. Ошибки вроде Cannot make non static method ... static стали обыденностью при обновлении ядра или использовании легаси-кода.

Как делать не надо:

// PHP 7.4: Deprecated-предупреждение (легко пропустить)
// PHP 8+: Fatal error
CMyLegacyModule::doSomething();  // метод doSomething() не static

Как надо:

// сделать метод статическим (если логика позволяет)
public static function doSomething(): void { /* ... */ }

2.4. Стратегия «защитного» программирования

После перехода на PHP 8 видно, что проблема не только в отдельных ошибках, а в самом стиле старого кода. Во многих кастомизациях разработчики привыкли полагаться на то, что данные скорее всего придут правильно: массивы существуют, свойства заполнены, а методы вызываются корректно. 

Новые версии языка требуют более аккуратного и предсказуемого подхода. Новый PHP подталкивает разработчиков к более строгому и безопасному стилю программирования.

Чтобы ваши кастомизации не превратились в тыкву после обновления версии PHP на сервере, следуйте правилам.

Внедрите строгие проверки is_array(). Забудьте про краткую запись if (count($val)). Перед любой операцией с массивом (особенно если это свойства инфоблока или параметры компонента) всегда проверяйте тип данных. Ещё лучше — используйте empty() вместо count() > 0.

Как делать не надо:

// сделать метод статическим (если логика позволяет)
public static function doSomething(): void { /* ... */ }

Как надо:

// хорошо явная проверка перед каждой операцией
$items = $arItem['ITEMS'] ?? [];
if (is_array($items) && !empty($items))
{
    $found = array_search($itemId, $items);
}

Проверяйте сигнатуры при наследовании. Если вы переопределяете методы системных таблиц (например, getMap()), убедитесь, что возвращаемые типы и аргументы в точности соответствуют родительскому классу в ядре.

Как делать не надо:

// раньше работало
class MyEntityTable extends \Bitrix\Main\Entity\DataManager
{
    public static function getMap()
    {
        return [ /* ... */ ];
    }
}

Как надо:

// проверяем сигнатуру в родителе и копируем точно
class MyEntityTable extends \Bitrix\Main\Entity\DataManager
{
    public static function getMap(): array
    {
        return [ /* ... */ ];
    }
}

Включите диагностику через логи. При миграции первым делом включайте display_errors и логирование в .settings.php, потому что многие ошибки в PHP 8 «молчат» в публичной части, но мгновенно убивают AJAX-запросы и фоновые процессы.

Подробнее про настройку обработки ошибок и логирования можно посмотреть в документации Bitrix Framework.

Принудительно приводите к массиву. Для устранения фатальных ошибок count(null) в PHP 8 рекомендуется массово заменять переменные на (array)$variable в проблемных строках.

// быстрый «патч» для массовой миграции шаблонов
// вместо рефакторинга каждого if/foreach приводим к массиву в точке входа
$items = (array)($arResult['ITEMS'] ?? []);
foreach ($items as $item)
{
    $props = (array)($item['PROPERTIES'] ?? []);
    // теперь count() и foreach гарантированно не упадут
}

Исправление сессий: При возникновении ошибки session_start до инициализации ядра (часто после перехода на PHP 8) помогает проверка файлов dbconn.php и init.php на наличие лишних пробелов или текста после закрывающего тега ?>.

Интерактив: найдите все места, где упадёт PHP 8+

Прежде чем двигаться дальше — проверьте себя. Вот реальный фрагмент из шаблона компонента. Сколько потенциальных TypeError вы здесь видите?

$items = $arResult['ITEMS'];
$count = count($items);

foreach ($items as $item)
{
    $photos = $item['PROPERTIES']['PHOTOS']['VALUE'];
    $photoCount = count($photos);

    $tags = $item['PROPERTIES']['TAGS']['VALUE'];
    $hasDiscount = array_search('discount', $tags) !== false;

    $related = $item['RELATED_ITEMS'];
    usort($related, function ($a, $b)
    {
        return $a['SORT'] <=> $b['SORT'];
    });

    echo "Товар: {$item['NAME']}, фото: {$photoCount}";
}
Ответ

Минимум 5 проблемных мест:

  1. Строка 2: count($items) — если $arResult['ITEMS'] не задан, $items = nullTypeError (фатал!)

  2. Строка 4: foreach($items as $item) — если $arResult['ITEMS'] не задан, $items = nullTypeError (фатал!)

  3. Строка 6: count($photos)TypeError (фатал!)

  4. Строка 10: array_search('discount', $tags)TAGS может быть nullTypeError (фатал!)

  5. Строка 13: usort($related, ...)RELATED_ITEMS может отсутствовать → TypeError (фатал!)

Исправленный вариант:

$items = (array)($arResult['ITEMS'] ?? []);
foreach ($items as $item)
{
    $photos = (array)($item['PROPERTIES']['PHOTOS']['VALUE'] ?? []);
    $tags = (array)($item['PROPERTIES']['TAGS']['VALUE'] ?? []);
    $related = (array)($item['RELATED_ITEMS'] ?? []);

    $hasDiscount = array_search('discount', $tags) !== false;
    if (!empty($related))
    {
        usort($related, fn($a, $b) => ($a['SORT'] ?? 0) <=> ($b['SORT'] ?? 0));
    }
}

Переход на PHP 8 — это не просто смена версии, а переход к культуре строгой типизации. 

Простое правило: каждый null должен быть обработан до того, как он попадёт в стандартную функцию. Если вы запомните только одну вещь из этой главы — пусть это будет $var ?? [] перед любым count, array_search и foreach.

Промпт для AI-проверки:

Скопируйте этот промпт в AI-ассистента и приложите к нему код шаблона, компонента, модуля или ORM-запроса, который хотите проверить.

# Промпт для AI-проверки кода

## Контекст

Целевой стек: PHP 8+, Битрикс24 с модулем main версии 23.0.0+.

Ключевые классы Bitrix ORM:

- Таблицы наследуются от \Bitrix\Main\ORM\Data\DataManager

- Query Builder: \Bitrix\Main\ORM\Query\Query (через ::query())

- Поля: \Bitrix\Main\ORM\Fields\{ScalarField, IntegerField, StringField, 

  ExpressionField, Relations\Reference, Relations\OneToMany, Relations\ManyToMany}

- JOIN-условия: \Bitrix\Main\ORM\Query\Join::on('this.FIELD', 'ref.FIELD')

- Удаление по фильтру: трейт \Bitrix\Main\ORM\Data\Internal\DeleteByFilterTrait

- Результаты: fetch(), fetchAll(), fetchObject(), fetchCollection()

В шаблонах компонентов структура $arResult приходит извне и может содержать 

скаляры/null там, где ожидается массив — защитные приведения типов уместны.

## Что проверять

1. ORM-запросы

   - Запросы без явного select() → добавь выбор только нужных полей.

   - Сложные getList() → предложи ::query().

   - SQL-выражения строкой или массивом в select → замени на ExpressionField.

   - JOIN/ReferenceField → проверь runtime-поля, Join::on(), безопасность SQL.

   - fetchAll() на больших выборках → fetch() в цикле или батчинг.

   - ORM-объекты/EO_Collection в обычном кеше → кешируй сам запрос используя setCacheTtl.

2. Удаление через ORM

   - delete($id) при составном PK → массив ключей.

   - Удаление по условию → deleteByFilter() + предупреждение о точности фильтра.

3. Совместимость с PHP 8.x

   - array-функции (count, array_search, in_array, array_keys, usort и т.д.) 

     на возможном null/не-массиве → защита через is_array() / ?? [] / (array).

   - foreach по переменным, которые могут быть null.

   - Обращение к вложенным массивам без ?? / isset().

   - Вызовы нестатических методов как статических.

   - Несовпадение сигнатур с интерфейсом или родителем (типы аргументов, 

     return type, nullable).

## Чего НЕ делать

- Не переписывай CIBlockElement::GetList на ORM без отдельной просьбы.

- Не меняй стиль форматирования кода (Битрикс использует фигурные скобки 

  на новой строке для классов и методов).

- Не меняй бизнес-логику без необходимости.

- Не выдумывай классы, поля таблиц и сигнатуры методов. Если не хватает getMap(), схемы таблицы или версии ядра — пометь место как требующее ручной проверки.

## Формат ответа

1. Список найденных проблем с пояснением, почему каждая опасна.

2. Сначала сделай аудит. Код исправляй только после списка проблем или если я

     явно прошу сразу исправленный вариант.

3. Если для исправления не хватает контекста — явно укажи, какого.

4. Отдельным блоком — места, требующие ручной проверки разработчика.

Желаю, чтобы ваши продакшены не падали, страницы грузились моментально, а TypeError встречался реже. Удачи в проектах!

Комментарии (0)