7.5. Стандартный маршрутизатор: Zend_Controller_Router_Rewrite

7.5.1. Введение

Zend_Controller_Router_Rewrite является стандартным маршрутизатором Zend Framework. Маршрутизация - это процесс принятия конечной точки URI (той части URI, которая идет после базового URL) и ее разложения на параметры для определения того, какой контроллер и какое действие этого контроллера должны получить запрос. Значения контроллера, действия и необязательных параметров сохраняются в объекте Zend_Controller_Request_Http, который затем обрабатывается диспетчером Zend_Controller_Dispatcher_Standard. Маршрутизация производится только один раз – когда вначале получен запрос и до того, как первый контроллер будет запущен.

Zend_Controller_Router_Rewrite спроектирован для того, чтобы обеспечить функциональность, подобную mod_rewrite, с использованием чистого php. Он отчасти основан на маршрутизации, используемой в Ruby on Rails и не требует каких-либо предварительных знаний о перезаписи URL веб-сервером. Он спроектирован для работы с единственным правилом mod_rewrite, пример которого приведен ниже:

RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php
        

или:

RewriteEngine on
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 
        

Rewrite Router может также использоваться с веб-сервером IIS, если Isapi_Rewrite был установлен как расширение Isapi со следующими правилами перезаписи:

RewriteRule ^[\w/\%]*(?:\.(?!(?:js|ico|gif|jpg|png|css)$)[\w\%]*$)? /index.php [I]
        
[Замечание] IIS Isapi_Rewrite

Если используется IIS, то $_SERVER['REQUEST_URI'] не будет определен, либо будет установлен как пустая строка. В этом случае Zend_Controller_Request_Http попытается использовать $_SERVER['HTTP_X_REWRITE_URL'], значение которого устанавливается расширением Isapi_Rewrite.

Если используется Lighttpd, то корректным будет следующее правило перезаписи:

url.rewrite-once = ( ".*\.(js|ico|gif|jpg|png|css)$" => "$0", "" => "/index.php")
        

7.5.2. Использование маршрутизатора

Для того, чтобы правильно использовать маршрутизатор, вы должны инстанцировать его, добавить пользовательские маршруты, и внедрить его во фронт-контроллер. Следующий код иллюстрирует эту процедуру:

<?php
/* Создание маршрутизатора */

$router = $ctrl->getRouter(); // по умолчанию возвращает rewrite router
$router->addRoute(
    'user',
    new Zend_Controller_Router_Route('user/:username', array('controller' => 'user', 'action' => 'info'))
);
        

7.5.3. Базовые операции Rewrite Router

Сущностью RewriteRouter (перезаписывающий маршрутизатор) является определение пользовательских маршрутов. Маршруты добавляются посредством вызовом метода addRoute() и передачей ему экземпляра класса, реализующего Zend_Controller_Router_Route. Например:

<?php
$router->addRoute('user', new Zend_Controller_Router_Route('user/:username'));
        

Rewrite Router поставляется вместе с четырьмя базовыми типами маршрутов (один из которых является специальным):

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

Первым параметром метода addRoute() является имя маршрута. Он используется в качестве идентификатора для получения маршрутов из маршрутизатора (например, в целях генерации URL). Вторым параметром является сам маршрут.

[Замечание] Замечание

Наиболее часто имя маршрута используется через хелпер Zend_View для URL:

<a href="<?= $this->url(array('username' => 'martel'), 'user') ?>">Martel</a>
            

В результате значением атрибута href будет user/martel.

Маршрутизация - простой процесс итерации по всем предоставленным маршрутам и сопоставления их определений с текущим URI запроса. Когда найдено соответствие, то из объекта маршрута возвращаются значения переменных и добавляются в объект Zend_Controller_Request для дальнейшего использования в диспетчере и пользовательских контроллерах. Если соответствие не найдено, то проверяется следующий маршрут в цепочке.

[Замечание] Обратный порядок сопоставления

Маршруты сопоставляются в обратном порядке, поэтому удостоверьтесь, что наиболее общие маршруты определены первыми.

[Замечание] Возвращаемые значения

Значения, возвращаемые при маршрутизации, получаются из параметров URL или определенных пользователем значений по умолчанию. Эти переменные позднее могут быть позднее получены через методы Zend_Controller_Request::getParam() и Zend_Controller_Action::_getParam()

Есть три специальные переменные, которые могут использоваться в маршрутах - 'module', 'controller' and 'action'. Эти специальные переменные используются диспетчером Zend_Controller_Dispatcher для нахождения контроллера и действия, которым передается управление.

[Замечание] Специальные переменные

Имена этих переменных могут быть другими, если вы измените их через методы setControllerKey и setActionKey.

7.5.4. Маршруты по умолчанию

Zend_Controller_Router_Rewrite изначально сконфигурирован с одним маршрутом по умолчанию, который будет соответствовать URI вида контроллер/действие. Кроме того, в качестве первого элемента пути может быть указано имя модуля, это позволяет использовать URI вида модуль/контроллер/действие. Этот маршрут будет также соответствовать любым дополнительным параметрам, по умолчанию добавляемым в конец URI - контроллер/действие/переменная1/значение1/переменная2/значение2.

Некоторые примеры того, чему будут соответствовать такие маршруты:

// Допустим, есть следующие настройки:
$ctrl->setControllerDirectory(
    array(
        'default' => '/path/to/default/controllers',
        'news'    => '/path/to/news/controllers',
        'blog'    => '/path/to/blog/controllers'
    )
);

Только модуль:
http://example/news
    module == news

Если модуль не найден, то считается, что это имя контроллера:
http://example/foo
    controller == foo

Модуль + контроллер:
http://example/blog/archive
    module     == blog
    controller == archive

Модуль + контроллер + действие:
http://example/blog/archive/list
    module     == blog
    controller == archive
    action     == list

Модуль + контроллер + действие + параметры:
http://example/blog/archive/list/sort/alpha/date/desc
    module     == blog
    controller == archive
    action     == list
    sort       == alpha
    date       == desc

        

Маршрутом, используемым по умолчанию, является объект Zend_Controller_Router_Route_Module, сохраненный в RewriteRouter под именем (индексом) 'default'. Он создается приблизительно следующим образом:

<?php
$compat = new Zend_Controller_Router_Route_Module(array(), $dispatcher, $request);
$this->addRoute('default', $compat);
        

Если вы не хотите использовать этот маршрут по умолчанию в своей схеме маршрутизации, то можете переопределить его путем создания собственного маршрута по умолчанию (т.е. сохранения его под именем 'default') или полностью удалить его через метод removeDefaultRoutes():

<?php
// Удаление всех маршрутов по умолчанию
$router->removeDefaultRoutes();
        

7.5.5. Базовый URL и поддиректории

RewriteRouter может использоваться в поддиректориях (например, http://domain.com/~user/application-root/), в этом случае базовый URL приложения (/~user/application-root) должен автоматически определяться в объекте Zend_Controller_Request_Http и соответствующим образом использоваться.

Если базовый URL определяется некорректно, то вы можете переопределить его через метод setBaseUrl() объекта Zend_Controller_Request_Http (см. Раздел 7.4.2.2, «Базовый URL и поддиректории»):

<?php
$request->setBaseUrl('/~user/application-root/');
        

7.5.6. Типы маршрутов

7.5.6.1. Zend_Controller_Router_Route

Zend_Controller_Router_Route - стандартный маршрут фреймворка. Он сочетает в себе легкость использования и гибкость определения маршрутов. Каждый маршрут состоит в основном из карты URL (статических и динамических частей (переменных)), и может быть инициализирован со значениями по умолчанию и требованиями к переменным.

Давай представим себе некое приложение, в котором нужно разместить несколько информационных страниц об авторах. Мы хотим иметь возможность набрать в своем броузере http://domain.com/author/martel, для просмотра информации о парне "martel". Маршрут для обеспечения такой функциональности может выглядеть следующим образом:

<?php
$route = new Zend_Controller_Router_Route(
    'author/:username', 
    array(
        'controller' => 'profile', 
        'action'     => 'userinfo'
    )
);

$router->addRoute('user', $route);
    

Первый параметр конструктора Zend_Controller_Router_Route - определение маршрута, которое будет сопоставляться с URL. Определения маршрутов содержат статические и динамические части, разделенные символом косой черты ('/'). Статические части являются обычным текстом: author. Динамические части, называемые переменными, помечаются знаком двоеточия в начале имени переменной: :username.

[Замечание] Использование символов>

Текущая реализация позволяет использовать любые (за исключением косой черты) символы в идентификаторе переменной, но сильно рекомендуется использовать в них только символы, допустимые для переменных в php. Есть вероятность, что в будущем реализация изменится, и это может вызвать скрытые ошибки в вашем коде.

Этот маршрут должен сработать, когда вы вводите http://domain.com/author/martel в своем броузере, в этом случае все его переменные будут добавлены в объект Zend_Controller_Request и будут доступны в вашем контроллере ProfileController. Переменные, возвращаемые в этом примере, могут быть представленые в виде следующего массива пар ключ/значение:

<?php
$values = array(
    'username'   => 'martel',
    'controller' => 'profile',
    'action'     => 'userinfo'
);
    

Затем Zend_Controller_Dispatcher_Standard должен вызвать метод userinfoAction() вашего класса ProfileController (используется модуль по умолчанию), основываясь на этих значениях. Вы сможете получать эти переменные через методы Zend_Controller_Action::_getParam() или Zend_Controller_Request::getParam().

<?php
public function userinfoAction() 
{
    $request = $this->getRequest();
    $username = $request->getParam('username');

    $username = $this->_getParam('username');
}
    

Определение маршрута может содержать в себе еще один специальный символ (метасимвол), представленный символом '*'. Он используется для сбора параметров, как в маршруте Module, используемом по умолчанию (пары переменная => значение, определенные в URI). Следующий маршрут приближенно воспроизводит поведение маршрута Module:

<?php
$route = new Zend_Controller_Router_Route(
    ':module/:controller/:action/*', 
    array('module' => 'default')
);
$router->addRoute('default', $route);
    
7.5.6.1.1. Значения переменных по умолчанию

Любая переменная в маршруте может иметь значение по умолчанию, для этого используется второй параметр конструктора Zend_Controller_Router_Route. Этот параметр является массивом с ключами, представляющими имена переменных, и со значениями в качестве значений переменных по умолчанию:

<?php
$route = new Zend_Controller_Router_Route(
    'archive/:year', 
    array('year' => 2006)
);
$router->addRoute('archive', $route);
        

Маршрут выше будет соответствовать URL-ам http://domain.com/archive/2005 и http://example.com/archive. В последнем случае переменная year будет иметь начальное значение по умолчанию 2006.

В этом примере переменная year будет добавлена в объект запроса. Поскольку не была предоставлена информация для маршрутизации (не определены параметры controller и action), то управление будет передано контроллеру и методу действия, используемым по умолчанию (оба определены в Zend_Controller_Dispatcher_Abstract). Для того, чтобы сделать этот пример более полезным, нужно передать действительные контроллер и действие в качестве значений по умолчанию:

<?php
$route = new Zend_Controller_Router_Route(
    'archive/:year', 
    array(
        'year'       => 2006, 
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);
        

В результате будет вызван метод showAction() класса ArchiveController.

7.5.6.1.2. Требования к переменным

Можно передать конструктору Zend_Controller_Router_Route третий параметр, в котором установлены требования к переменным. Они определяются как части регулярных выражений:

<?php
$route = new Zend_Controller_Router_Route(
    'archive/:year', 
    array(
        'year'       => 2006, 
        'controller' => 'archive',
        'action'     => 'show'
    ),
    array('year' => '\d+')
);
$router->addRoute('archive', $route);
        

В случае маршрута, определенного таким образом, маршрутизатор будет считать URL соответствующим ему только если переменная year содержит числовые данные - например, http://domain.com/archive/2345. URL вида http://example.com/archive/test не будет соответствовать этому маршруту, вместо этого будет произведен переход к следующему маршруту в цепочке.

7.5.6.2. Zend_Controller_Router_Route_Static

Во всех примерах выше использовались динамические маршруты - маршруты, содержащие шаблоны для сравнения. Но часто определенный маршрут должен быть неизменным, и применение регулярных выражений по отношению к нему было бы излишеством. Решением в данной ситуации является использование статических маршрутов:

<?php
$route = new Zend_Controller_Router_Route_Static(
    'login', 
    array('controller' => 'auth', 'action' => 'login')
);
$router->addRoute('login', $route);
    

Этот маршрут будет соответствовать URL http://domain.com/login и приводит к вызову AuthController::loginAction().

7.5.6.3. Zend_Controller_Router_Route_Regex

Кроме описанных ранее типов маршрутов - используемого по умолчанию и статического - есть маршруты на регулярных выражениях. Этот маршрут дает больше мощности и гибкости, чем другие типы маршрутов, но ценой некоторой дополнительной сложности. В то же время он должен быть более быстрым, чем стандартный маршрут.

Как и в случае стандартного, такой маршрут должен быть проинициализирован с определением маршрута и некоторыми значениями по умолчанию. Давай в качестве примера создадим маршрут для архива, такой же, как в предыдущих примерах, но на этот раз с использованием маршрута Regex:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array(
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);
    

Каждый определенный подшаблон регулярного выражения будет внедрен в объект запроса. В нашем примере после успешного сопоставления с http://domain.com/archive/2006 результирующий массив значений может выглядеть следующим образом:

$values = array(
    1            => '2006',
    'controller' => 'archive',
    'action'     => 'show'
);
[Замечание] Замечание

Ведущая и замыкающая косые черты удаляются из URL в маршрутизаторе до сопоставления. Поэтому URL-у http://domain.com/foo/bar/ будет соответствовать регулярное выражение foo/bar, но не /foo/bar.

[Замечание] Замечание

Указатели начала и конца строки ('^' и '$' соответственно) автоматически добавляются в начало и конец всех выражений. Поэтому вы не должны использовать их в своих регулярных выражениях, кроме этого, следует передавать строку выражения целиком.

[Замечание] Замечание

Этот класс маршрута использует символ # в качестве ограничителя. Это означает, что нужно экранировать символы хэша ('#'), но не прямой косой черты ('/') в своем определении маршрута. Поскольку символ '#' (называемый анкером) редко передается веб-серверу, вам нечасто придется использовать этот символ в своем регулярном выражении.

Вы можете получать содержимое заданного подшаблона обычным способом:

public function showAction() 
{
    $request = $this->getRequest();
    $year    = $request->getParam(1); // $year = '2006';
}
    
[Замечание] Замечание

Обратите внимание, что ключ является целым числом (1), а не строкой ('1').

Этот маршрут не будет работать в точности так же, как и аналогичный ему стандартный маршрут, потому что еще не определено значение по умолчанию для 'year'. Есть еще неочевидная проблема с замыкающей косой чертой, которая остается даже в том случае, если мы объявим значение по умолчанию для 'year' и сделаем подшаблон опциональным. Решение состоит в том, чтобы сделать часть 'year' опциональной вместе с косой чертой, но отлавливать только число:

$route = new Zend_Controller_Router_Route_Regex(
    'archive(?:/(\d+))?', 
    array(
        1            => '2006',
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);
    

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

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array(
        'controller' => 'archive',
        'action' => 'show'
    ),
    array(
        1 => 'year'
    )
);
$router->addRoute('archive', $route);
    

Это приведет к тому, что в объект запроса будут добавлены следующие значения:

$values = array(
    'year'       => '2006',
    'controller' => 'archive',
    'action'     => 'show'
);
    

Для того, чтобы соответствия работали в любом окружении, они могут быть определены в любом направлении. Ключи массива могут содержать как имена переменных, так и индексы подшаблонов:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array( ... ),
    array(1 => 'year')
);

// ИЛИ
        
$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array( ... ),
    array('year' => 1)
);
    
[Замечание] Замечание

Ключи подшаблонов должны быть представлены целыми числами.

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

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)/page/(\d+)',
    array( ... ),
    array('year' => 1)
);
    

Это приведет к тому, что в объекте запроса будут значения с разными ключами. Например, при URL http://domain.com/archive/2006/page/10 результатом будут следующие значения:

$values = array(
    'year'       => '2006',
    2            => 10,
    'controller' => 'archive',
    'action'     => 'show'
);
    

Поскольку регулярные выражения трудно реверсировать, то вам нужно будет подготовить реверсный URL, если хотите использовать хелпер для URL, или даже написать метод этого класса. Реверсный путь представляется строкой, оформленной для использования с функцией sprintf(), и определенной как четвертый параметр конструктора:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array( ... ),
    array('year' => 1),
    'archive/%s'
);
    

Все это можно реализовать через объект стандартного маршрута, поэтому вы спросите - какие преимущества дает использование маршрута Regex? Главным образом, он позволяет описывать любые типы URL без всяких ограничений. Предположим, у вас есть свой блог, вы хотите создавать URL вида http://domain.com/blog/archive/01-Using_the_Regex_Router.html и должны разлагать последний элемент пути 01-Using_the_Regex_Router.html на ID статьи и ее заголовок/описание. Это невозможно реализовать с помощью стандартного маршрута. С маршрутом Regex вы можете сделать нечто подобное для решения этой задачи:

$route = new Zend_Controller_Router_Route_Regex(
    'blog/archive/(\d+)-(.+)\.html',
    array(
        'controller' => 'blog', 
        'action'     => 'view'
    ), 
    array(
        1 => 'id', 
        2 => 'description'
    ),
    'blog/archive/%d-%s.html'
);
$router->addRoute('blogArchive', $route);
    

Как вы можете видеть, маршруты Regex дают несравненно большую гибкость по сравнению со стандартными маршрутами.

7.5.7. Использование Zend_Config вместе с RewriteRouter

Иногда может быть более удобным обновлять конфигурационный файл с новыми маршрутами, чем изменять код. Это возможно благодаря методу addConfig(). В сущности, вы создаете конфигурацию, совместимую с Zend_Config, считываете ее в своем коде и передаете RewriteRouter.

В качестве примера рассмотрим следующий INI-файл:

[production]
routes.archive.route = "archive/:year/*"
routes.archive.defaults.controller = archive
routes.archive.defaults.action = show
routes.archive.defaults.year = 2000
routes.archive.reqs.year = "\d+"

routes.news.type = "Zend_Controller_Router_Route_Static"
routes.news.route = "news"
routes.news.defaults.controller = "news"
routes.news.defaults.action = "list"

routes.archive.type = "Zend_Controller_Router_Route_Regex"
routes.archive.route = "archive/(\d+)"
routes.archive.defaults.controller = "archive"
routes.archive.defaults.action = "show"
routes.archive.map.1 = "year" 
; ИЛИ: routes.archive.map.year = 1
        

Этот INI-файл может быть затем прочитан в объект Zend_Config как показано ниже:

$config = new Zend_Config_Ini('/path/to/config.ini', 'production');
$router = new Zend_Controller_Router_Rewrite();
$router->addConfig($config, 'routes');
        

В примере выше мы говорим маршрутизатору, чтобы он использовал раздел 'routes' в файле INI для своих маршрутов. Ключ первого уровня в этом разделе используется для определения имени маршрута, в примере выше определяются маршруты 'archive' и 'news'. Каждый маршрут требует, как минимум, запись 'route' и одну или более записей 'defaults'; опционально может быть одна или более записей 'reqs' (сокращение от 'required'). Все это соответствует трем аргументам, передаваемым объекту Zend_Controller_Router_Route_Interface. Опция с ключом 'type' может использоваться для определения класса, используемого для данного маршрут;, по умолчанию используется класс Zend_Controller_Router_Route. В примере выше для маршрута 'news' должен использоваться класс Zend_Controller_Router_Route_Static.

7.5.8. Создание подклассов маршрутизатора

Стандартный RewriteRouter создан с тем, чтобы предоставлять полный набор тех функциональных возможностей, которые могут вам понадобиться. Как правило, вам нужно будет только создать новый тип маршрута для того, чтобы получить новый или измененный функционал сверх уже существующих типов маршрутов.

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

<?php
interface Zend_Controller_Router_Interface
{
  /**
   * @param  Zend_Controller_Request_Abstract $request
   * @throws Zend_Controller_Router_Exception
   * @return Zend_Controller_Request_Abstract
   */
  public function route(Zend_Controller_Request_Abstract $request);
}
        

Маршрутизация производится только один раз - когда в систему поступил первый запрос. Назначение маршрутизатора состоит в определении контроллера, действия и опциональных параметров, основываясь на переменных запроса, и установке их в запросе. Затем объект запроса передается диспетчеру. Если не найден соответствующий маршрут, то маршрутизатор не должен ничего делать с объектом запроса.

    Поддержать сайт на родительском проекте КГБ