Легаси-код в проекте - как читать, править и переводить на D7
Открыли проект, а там классы с буквой C и глобальные объекты. Разбираем, как это устроено, как безопасно править и что имеет смысл переводить на новое ядро.
Механика
В продукте живут сразу два ядра одновременно. Старое - процедурные функции и классы с одной буквой в начале имени, новое - пространства имён с автозагрузкой, ORM и контроллерами.
Старое ядро никуда не делось и в обозримое время никуда не денется. Часть модулей до сих пор даёт только старые классы, а публичные страницы собираются прологом и эпилогом с глобальными объектами.
Три глобальных объекта создаются в прологе и доступны на любой странице. Один отвечает за страницу, второй за текущего пользователя, третий за соединение с базой.
Выборки старого ядра возвращают объект результата, а вовсе не готовый массив. Обращение к нему как к массиву даёт непонятную ошибку, и это первое, обо что спотыкается человек с опытом других систем.
Два метода чтения очередной строки ведут себя по-разному. Один экранирует значения для вывода в разметку, второй отдаёт данные как есть, и подмена одного другим тихо меняет поведение страницы.
Ошибки методов старого ядра не бросают никаких исключений. Метод возвращает ложь, а текст ошибки кладёт в глобальную переменную или в объект приложения - проверять надо результат, а не полагаться на перехват.
Подключение модуля обязательно проверяется. Без успешного подключения классы модуля недоступны, и страница падает на первом же вызове.
Правка файлов ядра бессмысленна. Обновление продукта возвращает их к исходному виду, а место для своих правок - каталог решений проекта.
Новый код пишут на новом ядре, но границы его применимости понятны. Административные списки, изменение размера картинок и агенты живут в старом ядре, и переписывать их не на чем.
Перевод проекта на новое ядро не является самоцелью. Переводят то, что и так меняют: рабочий легаси без задач трогать не стоит, потому что риск больше пользы.
Отдельная привычка полезна на чужом проекте: смотреть, каким ядром написан соседний код, прежде чем писать свой. Файл, целиком собранный на старых классах, проще дополнить в том же стиле, чем разбавлять двумя подходами сразу.
Шаги
- Определить, какие части проекта написаны на старом ядре и какие на новом.
- Проверить подключение нужных модулей и защиту подключаемых файлов от прямого вызова.
- Убрать запросы в цикле и прямой SQL с неэкранированными данными формы.
- Проверять результат каждого вызова: ошибки лежат рядом, а вовсе не в исключении.
- Переводить на новое ядро только то, что и так предстоит менять по задаче.
- Оставить в старом ядре то, для чего нового API просто не существует.
Код
Читаем выборку старого ядра:
$res = CIBlockElement::GetList([], ['IBLOCK_ID' => 12], false, false, ['ID', 'NAME', 'PROPERTY_ARTICLE']);while ($row = $res->GetNext()) { // GetNext экранирует значения echo $row['NAME'], "\n";}// Fetch отдаёт данные как есть: для вывода в разметку их экранируют сами// SelectedRowsCount() отдаёт число найденных строк выборкиВыборка возвращает объект результата с методами чтения. Выбор между двумя методами определяет, экранированы значения или нет, и путать их - готовая уязвимость на выводе.
Проверяем результат вместо перехвата исключения:
if (!CIBlockElement::Delete($id)) { global $APPLICATION; $ex = $APPLICATION->GetException(); echo $ex ? $ex->GetString() : 'ошибка без описания';}// старое ядро возвращает ложь и кладёт текст ошибки рядом// у части методов ошибка попадает в глобальную переменную $strErrorИсключений от методов старого ядра ждать совершенно бессмысленно. Проверка возвращаемого значения и чтение объекта ошибки - единственный способ узнать, что операция не прошла.
Собираем данные одним запросом вместо цикла:
$ids = array_unique(array_column($rows, 'PRODUCT_ID'));$res = CIBlockElement::GetList([], ['ID' => $ids], false, false, ['ID', 'NAME']);$names = [];while ($row = $res->Fetch()) { $names[$row['ID']] = $row['NAME']; }// запрос в цикле на сотне позиций - сотня запросов и медленная страница// фильтр по массиву идентификаторов собирается в одно условие IN// список идентификаторов чистят от повторов заранееЗапросы в цикле - самая частая находка в чужом проекте. Один запрос с фильтром по списку идентификаторов и последующая связка по ключу решают ту же задачу за одно обращение к базе.
Пишем новый код на новом ядре:
use Bitrix\Main\Loader;use Bitrix\Iblock\ElementTable;
Loader::includeModule('iblock');$rows = ElementTable::getList([ 'select' => ['ID', 'NAME'], 'filter' => ['=IBLOCK_ID' => 12, '=ACTIVE' => 'Y'],])->fetchAll();// новое ядро отдаёт массивы и объекты результата с ошибкамиНовое ядро даёт типизированные результаты и понятные ошибки. Смешивать оба ядра в одном файле допустимо, а вот смешивать их в одном методе - верный способ запутать следующего разработчика.
Оборачиваем массовые операции транзакцией:
global $DB;$DB->StartTransaction();try { foreach ($ids as $id) { /* правка */ } $DB->Commit();} catch (\Throwable $e) { $DB->Rollback(); // частичная правка хуже отсутствия правки}Массовое удаление и правка без транзакции оставляют данные в половинчатом состоянии. Откат по ошибке возвращает всё как было, и разбирать последствия не приходится.
Защищаем подключаемые файлы:
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) { die(); }// первая строка каждого файла, не предназначенного для прямого вызоваФайл шаблона, открытый по прямому адресу, выполняется без окружения платформы. Строка защиты стоит буквально копейки и закрывает целый класс неприятных сюрпризов.
Ограничения
Полный перевод старого проекта на новое ядро обычно не окупается. Работающий легаси без задач переписывают только в одном случае: когда он мешает конкретной новой задаче.
Часть задач в новом ядре решить пока попросту нечем. Административные списки, изменение размера картинок и агенты остаются на старом API, и это нормально.
Смешение ядер требует дисциплины. Один метод пишут в одном стиле, иначе следующий разработчик тратит время не на задачу, а на понимание, какое ядро сейчас перед ним.
Правки файлов ядра при приёмке проекта проверяют отдельным шагом. Они переживут ровно до следующего обновления, а найденные заранее - переносятся в каталог решений спокойно.
Типичные проблемы
Ошибка о неизвестном классе на первой же строке.
Нужный модуль не подключён или его подключение не проверено. Без успешного подключения классы модуля просто не существуют для текущего запроса.
На странице появились экранированные символы вместо текста.
Значение прочитано методом с экранированием и экранировано повторно уже при выводе. Двойное экранирование выдаёт себя на странице сразу.
Данные из выборки попадают в разметку без экранирования.
Строка прочитана методом без экранирования, а на выводе про это забыли. Такой код - открытая дверь для внедрения чужой разметки.
Операция не выполнилась, но код продолжил работу.
Результат вызова никто не проверил, а исключений старое ядро не бросает вовсе. Текст ошибки при этом лежит в объекте ошибки самого приложения.
Правки в файлах ядра исчезли после обновления.
Правился каталог самой платформы вместо каталога решений проекта. Обновление возвращает файлы ядра к исходному виду без предупреждения.
Частые вопросы
Нужно ли переписывать весь старый код?
Нет. Переводят то, что и так меняют по задаче: переписывание работающего кода без причины даёт риск без выгоды.
Можно ли смешивать оба ядра?
Да, в проекте они живут рядом, и часть задач иначе не решить. Смешивать стоит по файлам и методам, а не внутри одного метода.
Чем два метода чтения строки отличаются?
Один экранирует значения, другой отдаёт их как есть. Первый удобен для вывода в разметку, второй - для обработки данных.
Где искать текст ошибки старого метода?
В объекте ошибки приложения или в глобальной переменной ошибки. Возвращаемая ложь означает отказ, а подробности лежат отдельно.
Что делать с правками в файлах ядра?
Переносить в каталог решений: свои компоненты, свой модуль, обработчики событий. До ближайшего обновления такие правки живут, после - исчезают.
Смежное
- Локальный стенд - оглавление подтемы
- Код, совместимый с PostgreSQL: кавычки, функции, индексы - что делать с прямым SQL перед сменой базы
- Приёмка чужого проекта: инвентарь, правки ядра, карта рисков - с чего начинают на чужом проекте
- Выборки из инфоблоков: GetList, ORM и разделы - оба ядра на одной задаче
- Обработчик события: регистрация, аргументы, отмена действия - старые и новые события
- Медленный запрос к базе: поиск, план, индекс - что делать с запросами в цикле
- Транзакции при записи: откат, границы, что не откатится - транзакции в новом ядре
- Архитектура 1С-Битрикс: структура файлов, ядро, жизненный цикл - устройство проекта целиком
- Старое ядро 1С-Битрикс - справочник по легаси-классам