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']
$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);
Если совпадение найдено
Поток исполнения определяет один из вариантов типа роут-обработчика:
PublicPageController
- обработчик для совместимости нативных страниц php с подключенной бизнес логикой, например компонент и html + js + php код.\Closure
- функция замыкания, любая функция на исполнение, которую возвращает обработчик.array
- массив с названием класса и наименование Action метода, который будет вызван на исполнение. Пример:[\Umsoft\Example::class, 'foo']
string
- строка с наименованием класса и метода для запуска. Пример:'\Umsoft\Example:foo'
- Любой другой тип:
// любой другой тип контроллера, выкидываем исключение
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
).
Объект Маршрутизатора (Router
) состоит из трёх полей:
routes
- массив реализованных из конфигураций объектов маршрутов (\Bitrix\Main\Routing\Route
).routesByName
- массив аллиасов к маршрутам по наименованию.configurations
- массив с объектами предварительной конфигурации маршрутов (\Bitrix\Main\Routing\RoutingConfiguration
).
Для создания и регистрации пользовательского маршрута необходимо реализовать следующую последовательность:
Создать объект конфигурации маршрута:
$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);
После выполнения метода releaseRoutes()
объекта Маршрутизатора (Router
), пользовательский объект конфигурации маршрута станет объектом маршрута (Bitrix\Main\Routing\Route
).