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

  • Tutorial
Многие аспекты 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.

Какой подход предпочитаете вы?

Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 11

    +1
    Мне кажется сомнительным решением писать роуты к cli-модулям, а не оформлять их как консольную команду
    И как такое чудо вешать на крон?
    Ни в одном популярном фреймворке такого вопроса и не возникнет.
    И не описан способ получения ссылки по интересующему роуту с передачей параметра, как это делается почти везде. Как в вашей системе получить ссылку на комментарий к созданному мной элементу каталога?
      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 без каких либо параметров).


      Как в вашей системе получить ссылку на комментарий к созданному мной элементу каталога?

      Увы, фреймворк ничего такого из коробки не предоставляет. Не знаю почему, но мне как-то даже не было нужно такое никогда. Можете уточнить для чего конкретно такое может быть нужно, что нельзя вручную сформировать ссылку? Интересно узнать сценарии использования.

        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
        

        В чем смысл именно такой реализации?
          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, хотя суть та же.

            0
            /usr/bin/php /path/to/bin/cli clean_cache:System/optimization

            в CleverStyle Framework я не смотря в код знаю, что за запрос отвечает статический метод cs\modules\System\cli\Controller::optimization_clean_cache()

            Наверно потому что вы автор. Лично для меня связь не настолько явная.

              0

              Естественно, я имею ввиду что оно полностью ложится на конвенцию. Если понять несложный принцип, то всё сразу становится на свои места, не нужно изучать код и конфигурацию для того чтобы с почти 100% вероятностью узнать что будет происходить и где это искать.

                0
                А как статику тестить?
                  0

                  Моки и стабы на используемый внешний код и вперёд. В инструментах для разработки самого фреймворка такие штуки уже есть готовые и активно используются, при желании можно любые собственные инструменты притащить.

      0
      А как сделать тогда кастомный рот без языкового префикса типо "/l/" и какой язык при этом будет выбран?
        0

        Есть события, с их помощью вы можете делать любые алиасы, которые вам только заблагорассудится.


        Если не указывать язык в URL — будут взяты к сведению заголовки запроса. К примеру, Accept-Language если это браузер, так же поддерживаются заголовки, которые отправляет Facebook, когда встраивает preview в ленте (ему почему-то не хотелось стандартный Accept-Language применять).
        Далее если и заголовков нет никаких — используется язык, сконфигурированный в настройках системы.
        Так же если пользователь зарегистрирован и в профиле явно указан язык — он так же будет иметь бОльший вес, чем язык в заголовках и чем в настройках системы.


        Но это уже не совсем к маршрутизации относится, это многоязычность, с которой тоже много все интересного происходит)

        0
        выглядит както костыльно, имхо

        Only users with full accounts can post comments. Log in, please.