Перейти к содержимому

Автодополнение и подсказки 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()
  • Раздел Основы

Первоисточники