Многие аспекты CleverStyle Framework имеют альтернативную по отношению к большинству других фреймворков реализацию тех же вещей.
Данная статья достаточно подробно описывает устройство работы маршрутизации, примеры использования, а так же примеры того, как можно в этот механизм вмешаться, либо, при желании, полностью его заменить его на собственный.
Основное отличие
Главное отличие маршрутизации от реализаций в популярных фреймворках типа Symfony, Laravel или Yii это декларативность вместо императивности.
Это значит, что вместо того, чтобы указывать маршруты в определённом формате и сопоставлять маршруту определённый класс, метод или замыкание, мы всего лишь описываем структуру маршрутов, и этой структуры достаточно для того, чтобы понять какой код будет выполнен в зависимости от маршрута.
Подобный подход конвенций вместо конфигураций удобен в том смысле, что требует меньше усилий во время написания кода, и не требует просмотра конфигурации для того, чтобы понять, какой код будет вызван при открытии определённой страницы, так как это очевидно из соглашения, принятого во фрейворке.
Основы маршрутизации
Любой URL в представлении фреймворка разбивается на несколько частей.
В самом начале до какой-либо обработки из пути страницы удаляются параметры запроса (?
и всё что после него).
Далее мы получаем общий формат пути следующего вида (|
используется для разделения выбора из нескольких вариантов, в []
сгруппированы необязательные самостоятельные компоненты пути), пример разбит на несколько строчек для удобства, перед обработкой путь разбивается по слэшах и превращается в массив из частей исходного пути:
[language/] [admin/|api/|cli/] [Module_name [/path [/sub_path [/id1 [/another_subpath [/id2] ] ] ] ] ]
Количество уровней вложенности не ограничено.
Первым делом проверяется префикс языка. Он не участвует в маршрутизации (и может отсутствовать), но при наличии влияет на то, какой язык будет использоваться на странице.
Формат зависит от используемых языков и их количества, может бы простым (en
, ru
), либо учитывать регион (en_gb
, ru_ua
).
После языка следует необязательная часть, определяющая тип страницы.
Это может быть страница администрирования ($Request->admin_path === true
), запрос к API ($Request->api_path === true
), запрос к CLI интерфейсу ($Request->cli_path === true
) или обычная пользовательская страница если не указано явно.
Далее определяется модуль, который будет обрабатывать страницу. В последствии этот модуль доступен как $Request->current_module
.
Стоит заметить, что название модуля может быть локализовано, к примеру, если для модуля My_blog
в переводах есть пара "My_blog" : "Мой блог"
, то можно в качестве названия модуля использовать Мой_блог
, при этом всё равно $Request->current_module === 'My_blog'
.
Остаток элементов массива после модуля попадает в $Request->route
, который может использоваться модулями, к примеру, для кастомной маршрутизации.
Перед тем, как перейти к следующим этапам, заполняются ещё 2 массива.
$Request->route_ids
содержит элементы из $Request->route
, которые являются целыми числами (подразумевается что это идентификаторы), $Request->route_path
же содержит все элементы $Request->route
кроме целых чисел, и используется как маршрут внутри модуля.
Как вклиниться в маршрутизацию на ранних этапах
У разработчика есть в распоряжении ряд событий, которые позволяют вклиниться уже на данных этапах и изменить поведение по собственному усмотрению.
Событие System/Request/routing_replace/before
срабатывает сразу перед определением языка страницы и позволяет как-то модифицировать исходный путь в виде строки, самые низкоуровневые манипуляции можно проводит в этом месте.
Событие System/Request/routing_replace/after
срабатывает после формирования $Request->route_ids
и $Request->route_path
, позволяя откорректировать важные параметры после того, как они были определены системой.
Пример добавления поддержки UUID как альтернативы стандартным целочисленным идентификаторам:
Event::instance()->on(
'System/Request/routing_replace/after',
function ($data) {
$route_path = [];
$route_ids = [];
foreach ($data['route'] as $item) {
if (preg_match('/([a-fd]{8}(?:-[a-fd]{4}){3}-[a-fd]{12}?)/i', $item)) {
$route_ids[] = $item;
} else {
$route_path[] = $item;
}
}
if ($route_ids) {
$data['route_path'] = $route_path;
$data['route_ids'] = $route_ids;
}
}
);
Структура маршрутов
Структура маршрутов являет собой древовидный JSON, в котором ключ каждого дочернего уровня является продолжением родительского, некоторые окончательные узлы могут быть пустыми, если соседние имеют более глубокую структуру.
Пример текущей структуры API системного модуля:
{
"admin" : {
"about_server" : [],
"blocks" : [],
"databases" : [],
"groups" : [
"_",
"permissions"
],
"languages" : [],
"mail" : [],
"modules" : [],
"optimization" : [],
"permissions" : [
"_",
"for_item"
],
"security" : [],
"site_info" : [],
"storages" : [],
"system" : [],
"themes" : [],
"upload" : [],
"users" : [
"_",
"general",
"groups",
"permissions"
]
},
"blank" : [],
"languages" : [],
"profile" : [],
"profiles" : [],
"timezones" : []
}
Примеры (реальные) запросов, подходящих под данную структуру:
GET api/System/blank GET api/System/admin/about_server SEARCH_OPTIONS api/System/admin/users SEARCH api/System/admin/users PATCH api/System/admin/users/42 GET api/System/admin/users/42/groups PUT api/System/admin/users/42/permissions
Получение окончательного маршрута
Получение маршрута из пути страницы это только первый из двух этапов. Второй этап учитывает конфигурацию текущего модуля и корректирует финальный маршрут соответственно.
Для чего это нужно? Допустим, пользователь открывает страницу /Blogs
, а структура маршрутов сконфигурирована следующим образом (modules/Blogs/index.json
):
[
"latest_posts",
"section",
"post",
"tag",
"new_post",
"edit_post",
"drafts",
"atom.xml"
]
В этом случае $Request->route_path === []
, но $App->controller_path === ['index', 'latest_posts']
.
index
будет здесь вне зависимости от модуля и конфигурации, а вот latest_posts
уже зависит от конфигурации. Дело в том, что если страница не API и не CLI запрос, то при указании неполного маршрута фреймворк будет выбирать первый ключ из конфигурации на каждом уровне, пока не дойдет до конца вглубь структуры. То есть Blogs
аналогично Blogs/latest_posts
.
Для API и CLI запросов в этом смысле есть отличие — опускание частей маршрута подобным образом запрещено и допускается только если в структуре в качестве первого элемента на соответствующем уровне используется _
.
К примеру, для API мы можем иметь следующую структуру (modules/Module_name/api/index.json
):
{
"_" : []
"comments" : []
}
В этом случае api/Module_name
аналогично api/Module_name/_
. Это позволяет делать API с красивыми методами (помним, что идентификаторы у нас в отдельном массиве):
GET api/Module_name GET api/Module_name/42 POST api/Module_name PUT api/Module_name/42 DELETE api/Module_name/42 GET api/Module_name/42/comments GET api/Module_name/42/comments/13 POST api/Module_name/42/comments PUT api/Module_name/42/comments/13 DELETE api/Module_name/42/comments/13
Расположение файлов со структурой маршрутов
Модули в CleverStyle Framework хранят всё своё внутри папки модуля (в противовес фреймворкам, где все view в одной папке, все контроллеры в другой, все модели в третьей, все маршруты в одном файле и так далее) для удобства сопровождения.
В зависимости от типа запроса используются разные конфиги в формате JSON:
- для обычных страниц
modules/Module_name/index.json
- для страниц администрирования
modules/Module_name/admin/index.json
- для API
modules/Module_name/api/index.json
- для CLI
modules/Module_name/cli/index.json
В тех же папках находятся и обработчики маршрутов.
Типы маршрутизации
В CleverStyle Framework есть два типа маршрутизации: основанный на файлах (активно использовался ранее) и основанный на контроллере (более активно используется сейчас).
Возьмем из примера выше страницу Blogs/latest_posts
и окончательный маршрут ['index', 'latest_posts']
.
В случае с маршрутизацией основанной на файлах, следующие файлы будут подключены в указанном порядке:
modules/Blogs/index.php modules/Blogs/latest_posts.php
Если же используется маршрутизация, основанная на контроллере, то должен существовать класс csmodulesBlogsController
(файл modules/Blogs/Controller.php
) со следующими публичными статическими методами:
csmodulesBlogsController::index($Request, $Response) : mixed csmodulesBlogsController::latest_posts($Request, $Response) : mixed
Важно, что любой файл/метод кроме последнего можно опустить, и это не приведет к ошибке.
Теперь возьмем более сложный пример, запрос GET api/Module_name/items/42/comments
.
Во-первых, для API и CLI запросов кроме пути так же имеет значение HTTP метод.
Во-вторых, здесь будет использоваться под-папка api
.
В случае с маршрутизацией основанной на файлах, следующие файлы будут подключены в указанном порядке:
modules/Module_name/api/index.php modules/Module_name/api/index.get.php modules/Module_name/api/items.php modules/Module_name/api/items.get.php modules/Module_name/api/items/comments.php modules/Module_name/api/items/comments.get.php
Если же используется маршрутизация, основанная на контроллере, то должен существовать класс csmodulesBlogsapiController
(файл modules/Blogs/api/Controller.php
) со следующими публичными статическими методами:
csmodulesBlogsapiController::index($Request, $Response) : mixed csmodulesBlogsapiController::index_get($Request, $Response) : mixed csmodulesBlogsapiController::items($Request, $Response) : mixed csmodulesBlogsapiController::items_get($Request, $Response) : mixed csmodulesBlogsapiController::items_comments($Request, $Response) : mixed csmodulesBlogsapiController::items_comments_get($Request, $Response) : mixed
В этом случае хотя бы один из двух последних файлов/контроллеров должен существовать.
Как можно заметить, для API и CLI запросов используется явное разделение кода обработки запросов с разными HTTP методами, в то время как для обычных страниц и страниц администрирования это не учитывается.
Аргументы в контроллерах и возвращаемое значение
$Request
и $Response
не что иное, как экземпляры csRequest
и csResponse
.
Возвращаемого значения в простых случаях достаточно для задания контента. Под капотом для API запросов возвращаемое значение будет передано в csPage::json()
, а для остальных запросов в csPage::content()
.
public static function items_comments_get () {
return [];
}
// полностью аналогично
public static function items_comments_get () {
Page::instance->json([]);
}
Несуществующие обработчики HTTP методов
Может случиться, что нет обработчика HTTP метода, который запрашивает пользователь, в этом случае есть несколько сценариев развития событий.
API: если нет ни csmodulesBlogsapiController::items_comments()
ни csmodulesBlogsapiController::items_comments_get()
(либо аналогичных файлов), то:
- в первую очередь будет проверено существования обработчика метода
OPTIONS
, если он есть — он решает что с этим делать - если обработчика метода
OPTIONS
нет, то автоматически сформированый список существующих методов будет отправлен в заголовкеAllow
(если вызываемый метод был отличный отOPTIONS
, то дополнительно код статуса будет изменен на501 Not Implemented
)
CLI: Аналогично API, но вместо OPTIONS
особенным методом является CLI
, и вместо заголовка Allow
доступные методы будут выведены в консоль (если вызываемый метод был отличный от CLI
, то дополнительно статус выхода будет изменен на 245
(501 % 256
)).
Использование собственной системы маршрутизации
Если вам по какой-то причине не нравится устройство маршрутизации во фреймворке, в каждом отдельном модуле вы можете создать лишь index.php
файл и в нём подключить маршрутизатор по вкусу.
Поскольку index.php
не требует контроллеров и структуры в index.json
, вы обойдете большую часть системы маршрутизации.
Права доступа
Для каждого уровня маршрута проверяются права доступа. Права доступа во фреймворке имеют два ключевых параметра: группу и метку.
В качестве группы при проверки прав доступа к странице используется название модуля с опциональным префиксом для страниц администрирования и API, в качестве метки используется путь маршрута (без учета префикса index
).
К примеру, для страницы api/Module_name/items/comments
будут проверены права пользователя для разрешений (через пробел group label
):
api/Module_name index api/Module_name items api/Module_name items/comments
Если на каком-то уровне у пользователя нет доступа — обработка завершится ошибкой 403 Forbidden
, при этом обработчики предыдущих уровней не будут выполнены, так как права доступа определяются на этапе окончательного формирования маршрута, до запуска обработчиков.
Напоследок
Реализация обработки запросов в CleverStyle Framework достаточно мощная и гибкая, являясь при этом декларативной.
В статье описаны ключевые этапы обработки запросов с точки зрения системы маршрутизации и её интереса для разработчика, но на самом деле если вникать в нюансы то там ещё есть что изучать.
Надеюсь, данного руководства достаточно для того, чтобы не потеряться.
Теперь должно быть понятно, почему для того, чтобы определить, какой код был вызван в ответ на определённый запрос, не нужно даже смотреть в конфигурацию. Достаточно определить тип используемой маршрутизации по наличию Controller.php
в целевой папке и открыть соответствующий файл.
Актуальная версия фреймворка на момент написания статьи 5.29, в более новых версиях возможны изменения, следите за заметками к релизам.
GitHub репозиторий: github.com/nazar-pc/CleverStyle-Framework
Документация по фреймфорку: github.com/nazar-pc/CleverStyle-Framework/tree/master/docs
Конструктивные комментарии как обычно приветствуются.
Автор: nazarpc