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

Composer и PSR-4 в 1С-Битрикс - vendor, автозагрузка и Loader

Composer - стандартный менеджер зависимостей PHP: ставит сторонние библиотеки и генерирует для них автозагрузчик по PSR-4. У 1С-Битрикс при этом есть собственный, гораздо более старый механизм автозагрузки - Bitrix\Main\Loader, который точно так же находит и подключает класс по имени, но по своим правилам, и настраивается отдельно в каждом модуле платформы. В реальном проекте оба механизма работают одновременно, и от того, где лежит composer.json, как подключён vendor/autoload.php и в каком порядке это происходит относительно собственной автозагрузки платформы, зависит, поднимется ли сайт вообще. Тема всплывает в трёх типичных ситуациях. Первая - в проект добавляют стороннюю библиотеку с Packagist. Вторая - свой код оформляют модулем и выбирают между двумя способами автозагрузки. Третья - сайт после переноса или обновления падает с Class not found там, где вчера всё работало.

Как это работает

Оба автозагрузчика - это просто две функции в одной очереди PHP. Функция spl_autoload_register() добавляет обработчик в общую очередь автозагрузки. Встретив ещё не объявленный класс, PHP опрашивает эту очередь по порядку регистрации. Опрос останавливается, как только очередной обработчик объявил класс. Если ни один обработчик не справился, PHP выбрасывает Error с текстом о том, что класс не найден. Bitrix\Main\Loader и автозагрузчик Composer из vendor/autoload.php - это ровно два таких обработчика в общей очереди, и они ничего не знают друг о друге, кроме порядка, в котором их зарегистрировали.

Loader ищет класс в два шага, и оба реестра заполняет модуль сам. Сначала проверяется явная карта «класс - файл», которую заполняет Loader::registerAutoLoadClasses(). Если класса там нет, идёт поиск по пространствам имён из Loader::registerNamespace(). Работает PSR-4-подобное правило, где каждый \ в namespace превращается в папку. Не нашлось ни там, ни там - ошибка. Оба реестра модуль заполняет в своём include.php, а этот файл подключается автоматически при Loader::includeModule('имя_модуля') - то есть автозагрузка классов конкретного модуля появляется не раньше, чем модуль явно подключили.

PSR-4 у Loader - тот же принцип, что у Composer, но не побайтово тот же стандарт. Оба транслируют пространство имён в путь одинаково: \ - в папку, имя класса - в имя файла. Разница - в регистре букв. Спецификация PSR-4 требует, чтобы регистр имени файла совпадал с регистром имени класса буква в букву. А правила оформления кода 1С-Битрикс требуют обратного. Файлы классов в /lib модуля называют строчными буквами независимо от регистра самого класса. Поэтому класс Loader из пространства Bitrix\Main лежит в файле lib/loader.php, а не lib/Loader.php. Для своего пространства, уже зарегистрированного через registerNamespace(), это работает без сюрпризов. Но тот же код когда-нибудь подключит строгий сторонний PSR-4-автозагрузчик, например после выноса в отдельный composer-пакет. Тогда регистр файла уже будет иметь значение. На файловой системе, чувствительной к регистру, несовпадение даст ту же ошибку про не найденный класс. Обычный Linux-сервер именно такой, в отличие от Windows и macOS по умолчанию.

Composer подключается через .settings.php, а не руками в коде. Путь к своему composer.json прописывают в /bitrix/.settings.php, в секции composer -> value -> config_path - и дальше платформа сама подключает /vendor/autoload.php, требовать его вручную не нужно. Среди шагов пролога это происходит на самом первом шаге - «подключение загрузчика, автозагрузки и утилит», то есть раньше соединения с базой и заведомо раньше init.php. Полный разбор шагов пролога - в статье «Архитектура платформы».

Порядок важен именно поэтому. Если подключить vendor/autoload.php вручную внутри init.php вместо .settings.php, обработчик встанет в очередь автозагрузки только на четвёртом шаге пролога - после соединения с базой и определения сайта. Любой класс из composer-зависимостей, нужный раньше (например, свой класс подключения к БД, указанный прямо в .settings.php), к этому моменту ещё не резолвится, и сайт падает раньше, чем init.php вообще подключается. А подключение vendor/autoload.php из init.php вдобавок к автоматическому даёт два независимых обработчика в очереди. Они могут указывать на две разные копии одних и тех же библиотек. Какая копия победит, решает не разработчик, а порядок регистрации.

Примеры

1. Куда класть composer.json, чтобы не мешать обновлениям

/home/bitrix/composer.json - вне DOCUMENT_ROOT
/home/bitrix/vendor/ - сюда composer install ставит зависимости
/home/bitrix/www/ - это DOCUMENT_ROOT
/home/bitrix/www/bitrix/ - системные файлы платформы, не трогать
/home/bitrix/www/local/ - пользовательский код проекта
// /bitrix/.settings.php - добавить в существующий массив настроек
'composer' => [
'value' => [
'config_path' => '/home/bitrix/composer.json',
],
],
Окно терминала
composer -V
# Composer version 2.8.5 2025-01-21 15:23:40

По умолчанию система ищет composer.json внутри /bitrix/ - но это системный каталог, который обновление платформы перезаписывает целиком, поэтому там файл не место. Официальная рекомендация - вынести его за пределы DOCUMENT_ROOT совсем, как в примере выше. Тогда composer.json не попадает под обновление и не скачивается напрямую по URL. В незащищённом виде это просто текстовый файл со списком зависимостей и версий. Для разведки перед атакой такой список - готовая подсказка. Если сервер не даёт доступа выше DOCUMENT_ROOT (типичная ситуация на части хостингов), официальный запасной вариант - папка /local/composer/, закрытая от внешнего доступа средствами веб-сервера.

2. Стандартные зависимости платформы через composer-merge-plugin

Файл /home/bitrix/composer.json:

{
"require": {
"wikimedia/composer-merge-plugin": "^2.0"
},
"extra": {
"merge-plugin": {
"include": [
"/home/bitrix/www/bitrix/composer-bx.json"
]
}
}
}
Окно терминала
composer install

Ядро само поставляет заготовку - /bitrix/composer.json.example, её удобно взять за основу своего файла. В composer-bx.json перечислены стандартные зависимости самой платформы: они нужны, например, CLI-командам вроде orm:annotate из статьи «D7 ORM» - без предварительно поставленных через composer зависимостей эта команда не запустится вовсе. Плагин composer-merge-plugin на лету подмешивает требования из composer-bx.json к требованиям проекта, и одна команда composer install ставит и то, и другое разом - результат появляется в /home/bitrix/vendor/.

3. Свои классы: Loader::registerNamespace против PSR-4 у Composer

Модуль регистрирует своё пространство имён сам, в include.php:

/local/modules/mycompany.helpers/include.php
// подключается автоматически при Loader::includeModule('mycompany.helpers')
\Bitrix\Main\Loader::registerNamespace(
'MyCompany\Helpers',
__DIR__ . '/lib'
);
/local/modules/mycompany.helpers/lib/pricecalculator.php
namespace MyCompany\Helpers;
class PriceCalculator
{
public static function withVat(float $price): float
{
return round($price * 1.2, 2);
}
}
\Bitrix\Main\Loader::includeModule('mycompany.helpers');
var_dump(\MyCompany\Helpers\PriceCalculator::withVat(1000));
// float(1200)

Класс нашёлся только после includeModule() - до этого вызова его пространство имён вообще не зарегистрировано. Для кода без привязки к жизненному циклу модуля тот же результат даёт обычный PSR-4 в /home/bitrix/composer.json:

{
"autoload": {
"psr-4": {
"MyCompany\\Pricing\\": "libs/pricing/src/"
}
}
}
Окно терминала
composer dump-autoload

После пересборки класс из libs/pricing/src/Calculator.php доступен везде, где подключён vendor/autoload.php, - без единого includeModule(), с самого первого шага пролога. Отсюда и граница уместности. registerNamespace() в модуле берут, когда код должен ставиться и удаляться штатным установщиком, получать версию и попадать в маркетплейс. Этот жизненный цикл разобран в статье «Свой модуль». Composer PSR-4 берут для кода без такого цикла: общих библиотек и обёрток над чужими SDK. Туда же идёт всё, что хочется тестировать обычным PHP-тулингом отдельно от платформы, как в статье «Модульное тестирование».

4. Поздняя автозагрузка ломает раннюю инициализацию

// /bitrix/.settings.php - кастомный класс соединения из собственного composer-пакета
'connections' => [
'value' => [
'default' => [
'className' => '\Acme\Db\LoggingConnection',
'host' => 'localhost',
'database' => 'dbname',
'login' => 'user',
'password' => 'pass',
],
],
],
// без строки ниже автозагрузчик Composer вообще не подключится сам
'composer' => [
'value' => [
'config_path' => '/home/bitrix/composer.json',
],
],

Если убрать секцию composer из .settings.php и вместо неё понадеяться на require_once($_SERVER['DOCUMENT_ROOT'] . '/local/vendor/autoload.php'); в начале init.php, результат - падение ещё до того, как init.php вообще подключится:

PHP Fatal error: Uncaught Error: Class "Acme\Db\LoggingConnection" not found

Класс соединения нужен на шаге установки связи с базой - это второй шаг пролога. init.php - только четвёртый, после определения сайта. Автозагрузчик, подключённый через .settings.php, встаёт в очередь на первом шаге и успевает к моменту, когда класс соединения понадобится; автозагрузчик из init.php - нет.

5. Одна и та же библиотека грузится дважды - и побеждает не та версия

// в проекте: composer require guzzlehttp/guzzle:^7.8, ставится в /home/bitrix/vendor/
// в стороннем модуле с маркетплейса: своя копия ^6.5 в
// /home/bitrix/www/bitrix/modules/partner.module/vendor/, подключена
// модулем напрямую собственным require(), в обход общего composer.json
$client = new \GuzzleHttp\Client();
echo (new \ReflectionClass($client))->getFileName();
/home/bitrix/www/bitrix/modules/partner.module/vendor/guzzlehttp/guzzle/src/Client.php

Вывод показывает не ту версию, что объявлена в composer.json проекта (^7.8). Отвечает версия, которая физически лежит в модуле (^6.5). Автозагрузчик модуля зарегистрировался в очереди spl_autoload_register() раньше и ответил первым. Многие решения с маркетплейса носят собственный vendor/ и подключают его сами именно потому, что не могут полагаться на то, что у проекта вообще настроен Composer. Официального способа заставить оба автозагрузчика использовать одну версию нет - остаётся проверять реальный путь класса через ReflectionClass::getFileName(), когда возникает подозрение на конфликт.

Справочник API

Файлы и точки подключения

МестоНазначениеОсобенности
composer.json вне DOCUMENT_ROOT (например /home/bitrix/)конфигурация зависимостей проектаофициальная рекомендация; запасной вариант - /local/composer/, закрытая от веба
/bitrix/.settings.php, ключ composer -> value -> config_pathпуть к своему composer.jsonбез него автоматического подключения vendor/autoload.php не будет
/vendor/ рядом с composer.jsonсами зависимостисоздаётся composer install; в репозиторий обычно не попадает
/vendor/autoload.phpавтозагрузчик Composerплатформа подключает сама, на первом шаге пролога, если задан config_path
/bitrix/composer.json.exampleобразец с зависимостью от merge-pluginшаблон для своего composer.json
composer-bx.jsonстандартные зависимости самой платформыподключается через wikimedia/composer-merge-plugin (не ниже ^2.0)
composer.lockточные зафиксированные версиикоммитить в приложении обязательно - иначе на другой машине встанут другие версии

Методы Bitrix\Main\Loader

МетодНазначениеОсобенности
Loader::includeModule($moduleName)подключить модуль, его include.php и /lib/autoload.phpbool; автозагрузка классов модуля доступна только после успешного вызова
Loader::registerNamespace($namespace, $path)зарегистрировать пространство имён по PSR-4-подобному правилунесколько путей на одно пространство - с main 20.600.0
Loader::registerAutoLoadClasses($moduleName, $arClasses)явная карта «класс -> файл»проверяется раньше, чем реестр пространств имён; $moduleName может быть null для классов вне модулей
Loader::getDocumentRoot()корень сайтас версии 14.0.0; удобен внутри include.php для построения путей

Команды Composer

КомандаНазначениеОсобенности
composer installпоставить зависимости по composer.lockбез lock-файла ведёт себя как update и создаёт его заново
composer updateпересчитать версии по ограничениям composer.jsonпереписывает composer.lock
composer dump-autoloadпересобрать vendor/autoload.phpобязателен после правки секции autoload, иначе новые классы не найдутся
composer check-platform-reqsсверить PHP и расширения сервера с требованиями пакетовстоит добавить в пайплайн деплоя

Частые ошибки

Class not found сразу после переноса на боевой сервер, хотя локально всё работало. vendor/ обычно не из репозитория (типичная запись в .gitignore), и на сервере либо забыли выполнить composer install, либо выполнили без закоммиченного composer.lock - тогда встали другие версии зависимостей, чем те, на которых разработчик проверял код локально.

После планового обновления платформы пропали composer.json и vendor, сайт лежит. Их положили внутрь /bitrix/ - а это системный каталог, который обновление перезаписывает целиком. Правильное место - вне DOCUMENT_ROOT или, если это невозможно, /local/composer/, закрытая от веб-доступа.

Свой класс из /local/modules/.../lib не находится, хотя файл на месте. Возможных причин три. В include.php модуля не вызван registerNamespace() или registerAutoLoadClasses(). Сам модуль не подключён через Loader::includeModule() до обращения к классу. Либо имя файла не в нижнем регистре: файлы /lib платформа ожидает строчными буквами независимо от регистра имени класса.

После добавления ручного require vendor/autoload.php в init.php посыпались ошибки в неожиданных местах. Автозагрузчик мог уже подключиться платформой через config_path в .settings.php. Тогда повторное подключение из init.php в лучшем случае избыточно. В худшем оно регистрирует второй обработчик на другой каталог vendor/. В очереди spl_autoload_register() побеждает не та копия одноимённого класса, которую ожидал разработчик.

Класс из composer-зависимости нужен раньше, чем что-либо в init.php, и сайт падает ещё до него. Пример - кастомный класс подключения к базе, указанный прямо в .settings.php через connections -> default -> className. Он нужен на шаге соединения с базой, а это на два шага раньше init.php. Если автозагрузчик Composer подключён только в init.php, до этого момента дело просто не доходит.

Пакет ставится и работает у разработчика, а на проде - ошибка платформы или PHP Parse error. Библиотека негласно требует более новый PHP, чем стоит на боевом сервере. Локально версия новее, и Composer резолвит зависимости именно под неё. Официальный минимум для 1С-Битрикс на сегодня - PHP 8.2, но реальный боевой сервер может отставать даже от этого минимума, либо конкретный пакет требовать версию ещё свежее. Несоответствие ловится заранее командой composer check-platform-reqs или явно заданной секцией platform (ключ php) в конфигурации Composer, эмулирующей версию боевого сервера.

Частые вопросы

Обязательно ли ставить Composer в проект на 1С-Битрикс?

Нет. Собственные классы прекрасно грузятся штатным Bitrix\Main\Loader через registerNamespace() или registerAutoLoadClasses(), без единого пакета с Packagist. Composer становится нужен, когда в проект добавляют стороннюю библиотеку либо используют CLI-инструменты самой платформы - например, команда orm:annotate прямо требует предварительно поставленных через composer зависимостей.

Можно ли просто подключить vendor/autoload.php внутри init.php?

Работать будет, но это не тот способ, который предусмотрен платформой, и у него есть цена. Официальный путь - зарегистрировать composer.json через ключ config_path в .settings.php: тогда автозагрузчик подключается платформой на первом шаге пролога, раньше соединения с базой. init.php подключается только на четвёртом шаге, и если composer-классы нужны раньше - например, кастомный класс соединения с базой из .settings.php - до init.php дело просто не доходит.

Куда правильно класть composer.json и vendor в проекте на Битрикс?

За пределами DOCUMENT_ROOT, например в /home/bitrix/ - так рекомендует официальная документация: файл не попадает под обновление платформы и не открывается напрямую по ссылке. Если такой возможности нет, официальный запасной вариант - папка /local/composer/, закрытая от внешнего доступа веб-сервером. Внутрь /bitrix/ класть нельзя ни в каком случае - это системный каталог, и обновление платформы перезапишет его целиком.

Чем автозагрузка Bitrix\Main\Loader отличается от PSR-4 у Composer?

Механика похожая, но не идентичная. Loader ищет класс в два шага: сначала по явной карте registerAutoLoadClasses(), затем по пространствам имён registerNamespace(), зарегистрированным PSR-4-подобным правилом - namespace через обратный слеш превращается в папку. Отличие в регистре: строгий PSR-4 требует, чтобы регистр имени файла совпадал с регистром имени класса буква в букву, а правила платформы требуют называть файлы в /lib модуля строго строчными буквами независимо от регистра класса. Для собственного пространства, зарегистрированного через registerNamespace(), это не проблема, а вот при переносе того же кода под чужой строгий PSR-4-автозагрузчик несовпадение регистра уже может дать ошибку про не найденный класс.

Что делать, если сторонний модуль конфликтует по версии библиотеки с той, что уже стоит в проекте через Composer?

Сначала проверить, не носит ли модуль собственную копию vendor и не подключает ли её сам - многие решения с маркетплейса делают именно так, потому что не могут полагаться на настроенный Composer в проекте. Если копии две, побеждает не версия из composer.json проекта, а та, чей автозагрузчик раньше встал в очередь spl_autoload_register - это проверяется вызовом (new ReflectionClass($object))->getFileName() на реальном объекте библиотеки. Надёжное решение - обновить или заменить конфликтующий модуль; временное - явно понимать порядок подключения и держать версии под контролем через composer.lock.

Связанные темы

  • Архитектура платформы - шаги пролога целиком: где среди них подключается автозагрузка и когда - init.php
  • Стандарты кода - как платформа выборочно опирается на PSR и что это значит для оформления своего кода
  • Свой модуль - структура /lib и include.php, куда попадает registerNamespace() собственного модуля
  • D7 ORM - команда orm:annotate, один из практических поводов ставить Composer в проект
  • Автодополнение и IDE - почему декларативный PSR-4 у Composer виден редактору сразу, а registerNamespace() платформы - только после подключения модуля
  • Модульное тестирование - autoload-dev и PSR-4 для тестируемого кода, отдельно от автозагрузки ядра
  • Окружение и деплой - требования к серверу и версии PHP, с которыми должны совпадать зависимости из composer.json
  • Раздел Основы

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