Многие аспекты CleverStyle Framework имеют альтернативную по отношению к большинству других фреймворков реализацию тех же вещей.
Данная статья достаточно подробно описывает устройство работы маршрутизации, примеры использования, а так же примеры того, как можно в этот механизм вмешаться, либо, при желании, полностью его заменить его на собственный.
Главное отличие маршрутизации от реализаций в популярных фреймворках типа Symfony, Laravel или Yii это декларативность вместо императивности.
Это значит, что вместо того, чтобы указывать маршруты в определённом формате и сопоставлять маршруту определённый класс, метод или замыкание, мы всего лишь описываем структуру маршрутов, и этой структуры достаточно для того, чтобы понять какой код будет выполнен в зависимости от маршрута.
Подобный подход конвенций вместо конфигураций удобен в том смысле, что требует меньше усилий во время написания кода, и не требует просмотра конфигурации для того, чтобы понять, какой код будет вызван при открытии определённой страницы, так как это очевидно из соглашения, принятого во фрейворке.
Любой URL в представлении фреймворка разбивается на несколько частей. В самом начале до какой-либо обработки из пути страницы удаляются параметры запроса (
Далее мы получаем общий формат пути следующего вида (
Количество уровней вложенности не ограничено.
Первым делом проверяется префикс языка. Он не участвует в маршрутизации (и может отсутствовать), но при наличии влияет на то, какой язык будет использоваться на странице. Формат зависит от используемых языков и их количества, может бы простым (
После языка следует необязательная часть, определяющая тип страницы. Это может быть страница администрирования (
Далее определяется модуль, который будет обрабатывать страницу. В последствии этот модуль доступен как
Стоит заметить, что название модуля может быть локализовано, к примеру, если для модуля
Остаток элементов массива после модуля попадает в
Перед тем, как перейти к следующим этапам, заполняются ещё 2 массива.
У разработчика есть в распоряжении ряд событий, которые позволяют вклиниться уже на данных этапах и изменить поведение по собственному усмотрению.
Событие
Событие
Пример добавления поддержки UUID как альтернативы стандартным целочисленным идентификаторам:
Структура маршрутов являет собой древовидный JSON, в котором ключ каждого дочернего уровня является продолжением родительского, некоторые окончательные узлы могут быть пустыми, если соседние имеют более глубокую структуру.
Пример текущей структуры API системного модуля:
Примеры (реальные) запросов, подходящих под данную структуру:
Получение маршрута из пути страницы это только первый из двух этапов. Второй этап учитывает конфигурацию текущего модуля и корректирует финальный маршрут соответственно.
Для чего это нужно? Допустим, пользователь открывает страницу
В этом случае
Для API и CLI запросов в этом смысле есть отличие — опускание частей маршрута подобным образом запрещено и допускается только если в структуре в качестве первого элемента на соответствующем уровне используется
К примеру, для API мы можем иметь следующую структуру (
В этом случае
Модули в CleverStyle Framework хранят всё своё внутри папки модуля (в противовес фреймворкам, где все view в одной папке, все контроллеры в другой, все модели в третьей, все маршруты в одном файле и так далее) для удобства сопровождения.
В зависимости от типа запроса используются разные конфиги в формате JSON:
В тех же папках находятся и обработчики маршрутов.
В CleverStyle Framework есть два типа маршрутизации: основанный на файлах (активно использовался ранее) и основанный на контроллере (более активно используется сейчас).
Возьмем из примера выше страницу
В случае с маршрутизацией основанной на файлах, следующие файлы будут подключены в указанном порядке:
Если же используется маршрутизация, основанная на контроллере, то должен существовать класс
Важно, что любой файл/метод кроме последнего можно опустить, и это не приведет к ошибке.
Теперь возьмем более сложный пример, запрос
Во-первых, для API и CLI запросов кроме пути так же имеет значение HTTP метод.
Во-вторых, здесь будет использоваться под-папка
В случае с маршрутизацией основанной на файлах, следующие файлы будут подключены в указанном порядке:
Если же используется маршрутизация, основанная на контроллере, то должен существовать класс
В этом случае хотя бы один из двух последних файлов/контроллеров должен существовать.
Как можно заметить, для API и CLI запросов используется явное разделение кода обработки запросов с разными HTTP методами, в то время как для обычных страниц и страниц администрирования это не учитывается.
Возвращаемого значения в простых случаях достаточно для задания контента. Под капотом для API запросов возвращаемое значение будет передано в
Может случиться, что нет обработчика HTTP метода, который запрашивает пользователь, в этом случае есть несколько сценариев развития событий.
API: если нет ни
CLI: Аналогично API, но вместо
Если вам по какой-то причине не нравится устройство маршрутизации во фреймворке, в каждом отдельном модуле вы можете создать лишь
Поскольку
Для каждого уровня маршрута проверяются права доступа. Права доступа во фреймворке имеют два ключевых параметра: группу и метку.
В качестве группы при проверки прав доступа к странице используется название модуля с опциональным префиксом для страниц администрирования и API, в качестве метки используется путь маршрута (без учета префикса
К примеру, для страницы
Если на каком-то уровне у пользователя нет доступа — обработка завершится ошибкой
Реализация обработки запросов в CleverStyle Framework достаточно мощная и гибкая, являясь при этом декларативной.
В статье описаны ключевые этапы обработки запросов с точки зрения системы маршрутизации и её интереса для разработчика, но на самом деле если вникать в нюансы то там ещё есть что изучать.
Надеюсь, данного руководства достаточно для того, чтобы не потеряться. Теперь должно быть понятно, почему для того, чтобы определить, какой код был вызван в ответ на определённый запрос, не нужно даже смотреть в конфигурацию. Достаточно определить тип используемой маршрутизации по наличию
Актуальная версия фреймворка на момент написания статьи 5.29, в более новых версиях возможны изменения, следите за заметками к релизам.
» GitHub репозиторий
» Документация по фреймфорку
Конструктивные комментарии как обычно приветствуются.
Данная статья достаточно подробно описывает устройство работы маршрутизации, примеры использования, а так же примеры того, как можно в этот механизм вмешаться, либо, при желании, полностью его заменить его на собственный.
Основное отличие
Главное отличие маршрутизации от реализаций в популярных фреймворках типа 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-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{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
Если же используется маршрутизация, основанная на контроллере, то должен существовать класс
cs\modules\Blogs\Controller
(файл modules/Blogs/Controller.php
) со следующими публичными статическими методами:
cs\modules\Blogs\Controller::index($Request, $Response) : mixed
cs\modules\Blogs\Controller::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
Если же используется маршрутизация, основанная на контроллере, то должен существовать класс
cs\modules\Blogs\api\Controller
(файл modules/Blogs/api/Controller.php
) со следующими публичными статическими методами:
cs\modules\Blogs\api\Controller::index($Request, $Response) : mixed
cs\modules\Blogs\api\Controller::index_get($Request, $Response) : mixed
cs\modules\Blogs\api\Controller::items($Request, $Response) : mixed
cs\modules\Blogs\api\Controller::items_get($Request, $Response) : mixed
cs\modules\Blogs\api\Controller::items_comments($Request, $Response) : mixed
cs\modules\Blogs\api\Controller::items_comments_get($Request, $Response) : mixed
В этом случае хотя бы один из двух последних файлов/контроллеров должен существовать.
Как можно заметить, для API и CLI запросов используется явное разделение кода обработки запросов с разными HTTP методами, в то время как для обычных страниц и страниц администрирования это не учитывается.
Аргументы в контроллерах и возвращаемое значение
$Request
и $Response
не что иное, как экземпляры cs\Request
и cs\Response
.Возвращаемого значения в простых случаях достаточно для задания контента. Под капотом для API запросов возвращаемое значение будет передано в
cs\Page::json()
, а для остальных запросов в cs\Page::content()
.public static function items_comments_get () {
return [];
}
// полностью аналогично
public static function items_comments_get () {
Page::instance->json([]);
}
Несуществующие обработчики HTTP методов
Может случиться, что нет обработчика HTTP метода, который запрашивает пользователь, в этом случае есть несколько сценариев развития событий.
API: если нет ни
cs\modules\Blogs\api\Controller::items_comments()
ни cs\modules\Blogs\api\Controller::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 репозиторий
» Документация по фреймфорку
Конструктивные комментарии как обычно приветствуются.
Only registered users can participate in poll. Log in, please.
Какой подход предпочитаете вы?
22.22%
Декларативный
4
77.78%
Императивный
14
18 users voted.
15 users abstained.