Pull to refresh

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

Reading time 9 min
Views 4.3K
Многие аспекты 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 репозиторий
» Документация по фреймфорку

Конструктивные комментарии как обычно приветствуются.
Only registered users can participate in poll. Log in, please.
Какой подход предпочитаете вы?
22.22% Декларативный 4
77.78% Императивный 14
18 users voted. 15 users abstained.
Tags:
Hubs:
+9
Comments 11
Comments Comments 11

Articles