Skip to main content

Routing

Подробное описание модернизации функционала маршрутизации Битрикс. Переход от нативного способа, к работе через функционал атрибутов php.

Атрибуты PHP: https://www.php.net/manual/ru/language.attributes.overview.php

Роутинг в Битрикс: https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=013764&LESSON_PATH=3913.3516.5062.13764

Контроллеры в Битрикс: https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=03750&LESSON_PATH=3913.3516.5062.3750

Функционал роутинга доступен в модуле main начиная с версии 21.400.0. Для пользовательских модулей использование собственных роутов в папке модуля на данный момент не предусмотрено.

SetUp

Описание настройки конфигурации работы с функционалом роутинга Битрикс.

Копируем файл .settings.php в директорию /local и добавляем в возвращаемый массив следующую структуру:

    'routing' => [
        'value' => [
            'config' => [
                'web.php',
                'api.php'
            ]
        ]
    ],

С версии 24.100.0 главного модуля файлы настроек .settings.php и .settings_extra.php могут быть размещены в папке /local , а файл dbconn.php — в папке /local/php_interface.

Создаем файлы web.php и api.php в директории /local/routes со следующим содержанием:

<?php declare(strict_types=1);
use Bitrix\Main\Routing\RoutingConfigurator;

return function (RoutingConfigurator $routes) {

};

Файлы с конфигурацией маршрутов по умолчанию располагаются в папках /bitrix/routes/ и /local/routes/.

Помещаем кастомизированный файл роут обработчика в директорию /local:

//todo: добавить код файла

Для запуска новой системы роутинга нужно перенаправить обработку 404 ошибок на файл routing_index.php в файле .htaccess:

#RewriteCond %{REQUEST_FILENAME} !/bitrix/urlrewrite.php$
#RewriteRule ^(.*)$ /bitrix/urlrewrite.php [L]
RewriteCond %{REQUEST_FILENAME} !/local/routing_index.php$
RewriteRule ^(.*)$ /local/routing_index.php [L]


How to use

Как использовать атрибуты для декларирования обработки методов в маршрутах.

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

<?php declare(strict_types=1);
namespace Umsoft;

#[\Umsoft\Attributes\GroupAttribute(group: 'example')]
class Example
{
    #[\Umsoft\Attributes\RouteAttribute(
        uri: 'foo',
        method:  \Umsoft\Attributes\HttpMethod::GET
    )]
    public static function fooAction(): void {
        echo __CLASS__ . ':' . __FUNCTION__ . PHP_EOL;
    }

    #[\Umsoft\Attributes\GroupAttribute(group: 'api')]
    #[\Umsoft\Attributes\RouteAttribute(
        uri: 'zoo/',
        method:  \Umsoft\Attributes\HttpMethod::GET
    )]
    public static function zooAction(): void {
        echo __CLASS__ . ':' . __FUNCTION__ . PHP_EOL;
    }
}
\Umsoft\Attributes\GroupAttribute - задает группу и префикс маршрута.
\Umsoft\Attributes\RouteAttribute - задает маршрут и метод обработки.

Все методы класса Example объявленные как маршруты, будут иметь префикс /example/ в своём uri.

Метод fooAction будет доступен по следующему uri маршруту: /example/foo

Метод zooAction будет доступен по следующему uri маршруту: /example/api/zoo/

Life Cycle

Детально описание жизненного цикла модернизированного скрипта обработчика маршрутов.

Начало php скрипта

<?php declare(strict_types=1);

Инициализируется логика основного модуля системы

require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/start.php';

\Bitrix\Main\Application::getDocumentRoot() -- Класса приложение не существует до момента инициализации ядра, по этой причине нет возможности использовать нативные методы ядра Битрикс. Получение значении директивы корня сервера исполняемый директории выполняется через нативный php $_SERVER['DOCUMENT_ROOT']

Инициализируется HTTP приложение Битрикс
$application = \Bitrix\Main\Application::getInstance();
$application->initializeExtendedKernel([
    'get' => $_GET,
    'post' => $_POST,
    'files' => $_FILES,
    'cookie' => $_COOKIE,
    'server' => $_SERVER,
    'env' => $_ENV
]);

Подключается файл пролога, так как он используется в нативном скрипте в 3-х из 4-х случаев и содержит определение автозагруженных классов и классов зарегистрированных модулей, а также автоматически подключает файлы init.php

require_once \Bitrix\Main\Application::getDocumentRoot() . '/bitrix/modules/main/include/prolog_before.php';

// todo:  ReflectionClass & ReflectionMethod

Создаются объекты конфигуратора маршрутов и маршрутизатора, далее добавляются в HTTP приложение Битрикс 

$routes = new \Bitrix\Main\Routing\RoutingConfigurator();
$router = new \Bitrix\Main\Routing\Router();
$routes->setRouter($router);
$application->setRouter($router);

Объявляется переменная массив для хранения путей файлов с конфигурацией маршрутов

$files = [];

Добавляются пути к пользовательским файлам с маршрутами из файла конфигурации .settings.php

/** @var array{config: array{string} } $routingConfig
 * config => [  0 => 'web.php', ... ];
 */
$routingConfig = \Bitrix\Main\Config\Configuration::getInstance()->get('routing');

if (!empty($routingConfig['config']))
{
    /** @var string[] $fileNames [ 'web.php', 'api.php' ] */
    $fileNames = $routingConfig['config'];

    foreach ($fileNames as $fileName)
    {
        foreach (['local', 'bitrix'] as $vendor)
        {
            if (file_exists(\Bitrix\Main\Application::getDocumentRoot() .'/'.$vendor.'/routes/'.basename($fileName)))
            {
                // expect: SITE_DIR/local/routes/web.php
                // expect: SITE_DIR/local/routes/api.php
                $files[] = \Bitrix\Main\Application::getDocumentRoot() .'/'.$vendor.'/routes/'.basename($fileName);
            }
        }
    }
}

Добавляются системные маршруты

if (file_exists(\Bitrix\Main\Application::getDocumentRoot() . '/bitrix/routes/web_bitrix.php'))
{
    //expect: файл отсутствует на сервере
    $files[] = \Bitrix\Main\Application::getDocumentRoot() . '/bitrix/routes/web_bitrix.php';
}

Устанавливаются конфигурации из файлов маршрутов

foreach ($files as $file)
{
    // return function (RoutingConfigurator $routes) функция обертка с конфигом роутов
    // {closure} - замыкание из файлов с роутингом
    $callback = include $file;

    // устанавливаются конфигурации \RoutingConfigurations, в $routes
    $callback($routes);
}

Регистрируются маршруты

// \RoutingConfigurations превращаются в роуты $routes => \Bitrix\Main\Routing\Route[]
$router->releaseRoutes();

Кэшируются маршруты файлов и конфигурация объекта роутера

\Bitrix\Main\Routing\CompileCache::handle($files, $router);

Выполняется поиск совпадения текущего маршрута из \HttpRequest в маршрутах роутера

// match request
$request = \Bitrix\Main\Context::getCurrent()->getRequest();
// маршрут \Route или пустота (void)
$route = $router->match($request);

Если совпадение найдено

Поток исполнения определяет один из вариантов типа роут-обработчика:

  1. PublicPageController - обработчик для совместимости нативных страниц php с подключенной бизнес логикой, например компонент и html + js + php код.
  2. \Closure - функция замыкания, любая функция на исполнение, которую возвращает обработчик.
  3. array - массив с названием класса и наименование Action метода, который будет вызван на исполнение. Пример: 
    [\Umsoft\Example::class, 'foo']
  4. string - строка с наименованием класса и метода для запуска. Пример: '\Umsoft\Example:foo'
  5. Любой другой тип:
// любой другой тип контроллера, выкидываем исключение
throw new \Bitrix\Main\SystemException(sprintf(
  'Unknown controller `%s`', $controller
));

Код исполнения:

//region Если роутер найден
if ($route !== null)
{
    // устанавливаем маршрут в инстанс приложения Битрикс
    $application->setCurrentRoute($route);

    // copy route parameters to the request
    if ($route->getParametersValues())
    {
        foreach ($route->getParametersValues()->getValues() as $name => $value)
        {
            //если есть параметры GET запроса ?, устнавиваются в глобальные переменные.
            // $name - параметр, $value - значение
            $_GET[$name] = $value;
            $_REQUEST[$name] = $value;
        }
    }

    $_SERVER["REAL_FILE_PATH"] = '/bitrix/routing_index.php';
    // получаем функцию-контроллер
    // callable , имя функции в роутере, в файле роута, пример [':class', 'method'];
    $controller = $route->getController();

    // тип контроллера - Публичная страница
    if ($controller instanceof \Bitrix\Main\Routing\Controllers\PublicPageController)
    {
        require_once \Bitrix\Main\Application::getDocumentRoot() . '/bitrix/modules/main/classes/general/virtual_io.php';

        $_SERVER["REAL_FILE_PATH"] = $controller->getPath();

        /** @var string $path Путь к исполняемому файлу */
        $path = \Bitrix\Main\Application::getDocumentRoot() . $controller->getPath();

        $physicalFileName = (new \Bitrix\Main\IO\File(
            path: $path
        ))->getPhysicalPath();

        if(!is_string($physicalFileName))
            throw new \Bitrix\Main\IO\InvalidPathException($path);

        require_once $physicalFileName;
        die;
    }
    // тип контроллера - Функция/замыкание
    elseif ($controller instanceof \Closure)
    {
        $binder = \Bitrix\Main\Engine\AutoWire\Binder::buildForFunction($controller);

        // pass current route
        $binder->appendAutoWiredParameter(new \Bitrix\Main\Engine\AutoWire\Parameter(
            \Bitrix\Main\Routing\Route::class,
            fn () => $route
        ));

        // pass request
        $binder->appendAutoWiredParameter(new \Bitrix\Main\Engine\AutoWire\Parameter(
            \Bitrix\Main\HttpRequest::class,
            fn () => \Bitrix\Main\Context::getCurrent()->getRequest()
        ));

        // pass named parameters
        $binder->setSourcesParametersToMap([
            $route->getParametersValues()->getValues()
        ]);

        // init kernel
        require_once \Bitrix\Main\Application::getDocumentRoot() . '/bitrix/modules/main/include/prolog_before.php';

        // call
        $result = $binder->invoke();

        // send response
        if ($result !== null)
        {
            if ($result instanceof \Bitrix\Main\HttpResponse)
            {
                // ready response
                $response = $result;
            }
            elseif (is_array($result))
            {
                // json
                $response = new \Bitrix\Main\Engine\Response\Json($result);
            }
            else
            {
                // string
                $response = new \Bitrix\Main\HttpResponse();
                $response->setContent($result);
            }

            $application->getContext()->setResponse($response);
            $response->send();
        }

        // terminate app
        $application->terminate(0);

    }
    // тип котроллера - Массив (классический контроллер)
    elseif (is_array($controller))
    {
        // подключаем ядро-пролог
        require_once \Bitrix\Main\Application::getDocumentRoot() . '/bitrix/modules/main/include/prolog_before.php';

        // classic controller
        // [':class', 'method'] = <= [ 0 => ':class' , 1 => 'method' ]
        [$controllerClass, $actionName] = $controller;

        /**
         * ! пытается вычислить модуль из пространства имен класса и проверяет подключен ли он
         * @see https://dev.1c-bitrix.ru/api_d7/bitrix/main/loader/autoload.php
         * \Bitrix\Main\Loader::requireClass($controllerClass);
        */
        //region Модификации в код
        /**
         * ? дополняю базовый код
         */
        try{
            \Bitrix\Main\Loader::requireClass($controllerClass);
        }catch(\Bitrix\Main\LoaderException $e){
            // пытаемся запустить контроллер без регистрации модуля
            //\Bitrix\Main\Loader::registerAutoLoadClasses(null, [
            //    'Umsoft\Controller\BaseController' => '/local/php_interface/lib/Umsoft/Controllers/BaseController.php',
            //    'Umsoft\Controller\MainController' => '/local/php_interface/lib/Umsoft/Controllers/MainController.php',
            //]);

            // todo: удалить
            //require_once  \Bitrix\Main\Application::getDocumentRoot() . '/local/php_interface/init.php';

            \Bitrix\Main\Loader::autoLoad($controllerClass);
        }
        //endregion

        // проверяем extend'ит ли класс, класс котроллера
        if(is_subclass_of($controllerClass, \Bitrix\Main\Engine\Controller::class)){

            // проверяем содержит ли имя метода постфикс Action
            if (substr($actionName, -6) === 'Action')
            {
                $actionName = substr($actionName, 0, -6);
            }

            // запускаем котроллер на исполнение
            $application->runController($controllerClass, $actionName);
        }

    }
    // тип контроллера - Строка
    elseif (is_string($controller))
    {
        require_once \Bitrix\Main\Application::getDocumentRoot() . '/bitrix/modules/main/include/prolog_before.php';

        // actually action could be attached to a few controllers
        // but what if action was made for the one controller only
        // then it could be used in routing
        $actionClass = $controller;
        \Bitrix\Main\Loader::requireClass($actionClass);

        if (is_subclass_of($controller, \Bitrix\Main\Engine\Action::class))
        {
            if (is_subclass_of($actionClass, \Bitrix\Main\Engine\Contract\RoutableAction::class))
            {
                /** @var \Bitrix\Main\Engine\Contract\RoutableAction $actionClass */
                $controllerClass = $actionClass::getControllerClass();
                $actionName = $actionClass::getDefaultName();

                /** @var \Bitrix\Main\HttpApplication $app */
                $app = \Bitrix\Main\Application::getInstance();
                $app->runController($controllerClass, $actionName);
            }
            else
            {
                throw new \Bitrix\Main\SystemException(sprintf(
                    'Action `%s` should implement %s interface for being called in routing',
                    $actionClass, \Bitrix\Main\Engine\Contract\RoutableAction::class
                ));
            }
        }
    }

    // любой другой тип контроллера, выкидываем исключение
    throw new \Bitrix\Main\SystemException(sprintf(
        'Unknown controller `%s`', $controller
    ));
}
//endregion

Если совпадение по маршруту не найдено (uri не зарегистрирован как роут-обработчик) поток исполнения передается в стандартный файл urlrewrite.php

Последняя строчка скрипта

// если маршрут не зарегистрирован как роут-обработчик,
// подключаем стандартный файл маршрутов urlrewrite;
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/urlrewrite.php';


Create Custom Route

Разбор создания и конфигурации пользовательского маршрута.

ВАЖНО:
Маршруты uri необходимо указывать без начального слэша, можно указывать слэш в конце. Пример: company, company/ - это два разных маршрута.
Префиксы маршрутов не должны содержать в себе слеши, используется только наименование. Пример: ->prefix('api')
Эта обязатеьная особенность связанна с обработкой и сопуставлением маршрутов в ядре Битрикс, разработчики не закладывали автовалидацию и автоисправление. 

В процессе исполнения жизненного цикла скрипта обработки маршрутизации активно взаимодействуют между собой две сущности RoutingConfigurator и Router

Пример:

// конфигуратор для маршрутов
$routes = new \Bitrix\Main\Routing\RoutingConfigurator();

// маршрутизатор
$router = new \Bitrix\Main\Routing\Router();

// устанавливаем маршрутизатор в конфигуратор
$routes->setRouter($router);

// связываем приложение с маршрутизатором
$application->setRouter($router);

Ссылка на Маршрутизатор (Router) содержится в защищенном поле объекта Конфигуратора (RoutingConfigurator).

image.png

Объект Маршрутизатора (Router) состоит из трёх полей:

  1. routes - массив реализованных из конфигураций объектов маршрутов (\Bitrix\Main\Routing\Route).
  2. routesByName - массив аллиасов к маршрутам по наименованию.
  3. configurations - массив с объектами предварительной конфигурации маршрутов (\Bitrix\Main\Routing\RoutingConfiguration).

image.png

Для создания и регистрации пользовательского маршрута необходимо реализовать следующую последовательность:

Создать объект конфигурации маршрута:

$conf = new \Bitrix\Main\Routing\RoutingConfiguration();

Связать конфигурацию маршрута с объектом Конфигуратора (RoutingConfigurator) :

// $routes : \Bitrix\Main\Routing\RoutingConfigurator
$conf->setConfigurator($routes);

До определения маршрута и контроллера, обязательно должен быть установлен объект Options. В противном случае, будет выбрашено исключение, из-за попытки вызвать метод объекта на значении null.

Создать и сконфигурировать объект настройки опций:

// создаем объект настройки опций
$options = new \Bitrix\Main\Routing\Options();

// ! для заполнения свойства parentPrefix необходим создать дополнительный объект опций и объединить их
$ao = new \Bitrix\Main\Routing\Options();
$ao->prefix('example');
$options->mergeWith($ao);

Связать объект опций с объектом конфигурации маршрута:

// $conf : \Bitrix\Main\Routing\RoutingConfiguration
$conf->setOptions($options);

Определить маршрут и исполняемый контроллер:

// $conf : \Bitrix\Main\Routing\RoutingConfiguration
// ! Определение маршрутов доступны через методы класса \Bitrix\Main\Routing\RoutingConfiguration
// определяем uri: /foo и контроллер класса \Umsoft\Example::class с методом fooAction
// наименование метода передается без постфикса Action
$conf->get('foo/', [\Umsoft\Example::class, 'foo']);

Зарегистрировать объект конфигурации в объекте Маршрутизатора (Router) :

// $conf : \Bitrix\Main\Routing\RoutingConfiguration
// $router : \Bitrix\Main\Routing\Router
$router->registerConfiguration($conf);

image.png

После выполнения метода releaseRoutes() объекта Маршрутизатора (Router), пользовательский объект конфигурации маршрута станет объектом маршрута (Bitrix\Main\Routing\Route).

image.png