Маршрутизация в 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-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 репозиторий
» Документация по фреймфорку
Конструктивные комментарии как обычно приветствуются.
Комментарии (8)
14 августа 2016 в 17:24
0↑
↓
Мне кажется сомнительным решением писать роуты к cli-модулям, а не оформлять их как консольную команду
И как такое чудо вешать на крон?
Ни в одном популярном фреймворке такого вопроса и не возникнет.
И не описан способ получения ссылки по интересующему роуту с передачей параметра, как это делается почти везде. Как в вашей системе получить ссылку на комментарий к созданному мной элементу каталога?14 августа 2016 в 17:43
0↑
↓
Они и есть консольные команды, просто формат для унификации аналогичен остальной маршрутизации.
CLI команд сейчас всего несколько, к примеру, очистить кэш можно следующим образом:./cli clean_cache:System/optimization
Где
clean_cache
выступает в роли аналога HTTP метода, аSystem/optimization
в роли аналога пути страницы.Параметры командной строки превращаются в унифицированные параметры, доступные через привычный
$Request->query()
, аналогично параметрам после?
в URL:./cli get:Module_name bool_param text_param="Some value"
./cli help:System
выведет справку с доступными командами и форматом вызова (аналогично вызову./cli
без каких либо параметров).Как в вашей системе получить ссылку на комментарий к созданному мной элементу каталога?
Увы, фреймворк ничего такого из коробки не предоставляет. Не знаю почему, но мне как-то даже не было нужно такое никогда. Можете уточнить для чего конкретно такое может быть нужно, что нельзя вручную сформировать ссылку? Интересно узнать сценарии использования.
14 августа 2016 в 18:01
0↑
↓
Можете уточнить для чего конкретно такое может быть нужно, что нельзя вручную сформировать ссылку?
допустим, генерация ссылки на комментарий или на какую-либо страницу/раздел/статью/путь к api и т.п
Или же генерация ссылки для карты сайта
Генерация ссылки в шаблоне для почтовой рассылки. Много сценариев можно придумать, и переименование модуля не приведет к тому, что придется вручную переписывать уже захардкоженные ссылки во всех местах с их упоминанием.Где clean_cache выступает в роли аналога HTTP метода, а System/optimization в роли аналога пути страницы.
Почему бы не сделать консольные команды по человечески, например:/usr/bin/php /path/to/bin/console cache:clear //symfony php app/cli.php main test world universe //phalcon php artisan make:console SendEmails //laravel
В чем смысл именно такой реализации?14 августа 2016 в 18:24
0↑
↓
Во фреймворке нет очевидной связи между определённым маршрутом и моделью, к примеру, статьи. Соответственно, нет простого способа в общем виде ассоциировать какие-то сущности с маршрутами и обратно. В последнее время всё больше перехожу на подход API + по сути отдельное приложение на фронтенде, которое работает с API. В этом случае пути всё равно придется генерировать не на сервере. В общем, нужно будет над этим подумать.
Приведу команду к вашему формату:
/usr/bin/php /path/to/bin/console cache:clear //symfony /usr/bin/php /path/to/bin/cli clean_cache:System/optimization //cleverstyle framework
Не вижу какой-то фундаментальной разницы. В то же время, в Symfony не так очевидно, какой код отвечает за команду, а в CleverStyle Framework я не смотря в код знаю, что за запрос отвечает статический метод
cs\modules\System\cli\Controller::optimization_clean_cache()
.Почему именно так? Мне это показалось весьма логичным и удобным (как в HTTP запросах: метод и цель), а в контексте остальной маршрутизации даже в некотором смысле очевидно. Вы выполняете операцию (
clean_cache
) над сущностью или в контексте определённого пути (System/optimization
).Если бы мы создавали статьи из командной строки, то у нас были бы методы
get
,post
,delete
и сущностьArticles
. То есть в формате Symfony получаетсяarticles:get
, а здесьget:Articles
, хотя суть та же.14 августа 2016 в 18:32
0↑
↓
/usr/bin/php /path/to/bin/cli clean_cache: System/optimization
в CleverStyle Framework я не смотря в код знаю, что за запрос отвечает статический метод cs\modules\System\cli\Controller: optimization_clean_cache ()
Наверно потому что вы автор. Лично для меня связь не настолько явная.14 августа 2016 в 18:37
0↑
↓
Естественно, я имею ввиду что оно полностью ложится на конвенцию. Если понять несложный принцип, то всё сразу становится на свои места, не нужно изучать код и конфигурацию для того чтобы с почти 100% вероятностью узнать что будет происходить и где это искать.
14 августа 2016 в 23:41
0↑
↓
А как статику тестить?14 августа 2016 в 23:50
0↑
↓
Моки и стабы на используемый внешний код и вперёд. В инструментах для разработки самого фреймворка такие штуки уже есть готовые и активно используются, при желании можно любые собственные инструменты притащить.