/bitrix.core.symfony

Базовый функционал для внедрения Symfony в Битрикс

Primary LanguagePHPMIT LicenseMIT

Базовый функционал для внедрения Symfony в Битрикс

Установка

composer.json:

    "repositories": [
        {
            "type": "git",
            "url": "https://github.com/proklung/bitrix.core.symfony"
        }
    ]
composer require proklung/bitrix-core-symfony

Инициализация

В init.php:

use Prokl\ServiceProvider\ServiceProvider;

$serviceProvider = new ServiceProvider('local/configs/services.yaml');

Для обеспечения "преемственности" (похожести) с оригиналом можно задать путь к файлу конфигурации (скажем, bundles.php) бандлов вторым (необязательным) параметром конструктора.

Переменные среды

Предполагается, что переменные среды к моменту инициализации контейнера уже загружены тем или иным способом.

Значимые переменные среды:

  • DEBUG (булево значение - режим отладки), APP_DEBUG - алиас DEBUG, но с большим приоритетом (если одновременно присустствуют DEBUG и APP_DEBUG, то в дело пойдет значение APP_DEBUG).

  • APP_ENV - код окружения. Если код не задан, то будет проинтерпретировано значение DEBUG в смысле - если в режиме отладки, то окружение dev, иначе prod.

Если переменные среды не заданы, то с помощью класса Prokl\ServiceProvider\LoadEnvironment их можно загрузить.

Скажем, в init.php, перед инициализацией контейнера:

    // Параметр конструктора - путь, где лежат файлы .env
    $loader = new \Prokl\ServiceProvider\LoadEnvironment($_SERVER['DOCUMENT_ROOT'] . '/../..');
    $loader->load(); // Загрузка $_ENV
    $loader->process(); // Обработка переменных

Конфигурирование

  1. Опция compile.container в подтягиваемом конфиге - компилировать ли контейнер в файл. Если не задана, то "нет, не компилировать". Имеет смысл для окружения, не равного "dev". Т.е. опция управляет дампированием контейнера на проде.

Место, где хранятся дампы контейнеров: <значение переменной контейнера kernel.cache_dir>/<SITE_ID>/containers

Пути к кэшу и логам

Определяются классом AppKernel. По умолчанию:

  • путь к кэшу (kernel.cache_dir) - /bitrix/cache
  • путь к логам (kernel.logs_dir) - '/../../logs' (два уровня выше DOCUMENT_ROOT - особенности используемой сборки Битрикс)

Чтобы это изменить нужно отнаследоваться от класса AppKernel и переопределить несколько переменных:

use Prokl\ServiceProvider\Services\AppKernel;

class MyKernel extends AppKernel
{
   protected $cacheDir = '/bitrix/cache/mycache';
    
   protected $logDir = '/logs-saver';
}

(второй вариант - отнаследоваться от AppKernel и переопределить методы getCacheDir и getLogDir).

Изменить через наследование класс ядра:

class MyServiceProvider extends ServiceProvider
{
    protected $kernelServiceClass = MyKernel::class;

    protected $cacheDir = '/bitrix/cache/mycache';

}

Второй вариант - отнаследоваться от ServiceProvider и заменить метод getPathCacheDirectory своей логикой.

Поддержка бандлов

Файл конфигурации - /config/standalone_bundles.php. Этот путь можно изменить через конструктор.

Папка, где лежат конфигурации - /local/configs. Конфигурации бандлов - /local/configs/packages.

Проблема с приватными сервисами

Согласно концепции Symfony все сервисы (в идеале) должны быть приватными и инжектиться. Но в кастомном случае часто нужно получать их через хелпер-сервис-локатор. Для превращения нужных сервисов в публичные предлагается такое решение. В общем разделе параметров контейнера появилась опция publicable_services:

parameters:
  publicable_services:
    - 'snc_redis.default'

После компиляции контейнера приватный сервис snc_redis.default станет публичным.

Сепаратные микро-контейнеры

Отдельные контейнеры - со своим конфигом, полностью изолированные (для модулей и т.п.).

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Prokl\ServiceProvider\Micro\AbstractStandaloneServiceProvider;
use Prokl\ServiceProvider\Micro\ExampleAppKernel;

class ExampleMicroServiceProvider extends AbstractStandaloneServiceProvider
{
    /**
     * @var ContainerBuilder $containerBuilder Контейнер.
     */
    protected static $containerBuilder;

    /**
     * @var string $pathBundlesConfig Путь к конфигурации бандлов.
     */
    protected $pathBundlesConfig = '/src/Micro/example.config/standalone_bundles.php';

    /**
     * @var string $configDir Папка, где лежат конфиги.
     */
    protected $configDir = '/src/Micro/example.config/example.config/example.yaml';

     /**
     * @var string $kernelServiceClass Класс, реализующий сервис kernel.
     * Нужен для того, чтобы экземпляры контейнеров в kernel сервисе не перемешивались.
     */
    protected $kernelServiceClass = ExampleAppKernel::class;

}

Пример класса ExampleAppKernel:

use Prokl\ServiceProvider\Micro\AbstractKernel;

class ExampleAppKernel extends AbstractKernel
{
    protected static $kernelContainer;
}

Где надо - инициализация:

$micro = new ExampleMicroServiceProvider('src/SymfonyDI/Micro/example.config/example.yaml');

Хэлпер container заточен под работу с микро-сервис-провайдерами:

var_dump(container($micro)->getParameter('example'));

Автозапуск сервисов

Чтобы сервис запустился автоматически после инициализации контейнера, он должен быть помечен тэгом service.bootstrap.

  app.options:
    class: Prokl\Services\AppOptions
    arguments: ['%kernel.environment%', '@parameter_bag']
    tags: ['service.bootstrap']

Поддерживается приоритет запуска. Тогда надо так:

  app.options:
    class: Local\Services\AppOptions
    arguments: ['%kernel.environment%', '@parameter_bag']
    tags: 
      - { name: 'service.bootstrap', priority: 100 }

Сервис с приоритетом 100 запустится раньше сервиса с приоритетом 200.

Автоматическая подвязка на события Битрикс

Тэг: bitrix.events.init.

  1. event - название события.
  2. method - метод-обработчик в сервисе
  3. module - модуль события
  4. sort - сортировка
  admin_entity_edit.event_init:
    class: Local\Bitrix\PsModuleInitializer
    tags:
      - { name: bitrix.events.init, module: ps.d7, event: onGetEntityList, method: registerEntities, sort: 0 }

Автоматическое подхватывание расширений Twig

Тэг twig.extension.

  service.twig.parameter:
    class: Prokl\Bundles\ParameterBundle\Twig\ParameterExtension
    public: true
    arguments:
      - '@service.parameter'
    tags:
      - { name: twig.extension }

Сервисы по умолчанию

Автоматом регистрируются сервисы:

  • service_container (и alias) - сервис-контейнер целиком
  • app.request - конвертор глобалов в Request
  • синонимы сервиса kernel
  • delegated_container_manipulator - манипулятор делегированными контейнерами.
  • bitrix.request.instance - Экземпляр битриксового Request
  • bitrix.response.instance - Экземпляр битриксового Response
  • bitrix.request - Symfony Request, полученный из битриксового
  • bitrix.request.psr7 - Битриксовый Request, приведенный к PSR-7
  • bitrix.response - Symfony Response, полученный из битриксового
  • bitrix.response.psr7 - Битриксовый Response, приведенный к PSR-7
  • psr17.http_factory - HttpFactory стандарта PSR-17
  • psr18.http_client - Http client стандарта PSR-18

Хэлперы

  1. container() - отдает экземпляр контейнера (выступает в роли сервис-локатора):
$kernel = container()->get('kernel');
  1. delegatedContainer() - отдает экземпляр манипулятора (реализующего интерфейс Symfony\Component\DependencyInjection\ContainerInterface) делегированными контейнерами.
$moduleService = delegatedContainer()->get('my_module_id.service');

В контейнере делегированный контейнер помечается тэгом delegated.container (их может быть сколь угодно много):

  module_notifier_container:
    class: Symfony\Component\DependencyInjection\ContainerInterface
    factory: ['Proklung\Notifier\DI\Services', 'getInstance']
    tags:
      - { name: 'delegated.container' }

Делегированный контейнер - автономный контейнер, сформированные в модуле, плагине и тому подобных местах.

Импорт в контейнер сервисов битриксового сервис-локатора

Автоматом подтягиваются в контейнер сервисы из битриксового сервис-локатора. Формат, секция services из /bitrix/.settings.php и /bitrix/.settings_extra.php. Также загрузчик пробегает по списку установленных модулей и подцепляет их тоже.

Для отдельных сервис-контейнеров (отнаследованных от AbstractStandaloneServiceProvider) такая загрузка не производится.

Если эта фича не нужна, то нужно отнаследоваться от ServiceProvider и заглушить метод loadBitrixServiceLocatorConfigs.

class MyServiceProvider extends ServiceProvider
{
    /**
     * {@inheritDoc}
     */
    protected function loadBitrixServiceLocatorConfigs(DelegatingLoader $loader) : void
    {
    }
}

BitrixSettingsDiAdapter

Адаптер-импортер настроек битриксового сервис-локатора (.settings.php) в симфонический контейнер.

  • importParameters(ContainerInterface $container, array $settings, ?string $section = null) - импорт параметров. section - если задано, то параметры лягут в именованную секцию параметров контейнера.
  • importServices(ContainerInterface $container, array $services) - импорт сервисов. Формат

Считываются конфиги в Битриксе как-то так:

use Bitrix\Main\Config\Configuration;

$this->config = Configuration::getInstance()->get('my_config') ?? [];
// Из модуля
$this->parameters = Configuration::getInstance('my.module')->get('parameters') ?? [];
$this->services = Configuration::getInstance('my.module')->get('services') ?? [];

Совместимость с новым механизмом битриксовых роутов

С версии 21.400.0 (от 16.07.2021) главного модуля в Битриксе появился сносный роутер.

Чтобы использовать в этом контексте контейнер нужно:

  • В файле описания маршрутов (например, /local/routes/web.php) в самом верху подключить ядро.

Это важно, т.к. без этого сервис-провайдер завалится на стадии подключения файла с роутами; они подключаются раньше инициализации ядра. И, если эту проблему еще можно решить, отключив проверку классов сервисов на существование, то запускающиеся автоматом сервисы по тэгу service.bootstrap обломятся стопроцентно.

use Local\ExampleBitrixActionController;
use Prokl\ServiceProvider\ServiceProvider;
use Bitrix\Main\Routing\Controllers\PublicPageController;
use Bitrix\Main\Routing\RoutingConfigurator;

require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");

$container = ServiceProvider::instance();

return function (RoutingConfigurator $routes) use ($container) {

    $routes->get('/countries3/{country}/', [$container->get(ExampleBitrixActionController::class), 'cacheAction'])
            ->default('country', 'Russia')
            ->name('first_bitrix_route')
    ;
    
    $routes->get('/', new PublicPageController('/index.php')); // Старый роут на статике.

};

Класс битриксового контроллера (ExampleBitrixActionController) с заточкой под DI:

namespace Local;

use Bitrix\Main\Engine\Contract\RoutableAction;
use Bitrix\Main\Engine\Controller;
use Symfony\Component\HttpKernel\KernelInterface;

class ExampleBitrixActionController extends Controller implements RoutableAction
{
    /**
     * @var KernelInterface $kernel
     */
    private $kernel;

    public function __construct(KernelInterface $kernel)
    {
        $this->kernel = $kernel;
        parent::__construct();
    }

    /**
     * @return string|Controller
     */
    public static function getControllerClass() {
        return ExampleBitrixActionController::class;
    }

    /**
     * @return string
     */
    public static function getDefaultName() {
        return 'testingAction';
    }

    public function cacheAction(string $country)
    {
        return ['cacheDir' => $this->kernel->getCacheDir(), 'country' => $country];
    }

    public function configureActions()
    {
        return [
            'cache' => [
                'prefilters' => [], 'postfilters' => [],
            ],
        ];
    }
}

Описывается сервисом так:

  Local\ExampleBitrixActionController:
    arguments: ['@kernel']

Ничего революционного, но так можно получить нормально-сконфигурированный класс контроллера, со всякими зависимостями и т.п.