Автодополнение и подсказки IDE в 1С-Битрикс - заглушки и аннотации
Автодополнение, подсказка параметров и переход к определению - обычная часть работы в современной IDE, и на большинстве PHP-проектов она просто работает из коробки. На 1С-Битрикс - нет: часть платформенных классов редактор не видит вообще, часть подсказывает наполовину, а подсказки для собственных ORM-сущностей не появляются, пока их не сгенерировать отдельной командой. Разберём, что этому мешает технически и как устроены заглушки для IDE. Заодно - почему на D7 в редакторе работать удобнее, чем на старом ядре. И что делать с типами в собственном коде. Без этого богатая часть возможностей IDE на Битрикс- проекте простаивает.
Как это работает
Какие классы вообще существуют - решается во время выполнения, а не заранее.
По официальной документации об автозагрузке классы модуля становятся доступны
только после успешного вызова Loader::includeModule(). Именно тогда
подключаются include.php и lib/autoload.php модуля. Только там происходят
вызовы Loader::registerNamespace() или Loader::registerAutoLoadClasses().
Они и связывают пространство имён Bitrix\Iblock\... с папкой на диске. Это
обычный код, а не декларативная PSR-4-секция composer.json,
которую любой анализатор читает без выполнения программы. Дальше добавляется
вопрос конкретного проекта. Каких модулей на диске просто нет, потому что они не
куплены или не установлены в этой редакции. Отсутствие catalog, crm или
bizproc - не редкость. И держат ли /bitrix/ рядом с /local/ в рабочей
копии редактора: многие эту папку сознательно не версионируют. В обоих случаях
результат для IDE один: файла с классом нет там, куда она смотрит, - смотреть
попросту не на что.
Часть методов ORM не существует в виде кода. У объектов и коллекций D7 ORM
именованные геттеры и сеттеры в классе не объявлены. Речь про EntityObject и
Collection, а также вызовы вроде getTitle() и setTitle(). По документации
об аннотациях их обрабатывает __call() во время выполнения. Основанием служат
поля, перечисленные в getMap() сущности. Открыть файл класса и найти там
getTitle нельзя - его там нет и не будет, платформа синтезирует поведение на
лету. Для статического анализатора метод, которого нет в исходном коде, не
существует вообще: единственный способ дать IDE знать о нём - сгенерировать
отдельный файл с настоящим объявлением. Этим занимается команда orm:annotate,
подробно разобранная в статье про D7 ORM, - здесь важен сам факт: без генерации
подсказки для объектов собственных сущностей не появятся, сколько ни жди.
Глобальные объекты объявлены без типа. $APPLICATION, $USER и $DB -
обычные переменные PHP. Запись global $APPLICATION; внутри функции сама по
себе не сообщает редактору, что перед ним объект CMain. У $USER это CUser,
у $DB - CDatabase. Устройство этих объектов и момент их появления в прологе
разобраны в статье про старое ядро. Раз тип нигде не объявлен формально, IDE
либо распознаёт переменную эвристически по имени (часть современных редакторов
так умеет), либо ждёт явной подсказки - однострочного докблока прямо над
использованием. Это приём самого языка PHPDoc, а не функция одной конкретной
IDE, поэтому он одинаково работает и в PhpStorm, и в редакторах на движке
Intelephense, и в любом другом инструменте, который умеет читать докблоки.
У старого API нет объявленных типов. Методы вроде
CIBlockElement::GetList() появились до того, как в PHP вообще завезли
скалярные типы параметров и типы возврата, и сигнатуры так и остались без них.
Класс и метод IDE честно находит, файл никуда не делся. Но дальше цепочка
обрывается: результат Fetch() это обычный array. Состав его ключей нигде не
описан. Для анализатора $row['NAME'] - обращение к элементу неизвестного
массива. У D7 в этом смысле принципиально иначе. Поля сущности описаны объектами
IntegerField, StringField и другими, с именем и типом. У растущей части
методов объявлены типы возврата. Результат операций - предсказуемые классы
Result, AddResult, UpdateResult с методами isSuccess() и getId(), а не
безымянный массив. Это самостоятельный довод в пользу D7 для нового кода - не
только про безопасность и производительность, но и про то, насколько быстро
набирается код с подсказками под рукой.
Отсюда и берутся заглушки для IDE. Индексатору достаточно найти файл с
сигнатурой класса - выполнять код не требуется. Поэтому сообщество решает
проблему буквально: собирает отдельную папку файлов, которые по именам классов и
методов повторяют платформу, но с пустыми телами вместо реальной логики. Такую
папку не кладут ни в /local/, ни тем более в /bitrix/. Её подключают только
на уровне настроек редактора, отдельной библиотекой для индексации. В PhpStorm
это раздел PHP Include Path. Из пути, который реально выполняет PHP, папку
физически исключают. Смешивать эти два мира небезопасно
- как это выглядит на практике и что случится, если заглушка всё же попадёт в рабочий код, - в примере ниже.
Примеры
1. Модуль ещё не подключён к запросу
if (\Bitrix\Main\Loader::includeModule('iblock')) { $rs = \CIBlockElement::GetList([], ['=IBLOCK_ID' => 5]);}У IDE может не быть проиндексированного файла модуля iblock. Так бывает, когда
модуль не установлен в этой редакции или /bitrix/ не входит в рабочую копию
редактора. Тогда CIBlockElement подсвечивается как неразрешённая ссылка. На
сервере, где модуль установлен, этот же код отработает без единой ошибки.
Проблема в том, что по одной только красной подсветке разработчик не отличит
«класса действительно нет» от «класс просто не попал в индекс».
2. Геттеры ORM-объекта во время выполнения
$project = ProjectTable::query() ->where('ID', 17) ->fetchObject();
echo $project->getTitle();Без сгенерированных аннотаций IDE не предложит getTitle() в списке методов
$project-> и в лучшем случае промолчит, в худшем - подчеркнёт вызов как
ошибку. При этом код рабочий: getTitle() существует, просто не в виде
объявленного метода, а как результат __call() поверх поля TITLE из
getMap(). Команду php bitrix.php orm:annotate запускают из папки /bitrix/
для нужного модуля. Модуль указывают ключом -m vendor.module: по умолчанию
сканируется только main. После запуска в файле аннотаций появляется настоящее
объявление класса сущности. Метод getTitle() начинает предлагаться и
проверяться как любой обычный.
3. Как устроена заглушка для IDE
// заглушка для индексатора, НЕ часть рабочего проектаclass CIBlockElement{ /** * @return CDBResult */ public static function GetList($arOrder = [], $arFilter = [], $bGroupBy = false, $arNavParams = false, $arSelect = []) { }}Тело метода пустое: для реальной работы сайта такой файл не значит ничего, и
именно поэтому его держат отдельно от /local/ и /bitrix/, подключая только
как библиотеку в настройках редактора. Индексатор находит сигнатуру и
подсказывает параметры и тип результата, а рабочий код по-прежнему выполняет
настоящий платформенный файл, подключённый штатным порядком через
Loader::includeModule(). Готовые наборы таких файлов делает сообщество, а не
платформа, и это стоит держать в голове: перед использованием любого набора
стоит проверить, под какую версию ядра он собран и давно ли обновлялся. Два
примера принципа на GitHub - bxApiDocs
(копирует структуру /bitrix/modules/ файлами с сигнатурами; архивирован с 2022
года - это иллюстрация подхода, а не готовая к установке рекомендация) и
bitrix-idehelper, который
ставится через composer. Оба - сторонние проекты, не часть платформы, и
подключаются к проекту только как библиотека для индексатора.
4. Аннотация собственного массива
/** * @return array{ID: int, NAME: string, DETAIL_PAGE_URL: string}|null */function getElementRow(int $elementId): ?array{ $rs = \CIBlockElement::GetList( [], ['=ID' => $elementId], false, false, ['ID', 'NAME', 'DETAIL_PAGE_URL'] );
$row = $rs->Fetch();
return $row ?: null;}Сам CIBlockElement::GetList() как возвращал нетипизированный CDBResult, так
и возвращает - платформенную заглушку изнутри не поменять. Но код, который
вызывает getElementRow(), получает конкретные ключи: набирая
getElementRow($id)[', разработчик увидит подсказку ID, NAME,
DETAIL_PAGE_URL, а опечатка в имени ключа подсветится сразу. Оборачивать
платформенные вызовы в такие типизированные функции стоит везде, где массив с
нетипизированным содержимым выходит за пределы одной функции.
Что подсказывает IDE, а что нет
| Что | Автодополнение | Особенности |
|---|---|---|
Классы D7 (Bitrix\Main\...) установленного и подключённого модуля | Да | если файл модуля физически есть в путях, которые индексирует IDE |
| Классы модуля, которого нет в проекте или в этой редакции | Нет | файла нет на диске - индексировать нечего |
Классы старого ядра (CIBlockElement, CUser, CFile) | Частично | сигнатура видна, если файл на месте; типы параметров и возврата почти нигде не объявлены |
$APPLICATION, $USER, $DB без докблока | Нет | обычная нетипизированная глобальная переменная |
$APPLICATION, $USER, $DB с @var CMain $APPLICATION | Да | подсказка вручную, работает в любом редакторе, понимающем PHPDoc |
Статические методы своей XxxTable (query(), getList(), add()) | Да | обычный объявленный класс-наследник DataManager |
Геттеры и сеттеры сгенерированного EO_Xxx без аннотаций | Нет | методы существуют только во время выполнения через __call() |
Те же геттеры и сеттеры после orm:annotate | Да | нужно перезапускать при каждом изменении getMap() |
Массив из CIBlockElement::GetList()->Fetch() напрямую | Нет | Fetch() возвращает обычный array, состав ключей нигде не описан |
Тот же массив за собственной функцией с @return array{...} | Да | типизируете сами, платформа тут ни при чём |
| Заглушки сообщества, подключённые как библиотека для IDE | Частично | это не часть платформы: сверяйте версию ядра и дату последнего обновления набора |
Частые ошибки
«Class not found» для класса, который точно работает на сервере. Симптом:
CIBlockElement или \Bitrix\Catalog\Model\Product подчёркнуты как
неразрешённая ссылка, хотя код рабочий. Причина - файла модуля нет в путях,
которые индексирует IDE: либо модуль не установлен в этой редакции, либо
/bitrix/ не входит в рабочую копию редактора.
Автодополнение обрывается после первого вызова. Симптом: после
$rs->Fetch() или $row[ редактор ничего не предлагает. Причина - Fetch()
возвращает обычный array без объявленного состава ключей, и дальше для
анализатора это неизвестные данные.
IDE предлагает метод, которого не существует, или не предлагает существующий
геттер сущности. Симптом: автодополнение расходится с реальным getMap()
сущности. Причина - аннотации устарели: поле переименовали или добавили в
сущность, а orm:annotate не перезапускали.
Сайт падает с ошибкой о повторном объявлении класса после настройки IDE.
Симптом: Cannot redeclare class CIBlockElement. Причина - файл-заглушка
случайно оказался в пути, который реально подключает PHP (например, его положили
в /local/ вместо отдельной библиотеки для редактора), и рабочий код на сервере
встретил и заглушку, и настоящий класс.
$APPLICATION или $USER подсвечены как неизвестный тип, хотя весь
/bitrix/ на месте. Симптом: у объекта не предлагается ни один метод. Причина
- глобальная переменная объявлена без подсказки типа:
global $APPLICATION;сам по себе для IDE ничего не значит.
Частые вопросы
Нужно ли подключать файлы-заглушки к рабочему сайту, чтобы автодополнение заработало?
Нет, и делать это не стоит. Заглушки существуют только для индексатора редактора: их подключают библиотекой в настройках IDE, а из пути, который реально выполняет PHP, исключают. Если такой файл случайно окажется в /local/ или /bitrix/ рабочего сайта, при удачном стечении обстоятельств он перекроет настоящий класс пустыми методами, а при неудачном - сайт упадёт с ошибкой о повторном объявлении класса.
Почему IDE постоянно подчёркивает $APPLICATION и $USER как ошибку, хотя весь код на месте?
Потому что это обычные нетипизированные переменные PHP. Запись global $APPLICATION; сама по себе не говорит редактору, что перед ним объект класса CMain, а $USER - класса CUser. Самый простой способ подсказать тип - докблок прямо над использованием: @var CMain $APPLICATION. Это общий приём PHPDoc, он работает в любом редакторе, который понимает докблоки, а не только в одной конкретной IDE.
Как получить подсказки для собственной ORM-сущности, а не только для платформенных?
Тем же способом, что и для платформенных - командой orm:annotate из папки /bitrix/, только с указанием своего модуля через ключ -m. По умолчанию команда сканирует лишь модуль main, поэтому без явного указания своя сущность в файл аннотаций не попадёт и геттеры с сеттерами объекта останутся без подсказок.
Правда ли, что писать на D7 в IDE удобнее, чем на старом ядре?
В целом да, и дело не только в архитектуре. Поля D7-сущности описаны объектами с именем и типом, у растущей части методов объявлены типы возврата, а результат операций - предсказуемые классы Result, AddResult, UpdateResult с методами isSuccess() и getId(). Старое ядро в основном писалось до появления типов в PHP, поэтому автодополнение там обрывается на первом же вызове метода без объявленного возврата.
Связанные темы
- Старое ядро - устройство
$APPLICATION,$USERи$DB, о которые чаще всего спотыкается автодополнение - Архитектура платформы -
/local/и/bitrix/, два ядра и жизненный цикл запроса, на которых держится вся эта статья - D7 ORM - полный разбор
orm:annotateи объектов сущности; здесь только его смысл для подсказок IDE - Ядро D7 -
Loader::includeModule()иrequireModule(), от которых зависит видимость классов модуля - Composer и автозагрузка - чем декларативный PSR-4
отличается от динамической регистрации через
registerNamespace() - Раздел Основы