Маршрутизація в CleverStyle Framework

Багато аспектів CleverStyle Framework мають альтернативну по відношенню до більшості інших фреймворків реалізацію тих же речей.

Дана стаття досить докладно описує пристрій роботи маршрутизації, приклади використання, а так само приклади того, як можна в цей механізм втрутитися, або, при бажанні, повністю його замінити його на власний.

Основна відмінність
Головна відмінність маршрутизації від реалізацій в популярних фреймворках типу Symfony, Laravel або Yii це декларативність замість імперативності.

Це означає, що замість того, щоб вказувати маршрути в певному форматі та зіставляти маршруту певний клас, метод або замикання, ми всього лише описуємо структуру маршрутів, і цієї структури достатньо для того, щоб зрозуміти який код буде виконаний в залежності від маршруту.

Подібний підхід конвенцій замість конфігурацій зручний у тому сенсі, що вимагає менше зусиль під час написання коду, і не вимагає перегляду конфігурації для того, щоб зрозуміти код, який буде викликаний при відкритті певної сторінки, так як це очевидно з угоди, прийнятого у фрейворке.

Основи маршрутизації
Будь URL в уявленні фреймворку розбивається на декілька частин. На самому початку до якої-небудь обробки з шляху сторінки видаляються параметри запиту (
?
і все, що після нього).

Далі ми одержуємо загальний формат шляху наступного виду (
|
використовується для поділу вибору з декількох варіантів,
[]
згруповані необов'язкові самостійні компоненти шляху), приклад розбитий на кілька рядків для зручності, перед обробкою шлях розбивається по слэшах і перетворюється в масив з частин вихідного шляху:


[language/]
[admin/|api/|cli/]
[Module_name
[/path
[/sub_path
[/id1
[/another_subpath
[/id2]
]
]
]
]
]

Кількість рівнів вкладеності не обмежена.

Насамперед перевіряється префікс мови. Він не бере участь в маршрутизації (і може бути відсутнім), але при наявності впливає на те, яка мова буде використовуватися на сторінці. Формат залежить від використовуваних мов і їх кількості, може б простим (
en
,
uk
), або враховувати регіон (
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
    ні, то автоматично сформований список існуючих методів буде відправлений у заголовку
    Дозволити
    (якщо викликається метод був відмінний від
    OPTIONS
    , то додатково код статусу буде змінено на
    501 Not Implemented
    )
CLI: Аналогічно API, але замість
OPTIONS
особливим методом є
CLI
, і замість заголовка
Дозволити
доступні методи будуть виведені в консоль (якщо викликається метод був відмінний від
CLI
, то додатково статус виходу буде змінено на
245
(
501 % 256
)).

Використання власної системи маршрутизації
Якщо вам з якоїсь причини не подобається пристрій маршрутизації у фреймворку, в кожному окремому модулі ви можете створити лише
index.php
файл і в ньому підключити маршрутизатор за смаком.

Оскільки
index.php
не вимагає контролерів і структури
index.json
, ви обійдете велику частину системи маршрутизації.

Права доступу
Для кожного рівня маршруту перевіряються права доступу. Права доступу у фреймворку мають два ключових параметри: групу і позначку.

В якості групи при перевірки прав доступу до сторінки використовується назва модуля з опціональним префіксом для сторінок адміністрування і API, в якості мітки використовується шлях маршруту (без урахування префікса
index
).

Наприклад, для сторінки
api/Module_name/items/comments
будуть перевірені права користувача для дозволів (через пробіл
group label
):


api/index Module_name
api/Module_name items
api/Module_name items/comments

Якщо на якомусь рівні у користувача немає доступу — обробка завершиться помилкою
403 Forbidden
, при цьому обробники попередніх рівнів не будуть виконані, так як права доступу визначаються на етапі остаточного формування маршруту, до запуску обробників.

Наостанок
Реалізація обробки запитів у CleverStyle Framework досить потужна і гнучка, будучи при цьому декларативною.

У статті описано ключові етапи обробки запитів з точки зору системи маршрутизації та її інтересу для розробника, але насправді якщо вникати в нюанси то там ще є що вивчати.

Сподіваюся, цього керівництва достатньо для того, щоб не загубитися. Тепер повинно бути зрозуміло, чому для того, щоб визначити код, який був викликаний у відповідь на певний запит, не потрібно навіть дивитися в конфігурацію. Досить визначити тип використовуваної маршрутизації за наявності
Controller.php
в цільовій папці і відкрити відповідний файл.

Актуальна версія фреймворку на момент написання статті 5.29, в більш нових версіях можливі зміни, слідкуйте за примітками до релізами.

» GitHub репозиторій
» Документація по фреймфорку

Конструктивні коментарі як завжди вітаються.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.