Контент


Ko3: роутинг

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

Структура маршрута

Маршрут (route) представляет собой именованный набор правил, содержащий инструкции по выбору имени контроллера и метода для дальнейшего их создания и выполнения. Чтобы не тратить лишних слов, давайте сразу рассмотрим классический пример из дефолтного файла bootstrap.php:

Route::set('default', '(<controller>(/<action>(/<id>)))')
	->defaults(array(
		'controller' => 'welcome',
		'action'     => 'index',
	));


Класс Route является по сути контейнером маршрутов, выполняющим также функции анализа имеющихся маршрутов на соответствие полученным от URI значениям. Статический метод Route::set() создает новый маршрут и добавляет его в перечень существующих. В качестве первого параметра он принимает имя маршрута, т.е. при последовательном добавлении двух маршрутов с одним именем первый будет замещен вторым. В примере имя маршрута — ‘default‘.

Второй параметр самый главный — это шаблон, описывающий структуру URI, который должен быть обработан данным маршрутом. В нем-то и определяется контроллер, метод и дополнительные параметры. В указанном выше примере предусмотрена типичная для Kohana структура ЧПУ — контроллер/метод/id (имя параметра указывается в угловых скобках, например ). Причем будет работать как URI вида ‘/welcome/index‘, так и просто ‘/welcome‘, т.е. параметры маршрута (даже имя контроллера) не являются обязательными. Дело в том, что опциональные параметры «обертываются» круглыми скобками. Таким образом, рассматриваем строку (<controller>(/<action>(/<id>))) и видим, что:

  • Пустой URI также будет соответствовать данному правилу (т.к. в круглые скобки заключена вся строка шаблона).
  • Если указан один параметр URI, то это будет имя контроллера (т.к. внутри главных скобок только этот ключ не заключен какие-либо другие скобки).
  • Если и второй параметр, то он будет назначен имени метода (ключ action, он является главным в подстроке (/<action>(/<id>))).
  • Ну и наименьший приоритет в данном разборе имеет ключ id, которому достанется только третий сегмент URI.

Необязательный участок URI, выделенный круглыми скобками, может состоять и из нескольких сегментов.

А если не будет ни одного параметра (обращение к корню сайта)? По идее URL не содержит информации об имени контроллера, как поступить системе? Для этого есть метод defaults(), который предоставляет значения по умолчанию для неуказанных сегментов. В данном случае будет обращение к методу index() контроллера Controller_Welcome.

Обратите внимание, что параметр id передан не будет! А вот если добавить с defaults() строчку 'id' => NULL, то он появится. Более того, в defaults() можно указать сколько угодно параметров, и они будут переданы в вызываемый метод даже если они не упомянуты в шаблоне маршрута.

Конечно, не все требуемые маршруты могут быть реализованы с помощью одного универсального. Давайте представим, что у нас есть контроллер Controller_Auth с методами login(), logout() и register(). Конечно же, можно использовать URL вида ‘/auth/login‘ для входа на сайт, но ведь хочется коротких адресов для подобных стандартных действий (список которых ограничен и мы его знаем), поэтому давайте реализуем это через маршруты (другой вариант — использование средств веб-сервера, например mod_rewrite для Apache, но это уже тема не касающаяся Kohana напрямую):

Route::set('auth', '<action>')
	->defaults(array(
		'controller' => 'auth',
		'action'     => 'login',
	));


Данный маршрут будет обработан в случае наличия только одного сегмента в URL, и в результате будет вызван метод контроллера Controller_Auth (он отсутствует в строке шаблона, но зато указан в качестве параметра по умолчанию). Однако есть и недостатки — ведь в случае URL/welcome‘ будет произведена попытка вызова несуществующего метода welcome() контроллера Controller_Auth. Это неправильно, поэтому имеется возможность дополнительно фильтровать поступающие сегменты с помощью регулярных выражений. Дело в том, что Route::set() имеет третий, необязательный параметр, который является массивом регулярных выражений для сегментов. Вот как мы будем ограничивать методы контроллера Controller_Auth для нашего нового маршрута:

Route::set('auth', '<action>', array('action' => '(login|logout|register)'))
	->defaults(array(
		'controller' => 'auth',
		'action'     => 'login',
	));


Теперь маршрут отработает только если единственный сегмент адреса соответствует одному из значений (login, logout или register). Если же надо ограничить перечень символов данного сегмента, можно использовать что-то вроде ‘[a-z0-9]‘ (тут разрешены латинские символы и цифры). Возможностей много, по сути это весь мощный арсенал регулярных выражений.

Стоп, скажете Вы. А что же будет, если при дефолтном маршруте ввести адрес с четырьмя параметрами, например /welcome/index/param1/param2? Мы получим ошибку Kohana_Request_Exception [ 0 ]: Unable to find a route to match the URI. Это означает, что не нашлось ни одного маршрута, удовлетворяющего данному адресу. Так что даже дефолтный маршрут не всемогущ, он может манипулировать максимум тремя сегментами. Впрочем, можно использовать все те же регулярные выражения, что весь хвост собрать в одном сегменте:

Route::set('default', '(<controller>(/<action>(/<id>)))', array('id' => '.+'))
	->defaults(array(
		'controller'	=> 'forum_category',
		'action'	=> 'index',
	));


Теперь параметр id будет содержать все, что указано после второго сегмента. Для этого мы использовали маску ‘.+‘, что означает ‘один или более любых символов’.

Мало? Вот вам еще одна возможность. Существует дополнительный параметр, отсутствующий в списке сегментов URI, но играющий роль при вызове метода контроллера — это параметр directory. Часто контроллеры находятся не непосредственно в директории classes/controller, а вложены еще в какие-то папки. Конечно, можно использовать преобразование знака подчеркивания в слэш, и вызывать URI вида ‘admin_users/manage‘, который будет использовать контроллер Controller_Admin_Users, но ведь это некрасиво. Давайте сделаем так, что все URI, начинающиеся с ‘admin/‘, будут вызвать контроллеры из папки admin:

Route::set('admin', 'admin/(<controller>(/<action>(/<id>)))')
	->defaults(array(
		'controller'	=> 'welcome',
		'action'	=> 'index',
		'directory'   => 'admin',
	));

Управление маршрутами

Объявлять маршруты мы научились, но это не все. Имеет значение не только содержимое маршрута, но и его номер в списке маршрутов объекта Route. Т.е. порядок добавления маршрутов важен. Поэтому если Вы добавляете в bootstrap.php свои маршруты, располагайте их ДО дефолтного маршрута. И вообще, тут действует правило — «общие (абстрактные) маршруты должы объявляться позже точных». Поэтому маршруты, нацеленные на отслеживание строго ограниченного числа адресов (к примеру, созданный нами маршрут ‘auth‘), должны быть объявлены первыми.

Метод Route::set(), хоть и является статическим, возвращает вполне конкретный объект Route (который уже добавлен в список маршрутов). Далее можно к нему применить метод defaults(), который мы разбирали. Однако работа с ним на этом не заканчивается, есть два полезных метода:

  • matches($uri). Данный метод проверяет указанный URI на соответствие правилам маршрута. В частности, именно с помощью данного метода система выбирает из перечня маршрутов подходящий. Если маршрут не подходит, то метод возвращает FALSE, а иначе — массив пар параметр => значение.
  • uri(array $params = NULL). Метод являет собой полную противоположность методу matches(), он осуществляет обратную маршрутизацию (т.е. формирует URI из описания маршрута и массива известных параметров). Например, $this->request->route->uri($this->request->route->matches($this->request->uri)) должен вернуть то же значение, что и просто $this->request->uri.

Помимо обычных методов, можно использовать статические (один из них, Route::set() мы же видели). Вот они:

  • Route::get($name) возвращает маршрут с указанным именем. Если маршрут не найден, генерируется Kohana_Exception.
  • Route::all() возвращает все маршруты в виде массива объектов класса Route (в качестве ключей имена маршрутов).
  • Route::cache($save = FALSE) выполняет как кэширование маршрутов, так и извлечение их из кэша — в зависимости от параметра $save (по умолчанию загружает). Если загрузка прошла успешна, то сами маршруты сохраняются в объекте Route, как результат возвращается TRUE, в противном случае приходит FALSE. Так что можно вместо обычного добавления маршрутов действовать так:

    if ( ! Route::cache()) {
      // маршруты не были загружены из кэша
      // добавляем маршруты вручную
      Route::set(...)
      // сохраняем маршруты в кэш
      Route::cache(TRUE);
    }

Из свойств объекта Route доступна только статическая переменная Route::$default_action, которая определяет выполняемый метод по умолчанию (изначально это конечно ‘index‘). Таким образом, сегмент ‘action‘ может вообще отсутствовать в правилах маршрута, в таком случае будет использован дефолтный. А вот с контроллером такое не пройдет.

Работа с маршрутами через Request

Объект Request является одним из центральных в Ko3. Именно в нем производится поиск нужного маршрута, создание экземпляра контроллера и выполнение его метода. Результаты поиска сохраняются в свойствах $controller, $action и $directory, а сам выбранный маршрут — в свойстве $route. Получить параметры маршрута можно через метод param($key = NULL, $default = NULL), причем если имя параметра не указано, будут возвращены все параметры.

Также доступен и обратный роутинг, через все тот же метод uri(array $params = NULL). В качестве маршрута для формирования URI будет использован маршрут, хранящийся в свойстве $route объекта Request.

Дополнительная информация

Имена контроллера и метода могут отсутствовать в шаблоне маршрута, но в таком случае должны быть приведены в методе Route::defaults(). Иначе как система определит, как контроллер вызвать? Кстати, для имени метода еще есть лазейка. Дело в том, что после нахождения нужного маршрута Kohana выполняет следующие операции:

  • Попытка создания экземпляра контроллера. Если он абстрактный, генерируется Kohana_Exception.
  • Выполняется метод before() контроллера.
  • Определяется имя метода. Если оно до сих пор не определено (свойство $action объекта Request), то используется приведенное в $default_action значение.
  • Выполняется метод.
  • Выполняется метод after().

Как мы видим, имя контроллера может быть определено только на основании анализа маршрутов, а вот для изменения имени метода есть «последний шанс» — можно использовать метод before(), и в нем изменить свойство $this->request->action (в принципе, это возможно и в конструкторе контроллера).

Маршруты могут быть добавлены не только через bootstrap.php. Например, любой подключаемый модуль может содержать файл init.php, который будет подключен во время добавления этого модуля методом kohana::modules(). Напомню, что обычно это происходит ДО добавления дефолтного маршрута (см. содержимое файла bootstrap.php).

Необязательные сегменты маршрута разбираются слева направо, с учетом уровня вложенности. Например, при маршруте '(<controller>(/<action>(/<param1>(/<param2>))(/<param3>)))' вызов URI вида ‘test/test/1/2‘ передаст в метод test() параметры $param1 и $param2, хотя вроде бы $param3 на уровень выше, чем $param2. Другой пример — ‘(<controller>(/<action>(/<param1>(/<param2>))/<param3>))‘. Здесь $param3 вообще находится по уровню важности рядом с названием метода, однако в случае передачи ‘test/test/1/2‘ его значение будет не 1, а 2. Почему я об этом говорю? Дело в том, что данные параметры передаются в метод контроллера именно слева на право. Т.е., если метод объявлен как test($param1 = NULL, $param2 = NULL, $param3 = NULL) и происходит вызов URI вида ‘test/test/1/2‘, то будут переданы первый и третий аргумент, что не совсем очевидно. Вообще, при работе с плавающим числом доп. аргументов в маршруте лучше вообще не объявлять аргументы методов, а работать напрямую с $this->request->param().

По умолчанию сегмент не может содержать слэш, точку, запятую, точку с запятой или вопросительный знак. Чтобы решить эту проблему (например, для передачи в URI имени файла) необходимо добавить в маршруте маску для этого сегмента, например ‘.+‘, как мы раньше это сделали для параметра id, чтобы включить поддержку более трех сегментов в URI.

Ну, и напоследок, пример маршрута для блога в виде blog/YYYY/MM/DD/some-blog-article. Надеюсь, объяснения не понадобятся.

Route::set('blog', '(blog(/<year>(/<month>(/<day>(/<slug>)))))',
		array(
			'year' 	=> '[0-9]{4}',
			'month'	=> '[0-9]{1,2}',
			'day'	=> '[0-9]{1,2}',
		))
	->defaults(array(
		'controller'	=> 'blog',
		'action'	=> 'show',
		)
	);

UPDATE. Zabex в комментариях подсказал, как сделать более точную регулярку для данного маршрута, так что встречаем новый вариант:

Route::set('blog', '(blog(/<year>(/<month>(/<day>(/<slug>)))))',
		array(
			'year' 	=> '[0-9]{4}',
			'month'	=> '(?:0[1-9]|1[0-2])',
			'day'	=> '(?:[0][1-9]|[1-2][0-9]|3[0-1])',
		))
	->defaults(array(
		'controller'	=> 'blog',
		'action'	=> 'show',
		)
	);

Надеюсь, получилось расказать все доступно и ничего не забыть.

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в Kohana3.

Теги: , , .


Комментарии (26)

Будьте в курсе обсуждения, подпишитесь на RSS ленту комментариев к этой записи.

  1. altspam пишет:

    Спасибо за статью. Поправка: регулярное выражение [1-12] соответствует только цифрам 1 и 2, а не диапазону 1—12.

  2. BIakaVeron пишет:

    Тьфу, забыл этот момент убрать. Совершенно верно, ниже в статье приведен более корректный вариант, правда он получается довольно избыточным (принимает числа от 0 до 99). Вообще интересно, а можно ли четко ограничить подобный диапазон? К примеру, ‘[0-1][0-9]‘ сузит его до чисел 0 — 19…

  3. altspam пишет:

    По-моему, напрямую нельзя. Иначе не было бы проблемы регулярного выражения для проверки диапазонов в сегментах IP-адреса :)

  4. Zares пишет:

    Вот рабочий пример валидации месяца:
    $string = «12″;
    if (preg_match(‘/^(?:0[1-9]|1[0-2])$/’ ,$string)) {
    echo «month is valid!»;
    }

  5. taggi пишет:

    Спасибо BIakaVeron, все отлично и доходчиво описано. Хорошо бы портировать Kohana Debug Toolbar с версии 2.3 до 3.

  6. BIakaVeron пишет:

    @Zares
    Некое перечисление все равно еще присутствует, но это уже ближе к идеалу. Спасибо, обновил последний пример.
    Хотя, конечно, получать 404 в ответ на некорректную дату тоже не очень правильно. Подобные вещи удобнее фильтровать уже в контроллере…

  7. Satisfaction пишет:

    Отличная статья, вот только подскажите какой нибудь модуль для работы с формами под «тройку», имхо только изза отсутствия оного не могу начинать серьезных проектов =(
    Так хожу осматриваюсь к третьей линейке.

  8. BIakaVeron пишет:

    Честно говоря, никогда не понимал смысла в модулях для управлениями формами. Все можно вполне спокойно делать руками…
    Быстрый поиск на github’е ничего не дал, возможно пока ничего не портировали.

  9. Xobb пишет:

    Route::set('auth', '', array('action' => '(login|logout|logout_all)'))
    ->defaults(array(
    'controller' => 'auth',
    'action' => 'login',
    ));

    Такой роут не работает. бросает ексепшон. что же делать?

  10. BIakaVeron пишет:

    @Xobb
    Вариант в тексте статьи работает у меня (там была пропущена одна кавычка, исправил). Я так полагаю, в твоем коде блог «съел» тэг action во втором параметре? Эксепшн какой бросает? Request_Exception?

  11. Xobb пишет:

    Упс, проблема решена. Когда выделял не заметил пропущеной | и потому регекспа валилась.

  12. Бубнов Славик пишет:

    Какая-то странная логика с кешированием в этом классе (или я как всегда не так все понимаю =) ).

    Дело вот в чем: мне надо, чтобы «маршруты» кешировались, но добавление новых маршрутов может происходить в других модулях (init.php). И писать что-т типа такого

    if ( ! Route::cache()) {
    // маршруты не были загружены из кэша
    // добавляем маршруты вручную
    Route::set(…)
    // сохраняем маршруты в кэш
    Route::cache(TRUE);
    }

    каждый раз (в каждом init.php) не хочется.

    Я подумал, что стоит сделать так (у себя так и сделаю, когда с работу приду домой =) )

    1. Перед вызовом модулей в бутстрапере выгрести кеш
    Route::cache();

    2. Переопределить класс роутера и метод set(…) в частности:
    public static function set($name, $uri, array $regex = NULL) {
    // Если маршрут есть в кеше — выходим
    if (isset(Route::$_routes[$name])) {
    return self;
    }

    // Иначе запоминаем маршрут и сохраняем кеш
    Route::$_routes[$name] = new Route($uri, $regex);
    Route::cache(TRUE);

    return self;
    }

    Интересно, я правильно делаю? =) И может есть решение проще, которое я не заметил?

  13. BIakaVeron пишет:

    Если маршруты загружены из кэша, то там будут и те маршруты, что были добавлены в модулях. ИМХО, стоит в случае успешной загружки из кэша выставлять какую-нибудь переменную или константу в TRUE, а в модулях и дальше по bootstrap’у ее проверять.

  14. Бубнов Славик пишет:

    ну так для этого я и написал в методе set(…) этот код:

    // Если маршрут есть в кеше — выходим
    if (isset(Route::$_routes[$name])) {
    return self;
    }

  15. Бубнов Славик пишет:

    Исправленный (и протестированный) перегруженный метод set(…):

    public static function set($name, $uri, array $regex = NULL) {
    // If route is exists — return
    if (isset(Route::$_routes[$name])) {
    return Route::$_routes[$name];
    }

    // Else add route & save all routes to cache
    Route::$_routes[$name] = new Route($uri, $regex);
    Route::cache(TRUE);

    return Route::$_routes[$name];
    }

    У меня работает как и задумывалось.

  16. BIakaVeron пишет:

    Таким образом, в кэш каждый раз будут загружаться ВСЕ накопленные маршруты… Чем больше вызовов set(), тем больше лишней работы. Да и принудительное кэширование не айс (ИМХО)…

  17. Бубнов Славик пишет:

    Да, спасибо за замечания =)

    Переделал. Теперь кеширование происходит один раз (если надо), при этом «переопределение» одноименных маршрутов тоже работает (код сюда не рискнул вставлять — мои комменты итак достаточно длинные ;) ).

  18. vlad пишет:

    День добрый)
    Уже неделю разбираюсь с коханой — нарвится) но до полного понимания увы, еще очень далеко)
    у меня есть котроллер welcome, там есть экшены method1, method2($id), method3($id), method4($id).
    привызове первого метода выводится каталог перхнего уровня. Выбираем позицию вызывается метод 2, который выводит подкаталог (2 уровень) и так далее.
    получается адрес что-то вроде method1/134/143/452
    где цифры это параметры в методах 2, 3 и 4 соответственно.
    Например, когда я вызываю этот адрес method1/134/143/452 то хочу вызвать метод4 с параметром 452.
    для этого пишу в конфиге:
    Route::set(‘producer’, ‘(/(/(/)))’,array(
    ‘action’ => ‘method1′,
    ‘method2′ => ‘\d’,
    ‘method2′ => ‘\d’,
    ‘method3′ => ‘\d’,
    ‘method4′ => ‘\d’))
    ->defaults(array(
    ‘controller’ => ‘welcome’,
    ‘action’ => ‘method1′,
    ));

    но увы не работает((( выдает что не правильно.
    Конечно можно Route для каждого метода прописать но у меня их штук семь.. хотчеться понять как можно сделать одним Rout’ом.

  19. BIakaVeron пишет:

    Не совсем понятно. А нужны ли при вызове метода4 предыдущие два параметра? Если во всех случаях используется method1, то зачем его прямо указывать в роуте?
    Может, проще использовать схему примерно такого вида:
    method2/134
    method3/143
    method4/452
    А в defaults указать имя экшена (‘method1′) и дополнительный параметр, который определяется цифрой после слова method (чтобы определить, какой экшен запустить).

    ЗЫ. Чтобы блог не скушал тэги, заворачивайте код в тэг PRE с параметром lang=’php’.

  20. vlad пишет:

    прошу прощения, не точно рассказал суть проблемы.
    Что касается метода 1, то он используется толко при получении каталога первого уровня, так же как и метод 2 — второго и метод 3 — третьего.
    Дело в том что в конечном варианте в методе 4 будут использованы как дополнительные параметры, по крайней мере я так хотел. И для того чтобы иметь возможность иметь доступ уже к полученным однажды id, думал их в адресной строке сохранять. Но, как понимаю так вроде не получиться(..
    Наверное придеться писать несколько Rout’ов

  21. BIakaVeron пишет:

    Я правильно понимаю, тут будет иерархия каталогов? Типа hardware/computers/notebooks/asus/…? Так тут непредсказуемый уровень вложенности, ИМХО тут надо анализировать всю строку уже в контроллере. Т.е. роут тупо передает все эти методы в виде одного параметра, а уже экшен его разбивает на составляющие.

  22. vlad пишет:

    да так и есть.
    спасибо, будем разбирать)

  23. Евгений пишет:

    Здравствуйте. Может немного не в тему…
    Мне нужно чтобы все параметры контроллеров были с суффиксом .html.
    В config.php указал:
    $config['url_suffix'] = ‘.html’;

    Так не применяются стили и картинки не грузятся, потому что путь к стилям задаю так:
    <link href=»" rel=»stylesheet» type=»text/css» />
    При рендеринге выходит
    …css/style.css.html
    Как обойти эту проблему?

  24. Исраэль Райдер пишет:

    Иван, во втором Вашем примере -

    Route::set('auth', '<action>', array('action' => '(login|logout|register)'))
    	->defaults(array(
    		'controller' => 'auth',
    		'action'     => 'login',
    	));


    для чего Вы указываете дефолтный action, ‘action’ => ‘login’, ?
    Ведь в шаблоне этот параметр, ‘<action>’, не заключён в круглые скобки. Значит, он обязательно должен присутствовать в адресе, который этому шаблону будет соответствовать. Значит, его дефолтное значение указывать не нужно.

  25. Исраэль Райдер пишет:

    Иван, на примере Вашего третьего примера –

    Route::set('admin', 'admin/(<controller>(/<action>(/<id>)))')
    	->defaults(array(
    		'controller'	=> 'welcome',
    		'action'	=> 'index',
    		'directory'   => 'admin',
    	));


    я хотел бы задать вопрос, касающейся синтаксиса написания шаблона, и для того, что бы разобраться в путанице – ключ параметра, и его значение.
    Ясно, что в шаблоне я могу прописать ключ параметра, для этого я должен заключить его в угловые скобки. А могу прописать и само его значение, в Вашем примере это ‘admin’.
    Вопрос. А позволяет ли мне синтаксис описать в шаблоне и ключ параметра, и его значение? То есть, в Вашем примере, что бы из самого шаблона было ясно, что первый сегмент адреса, это должен быть именно ‘directory’ , и его значение должно быть именно ‘admin’ ? И тогда, дефолтная строчка
    ‘directory’ => ‘admin’,
    оказывается лишней. Если да, подскажите пожалуйста, как это можно сделать.
    Если же нет, тогда мне понятна логика вашего примера. Роут, видя в первом сегменте адреса слово ‘admin’ , заключает, что этот адрес соответствует шаблону. То есть здесь, слово ‘admin’ – это всего навсего контрольное слово для определения соответствия, оно, в принципе, могло быть и любым другим. А вот то, что ‘directory’ – это именно ‘admin’ , Роут узнаёт только из дефолтной строчки ‘directory’ => ‘admin’, .

  26. biakaveron пишет:

    Я слегка подправил комментарии, чтобы оформить роуты как php-код.

    1. Да, Вы абсолютно правы. Для обязательных ключей параметр default не используется. Видимо от предыдущего роута осталось.
    2. В приведенном мной примере изначально параметр directory может быть только одним — ‘admin’, поэтому я использовал такой синтаксис. Иногда удобнее выделить отдельный ключ <directory> и использовать regex для него, например (admin) или (admin|user). Если directory обязательно должен присутствовать, то от default’а мы избавляемся. Но, насколько я знаю, указать параметр ТОЛЬКО в шаблоне роута, и при этом указать его и как имя, и как значение нельзя.



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.