KO3: HMVC и роутинг

    Не так давно вышла третья версия этого фреймворка. Еще до его выхода я делал небольшой обзор новых возможностей. Тогда я обошел тему роутинга и HMVC, но сегодня готов исправиться.

    Часть первая, роутинг


    Итак, роутинг сильно изменился. Больше настройки не хранятся в конфигурационном файле, а задаются в коде файла bootstrap с помошью статического метода Route::set, имеющего следующий синтаксис:
      /**
       * Stores a named route and returns it.
       *
       * @param  string  route name
       * @param  string  URI pattern
       * @param  array  regex patterns for route keys
       * @return Route
       */
      public static function set($name, $uri, array $regex = NULL)

    * This source code was highlighted with Source Code Highlighter.

    Этот метод возвращает роут, объект хранящий информацию о параметрах, которые необходимо получить из url и может сказать, способен ли он обработать тот или иной url. Основное правило, которое раньше применялось для отображения большинства адресов url на конкретные методы контроллера, теперь просто является роутом по умолчанию, который легко можно убрать, и который задан для примера в файле bootstrap:
    Route::set('default', '(<controller>(/<action>(/<id>)))')
      ->defaults(array(
        'controller' => 'welcome',
        'action'   => 'index',
      ));

    * This source code was highlighted with Source Code Highlighter.

    Следует отметить, что при определении соответствия url конкретному роуту, роуты тестируются в порядке добавления, потому более общие правила должны быть добавлены после более частных. В данном случае роут default очень общий и если вы решите его оставить, все другие роуты лучше добавлять перед ним.

    Рассмотрим параметры Route::set более внимательно. Первый параметр, $name, используется для дальнейшей идентификации роута, в частности методу Route::get следует передавать именно его. Следующий параметр $uri и есть шаблон, который определяет, как должен выглядеть url, подходящий для этого роута. В этом шаблоне все параметры должны быть указаны в треугольных скобках (controller, action и id в роуте по умолчанию). Все захваченные параметры доступны через Request->param($name, $default). Если какая-то часть url не обязательна (включая параметры), ей нужно заключить в скобки. Последний параметр метода Route::set это массив $regex с регулярными выражениями, которые тестируются для параметров. По умолчанию все параметры тестируются выражением "[^/.,;?]++". Если для какого-то параметра это выражение не подходит, вам нужно передать в массиве $regex подходящее выражение с ключем в массиве, соответствующим имени параметра. Например, для роута по умолчанию было бы логично задать такой массив:
    Route::set('default', '(<controller>(/<action>(/<id>)))', array(
        'id' => '\d{1,10}',
      ))

    * This source code was highlighted with Source Code Highlighter.

    Для каждого параметра можно задать значения по умолчанию, которые будут использоваться, если параметр в шалоне задан как не обязательный и в url его нет. Это делается с помошью метода defaults, который применяется к объектам Route и принимает в качестве параметра массив, построенный по тому же принципу, что и параметр $regex метода Route::set.

    Среди параметров, которые можно передать через url и метод defaults, есть 3 параметра с особым значением. Их имена controller, action и directory. Эти параметры нельзя получить через метод Request->param, но они доступны прямо у объекта Request. Т.е. Request->controller, Request->action, и Request->directory.
    Параметр controller. Единственный обязательный из тройки. Должен присутствовать либо прямо url, либо задаваться через метод Route->defaults. Название говорит самом за себя.
    Параметр action. Еще один параметр с говорящим названием. Если не указан (или указан как пустая строка), берется значение по умолчанию «index».
    Параметр directory. Задает подкаталог каталога controller, в котором следует искать нужный контроллер. Если файл лежит в controller/tools/sidebars/bottom, то имя класса должно быть Controller_Tools_Sidebars_Bottom, а параметры: controller = 'bottom', directory = 'tools/sidebars'.

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

    Пример 1
    Route::set('wigets', 'wigets(/<action>(/<id>))')
      ->defaults(array(
        'controller' => 'wiget',
      ));

    * This source code was highlighted with Source Code Highlighter.

    Что бы соответствовать этому роуту, адрес должен начинаться на wigets, может иметь 2 произвольных параметра через слеш. Способа изменить контроллер из url здесь нет, поэтому роут обслуживает только один контроллер wiget.

    Пример 2
    Route::set('articles', 'articles(/<action>(/<sorting>/<page>))', array(
        'sorting' => '(?:asc|desc)',
        'page' => '\d{1,5}'
      ))

    * This source code was highlighted with Source Code Highlighter.

    Адрес должен начинаться на articles, дальше идет не обязательный параметр action и еще 2 параметра, окруженные только одними скобками с заданными регулярными выражениями. Выражение в скобках может быть либо опущено только полностью, либо принято. В данном случае это означает, что если адрес будет иметь вид /articles/list/asc/, то параметр asc не будет принят, ведь вместе с ним в одних скобках стоит еще один параметр, которому ничего не соответствует.

    Пример 3
    Route::set('articles', 'articles(/<action>((/<id>)/<sorting>(<page>)))', array(
        'sorting' => '(?:name|date|age)',
        'page' => '\d{1,5}',
      ))

    * This source code was highlighted with Source Code Highlighter.

    А вот тут вообще потрясающие вещи продемонстрированы. Во первых, параметр id является не обязательным и находится не в конце выражения. Это значит, что будут верны как адреса /articles/list/154/date так и /articles/list/date. Кроме того, sorting не отделено от page никаким разделителем, поэтому url /articles/list/date20 тоже будет принят.

    Пример 4
    Route::set('articles', 'articles(,<action>((,<id>),<sorting>(<page>)))', array(
        'sorting' => '(?:name|date|age)',
        'page' => '\d{1,5}',
      ))

    * This source code was highlighted with Source Code Highlighter.

    То же самое, только разделитель — запятая. Никаких границ для фантазии :)

    Вообще, говоря о роутах, я бы рекомендовал больше не полагаться на одно общее правило, как в старой версии, а создавать более безопасные частные описания. Правда здесь есть небольшая засада, связанная с тем, что для каждого роута генерируется регулярное выражение при старте приложения. Это, конечно, не слишком быстро при паре десятков роутов, и поэтому роуты нужно кешировать:
    if ( ! Route::cache())
    {
      Route::set(/*…*/);
      
      Route::set(/*…*/);
      
      Route::cache(TRUE);
    }

    * This source code was highlighted with Source Code Highlighter.


    Кроме маршрутизации адресов, роуты служат еще одной важной цели: с помошью них эти самые адреса можно генерировать. Для этого есть метод Route->uri($params), которому передаются недостающие параметры в виде ассоциативного массива и возвращается готовый uri.

    Часть вторая, HMVC


    Многие уже слышали что новая кохана имеет в своей основе парадигму HMVC, отличающейся от привычной MVC тем, что любой из компонентов может для своих нужд запустить еще один запрос к любому контроллеру, минуя протокол http. Это легко продемонстрировать на примере:
    /*
    *   Получить баннеры клиента с id 12
    */
    Request::factory('/wigets/advert/client12')
      ->execute()

    * This source code was highlighted with Source Code Highlighter.

    Методу factory передается строка url, которая заново проходит цикл маршрутизации.

    Собственно обработка основного запроса, приходящего по http мало чем отличается от продемонстрированного выше:
    /**
    * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO'].
    * If no source is specified, the URI will be automatically detected.
    */
    echo Request::instance()
      ->execute()
      ->send_headers();

    * This source code was highlighted with Source Code Highlighter.

    Разница только в том, как мы создаем объект запроса, либо синглтоном (основной запрос) либо через фабрику. И в том и в другом случае мы получаем ответ в свойстве Request->response (вот такая тавтология с названиями). Если мы пытаемся преобразовать объект Request к строке (что во втором примере делает оператор echo), возвращается $this->response.

    Из этой разницы в способе получения объекта Request следует одно строгое правило при работе с третьей коханой — нужно четко понимать к какому объекту Request следует обращаться для получения переменных запроса и возвращения результата. Запрос всего приложения всегда доступен по Request::instance(), а запрос текущего маршрута доступен Controller->request, т.е. в контроллере $this->request.

    На самом деле необходимость такого подхода назрела уже давно. Любой крупный сайт имеет в своем дизайне всевозможные виджеты, сайдбары и прочие блоки, содержимое которых слабо зависит от основного контента на странице, но требует для своего отображения работы контроллера. Раньше приходилось мудрить, если блок нужен был в пределах одного контроллера, делался отдельный метод. Если в большем количестве контроллеров, переносилось в модель или представление, в зависимости от сложности блока. Теперь с этим покончено, можно не стесняясь прямо в представлении вызвать любой url как-то так:
    <div class="sidebar">
      <?= Request::factory('/wigets/voting')->execute() ?>
    </div>

    * This source code was highlighted with Source Code Highlighter.

    А теперь дополнительный бонус: если у нас в этом блоке будет какая-то форма с отправкой данных (например, голосовалка), то мы может отправлять ajax запросы прямо на /wigets/voting и получать в ответ текст одного этого блока без дополнительных проверок, ajax это был или нет.

    Все что касается блоков контента, конечно здорово, но этим не ограничивается применение HMVC в кохане. Дело в том, что никто не ограничивает нас в выборе формата возвращаемых данных. Мы можем записать в Request->response данные любого типа и совершенно просто принять их на другой стороне. Я пока не могу придумать, для чего это может понадобиться, но сам факт того, что любые данные теперь можно получить в любом контроллере или представлении очень радует.

    Еще одна особенность, замеченная мной в работе Request состоит в том, что метод execute() для одного объекта можно вызвать сколько угодно раз. Причем, никаких манипуляций со свойством response между запросами кохана не делает, в том числе и не очищает. Это одновременно и баг и фича. Во первых, будьте внимательны, сбрасывайте response сами, во вторых, имейте ввиду, в нее можно накапливать данные между запросами (при условии что объект Request не меняется, естественно).

    Часть третья, лучшая интеграция роутинга и HMVC


    Ну а теперь некоторые рассуждения от меня лично. Предложенный способ создания объекта Request::factory('url') — никуда не годиться. Фактически всегда нужно делать умнее:
    Request::factory(Route::get('wigets')->uri(array(
        'action' => 'userslist',
        'sorting' => 'name',
      )));

    * This source code was highlighted with Source Code Highlighter.

    Т.е. не вручную составлять адрес, а генерировать его из роута.

    И вот тут, на мой взгляд, кроется небольшой недостаток в архитектуре. Дело в том, что мы передаем в роут параметры, чтобы получить строку uri, а в Request передаем строку чтобы получить параметры. Это сопровождается потерей производительности при вызове:
    — потери на генерацию строки uri (для выполнения контроллера она не нужна)
    — лишние проверки на совпадение с роутами (мы знаем роут заранее)
    — разбор uri на параметры

    Было бы здорово, подумал я, если бы можно было сделать как-нибудь так:
    Request::factory('wigets', array(
        'action' => 'userslist',
        'sorting' => 'name',
      ));

    * This source code was highlighted with Source Code Highlighter.

    Подумал я, и написал модуль перекрывающий метод Request::factory. Модуль дополняет интерфейс создания запросов как показано выше. Кроме того, время выполнения пустых запросов уменьшается вдвое. Первый параметр — название роута, второй должен быть массивом параметров. Если второй параметр не задан, первый, как и прежде, считается строкой uri. Т.е. никакой функционал не ломается.

    Скачать модуль.

    Единственный побочный эффект при использовании такой нотации — строка Request->uri будет содержать не настоящий uri, указанный в роуте, а строку вида /route/controller/action. В частности это видно в выводе профайлера.

    Спасибо за внимание.

    Огромная просьба к знатокам украинского языка, воздержитесь. Все уже в курсе.
    Поделиться публикацией

    Похожие публикации

    Комментарии 17
      +3
      Отличная статья. После нее у меня не осталось вопросов по поводу роутинга в kohana.
        +3
        Новый роутинг стал мощнее, но менее наглядный. Было бы лучше, если бы они оставили возможность использовать старый роутер для простых роутов, но, при этом, для сложных дали использовать новый.
          +3
          А есть ли смысл? Зная как писать по новому, писать по старому.
          +5
          Позволю себе несколько небольших дополнений:

          1. Роутинг может быть откорректирован не только в bootstrap.php, но в принципе в любом месте приложения. В первую очередь это касается подключаемых модулей. Как мы помним, при подключении модуля ядро выполняет скрипт init.php, если он существует в папке модуля. ИМХО, тут самое место для специфических роутов данного модуля. Кроме того, модули подключаются раньше, чем выполняется дополнение дефолтного роута, так что они не будут им перекрыты (в отличие от роутов, указанных где-нибудь в контроллере).
          2. Если какие-то контроллеры или методы должны быть скрыты от вызова в качестве основного УРЛ (как правило, это скрипты виджетов), существует очень простой способ проверки:
          if ($this->request === Request::instance())
          {
          // это вызов из браузера (или из CLI)
          }
          else
          {
          // нас вызывали через HMVC
          }
            0
            Немного обескураживает количество человек, которым статья не понравилась, что для технической статьи редкость. Не могу не спросить, что не так?
              0
              Возможно дело в последней приписке? По крайней мере для меня :)
              Хотя уверен, автор ничего плохого не имел ввиду. Надо было выразиться поконкретнее (можно еще исправить)
              0
              Спасибо. Очень полезно.
              Но както фраза «Огромная просьба к знатокам украинского языка, воздержитесь. Все уже в курсе.» звучит не толерантно, я вот таки не воздержался и выразил автору благодарность.

              А еще минусанул :P
                +2
                я вот таки не воздержался
                На сколько я вижу, воздержались. Эта приписка для тех, кому кроме набившего оскомину баяна сказать нечего.
                0
                Спасибо за статью и модуль. А почему от системных наследуются классы в папке classes/exrequest, а уже от них классы в корне classes — это какой-то принцип модульности? Ведь получается лишний пустой уровень, почему бы не наследоваться сразу?
                  +1
                  Это сделано, чтобы в приложении осталась возможность наследоваться от Exrequest_Request и Exrequest_Route, тем самым добавив свою функциональность и оставляя функциональность модуля.
                  0
                  скажите, а как в примере 1 сделать так чтобы «wiget» не было чувствительным к регистру?
                  • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Отличная статья, спасибо большое автору. Еще хочется :)
                      0
                      Спасибо, как раз то что я искал.

                      А я пытался предопределять роуты в bootstrap и указывать флаг inner
                        0
                        Спасибо, как раз помогло разобраться.
                        Я конечно понимаю что пинать трупы не очень хорошо, всё-таки 2,5 года уже прошло со дня публикации, но всё-таки возможно автор поможет разобраться:
                        Написал роутинг под свои задачи, вот только с тестированием параметров не понял. Имеем:
                        Route::set('catalog', 'catalog(/(/))')
                        ->defaults(array(
                        'controller' => 'catalog',
                        'action' => 'index',
                        ), array(
                        'catalog_id' => '\d{1,5}',
                        'good_id' => '\d{1,5}',
                        ));
                        Что по идее должно происходить если параметры catalog_id и good_id не проходят проверку — \d{1,5}? Просто я эти данные впоследствии использую для sql запроса, и естественно всякой гадости не хотелось бы, а пока туда попадает всё что передам. Может быть неправильно написал роут? Не попалось мне примеров что то, где бы дополнительные параметры шли вместе с дефолтными, возможно я со скобочками/запятыми намудрил, по тому и не работает?
                          0
                          регулярки для параметров пишутся не в defaults, а внутри команды set вторым параметром
                            0
                            Ну да, наоборот, долго гуглил, но нашёл :)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое