Хотя ZF3 предоставляет вам множество типов маршрута, в некоторых ситуациях вам необходимо будет написать свой собственный тип.
Одна из таких ситуаций - когда вам нужно определить правила установления соответствия URL динамически. Обычно конфигурация маршрутизации хранится в файле конфигурации модуля, но в некоторых системах управления содержимым документы будут храниться в базе данных. Для таких систем, вам нужно будет разработать свой тип маршрута, который будет подключаться к БД и выполнять сопоставление маршрута с данными, хранящимися в базе. Вы не можете хранить эту информацию в файле конфигурации, потому что новые документы создаются администраторами системы, а не программистами.
Как мы знаем, каждый класс маршрута должен реализовывать интерфейс Zend\Router\Http\RouteInterface
.
Методы этого интерфейса представлены в таблице 5.4:
Имя метода | Описание |
---|---|
factory($options) |
Статический метод для создания класса маршрута. |
match($request) |
Метод, который выполняет сопоставление с данными HTTP-запроса. |
assemble($params, $options) |
Метод для генерации URL с помощью параметров маршрута. |
getAssembledParams() |
Метод для извлечения параметров, которые были использованы для генерации URL. |
Статический метод factory()
используется маршрутизатором ZF3 (TreeRouteStack
или SimpleRouteStack
)
для инстанцирования класса маршрута. Маршрутизатор передает массив options
как аргумент для
метода factory()
.
Метод match()
используется для сопоставления HTTP-запроса (или, если быть точнее, его URL)
с опциями, передаваемыми классу маршрута через factory()
. Метод match()
должен возвращать
либо экземпляр класса RouteMatch
при успешном сопоставлении, либо null
при неудаче.
Метод assemble()
используется для генерации строки URL с помощью
параметров маршрута и опций. Назначение метода помощника getAssembledParams()
-
вернуть массив параметров, которые использовались при генерации URL.
Для демонстрации создания пользовательского типа маршрута, давайте усовершенствуем наш предыдущий подход к созданию простой системы документации с помощью типа Regex. Недостаток типа Regex заключается в том, что вы не можете организовать иерархию статических страниц, создав подкаталоги под каталогом doc (при генерации URL для такой страницы разделитель директорий в виде слеша будет закодирован, что сделает гиперссылку недоступной для использования).
Кроме того, класс, который мы создадим будет более мощным, потому что будет не только распознавать URL, начинающиеся с "/doc" и заканчивающиеся на ".html". Вместо этого, он будет распознавать общие URL, такие как /help" или "/support/chapter1/introduction".
Чего мы хотим добиться:
Класс StaticRoute
должно быть возможно вставить в стек маршрутов (либо в SimpleRouteStack
,
либо в TreeRouteStack
) и использовать вместе с другими типами маршрутов.
Класс маршрута должен распознавать общие URL, такие как "/help" или "/introduction".
Класс маршрута должен сопоставлять URL со структурой каталогов. Пусть, например, есть URL - "/chapter1/introduction", тогда маршрут должен проверить, существует ли соответствующий файл шаблона представления <base_dir>/chapter1/introduction.phtml и возможно ли его прочесть, и если это так, сообщить о соответствии. Если файла не существует (или его не прочесть), должен возвращаться статус ошибки.
Класс маршрута должен проверять URL на допустимость имен файлов, используя регулярное выражение. Например, имя файла "introduction" является допустимым, в то время как имя "*int$roduction" - нет. Если имя файла недопустимо, должен возвращаться статус ошибки.
Маршрут должен уметь собирать строку URL с помощью имени и параметров.
Для начала создайте подкаталог Route под корневым каталогом модуля и поместите туда файл StaticRoute.php (рисунок 5.9).
Вставьте приведенный ниже кусок кода в этот файл:
<?php
namespace Application\Route;
use Traversable;
use \Zend\Router\Exception;
use \Zend\Stdlib\ArrayUtils;
use \Zend\Stdlib\RequestInterface as Request;
use \Zend\Router\Http\RouteInterface;
use \Zend\Router\Http\RouteMatch;
// Пользовательский маршрут, обслуживающий "статические" веб-страницы.
class StaticRoute implements RouteInterface
{
// Создаем новый маршрут с заданными опциями.
public static function factory($options = [])
{
}
// Сопоставляем данный запрос.
public function match(Request $request, $pathOffset = null)
{
}
// Составляем URL с помощью параметров маршрута.
public function assemble(array $params = [], array $options = [])
{
}
// Получаем список параметров, использованных при составлении URL.
public function getAssembledParams()
{
}
}
Как видите, мы разместили класс StaticRoute
в
пространстве имен Application\Route
(строка 2).
В строках 4-9 мы определяем несколько псевдонимов имен классов, чтобы сделать эти имена короче.
В строках 12-33 мы определяем скелет класса StaticRoute
. Класс StaticRoute
реализует интерфейс RouteInterface
и определяет все методы, указанные
интерфейсом: factory()
, match()
, assemble()
и getAssembledParams()
.
Теперь давайте добавим несколько защищенных свойств и метод конструктора в
класс StaticRoute
, как показано ниже:
<?php
//...
class StaticRoute implements RouteInterface
{
// Базовый каталог представления.
protected $dirName;
// Префикс пути для шаблонов представления.
protected $templatePrefix;
// Шаблон имени файла.
protected $fileNamePattern = '/[a-zA-Z0-9_\-]+/';
// Умолчания.
protected $defaults;
// Список собранных параметров.
protected $assembledParams = [];
// Конструктор.
public function __construct($dirName, $templatePrefix,
$fileNamePattern, array $defaults = [])
{
$this->dirName = $dirName;
$this->templatePrefix = $templatePrefix;
$this->fileNamePattern = $fileNamePattern;
$this->defaults = $defaults;
}
// ...
}
В строке 7 представленного фрагмента мы определяем свойство $dirName
, которое
предназначено для хранения имени базового каталога, где будут располагаться
"статические" шаблоны представлений. В строке 10 мы определяем переменную класса
$templatePrefix
для хранения префикса, который будет прибавляться ко всем
именам шаблонов представлений. Строка 13 содержит переменную $fileNamePattern
,
которая будет использоваться для проверки имени файла.
В строках 22-29 мы определяем метод конструктора, который вызывается при создании экземпляра для инициализации защищенных свойств.
Далее, давайте реализуем метод factory()
для созданного нами класса маршрута StaticRoute
.
Метод factory()
будет вызываться маршрутизатором для инстанцирования класса маршрута:
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Создаем новый маршрут с заданными опциями.
public static function factory($options = [])
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
} elseif (!is_array($options)) {
throw new Exception\InvalidArgumentException(__METHOD__ .
' expects an array or Traversable set of options');
}
if (!isset($options['dir_name'])) {
throw new Exception\InvalidArgumentException(
'Missing "dir_name" in options array');
}
if (!isset($options['template_prefix'])) {
throw new Exception\InvalidArgumentException(
'Missing "template_prefix" in options array');
}
if (!isset($options['filename_pattern'])) {
throw new Exception\InvalidArgumentException(
'Missing "filename_pattern" in options array');
}
if (!isset($options['defaults'])) {
$options['defaults'] = [];
}
return new static(
$options['dir_name'],
$options['template_prefix'],
$options['filename_pattern'],
$options['defaults']);
}
}
В этом коде мы видим, что метод factory()
принимает массив options
в качестве аргумента (строка 9). Этот массив может содержать опции
для конфигурации класса маршрута. Для класса StaticRoute
доступны
следующие опции:
dir_name
- базовый каталог, где хранятся все "статические" шаблоны представлений.template_prefix
- префикс для всех имен шаблонов.filename_pattern
- регулярное выражение для проверки имен файлов.defaults
- параметры, возвращаемые маршрутизатором по умолчанию.После разбора опций, в строках 37-41 мы вызываем метод конструктора класса, чтобы
инстанцировать и вернуть объект StaticRoute
.
Следующий метод, который мы добавим к классу маршрута StaticRoute
- это метод match()
:
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Сопоставляем заданный запрос.
public function match(Request $request, $pathOffset=null)
{
// Гарантируем, что этот тип маршрута используется в HTTP-запросе
if (!method_exists($request, 'getUri')) {
return null;
}
// Получаем URL и его путь.
$uri = $request->getUri();
$path = $uri->getPath();
if($pathOffset!=null)
$path = substr($path, $pathOffset);
// Получаем массив сегментов пути.
$segments = explode('/', $path);
// Сверяем каждый сегмент с допустимым шаблоном имени файла
foreach ($segments as $segment) {
if(strlen($segment)==0)
continue;
if(!preg_match($this->fileNamePattern, $segment))
return null;
}
// Проверяем, существует ли такой файл .phtml на диске
$fileName = $this->dirName . '/'.
$this->templatePrefix.$path.'.phtml';
if(!is_file($fileName) || !is_readable($fileName)) {
return null;
}
$matchedLength = strlen($path);
// Подготавливаем объект RouteMatch.
return new RouteMatch(array_merge(
$this->defaults,
['page'=>$this->templatePrefix.$path]
),
$matchedLength);
}
}
Как мы видим, метод match()
принимает два аргумента: объект
HTTP-запроса (экземпляр класса Zend\Stdlib\Request
) и смещение
пути URL. Объект запроса используется для доступа к URL запроса (строка 17).
Параметр смещения пути - это неотрицательное число типа integer, которое
указывает часть URL, с которой сопоставляется маршрут (строка 21).
В строке 24 мы извлекаем сегменты URL. После этого мы проверяем, является ли
каждый сегмент допустимым именем файла (строки 27-32). Если сегмент не
является действительным именем файла, мы возвращаем null
в качестве статуса ошибки.
В строке 35 мы вычисляем путь к шаблону представления, а в строках 37-39 проверяем, действительно ли такой файл существует и доступен для чтения. Таким образом мы сопоставляем URL со структурой каталогов.
В строках 44-48 мы подготавливаем и возвращаем объект RouteMatch
с параметрами по умолчанию,
а также параметром "page" с именем шаблона представления для визуализации.
Чтобы завершить реализацию нашего класса StaticRoute
, добавим методы assemble()
и
getAssembledParams()
, которые будут использоваться для генерации URL с помощью параметров
маршрута. Код для этих методов представлен ниже:
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Собирает URL с помощью параметров маршрута
public function assemble(array $params = [],
array $options = [])
{
$mergedParams = array_merge($this->defaults, $params);
$this->assembledParams = [];
if(!isset($params['page'])) {
throw new Exception\InvalidArgumentException(__METHOD__ .
' expects the "page" parameter');
}
$segments = explode('/', $params['page']);
$url = '';
foreach($segments as $segment) {
if(strlen($segment)==0)
continue;
$url .= '/' . rawurlencode($segment);
}
$this->assembledParams[] = 'page';
return $url;
}
// Получаем список параметров, использованных при сборке URL.
public function getAssembledParams()
{
return $this->assembledParams;
}
}
В этом фрагменте мы определили метод assemble()
, который принимает два аргумента:
массив parameteres
и массив options
(строка 9).
Метод строит URL путем кодирования сегментов и их объединения (строки 10-26).
Метод getAssembledParams()
просто возвращает имена параметров,
которые мы использовали для генерации URL.
Итак, мы закончили класс маршрута StaticRoute
. Чтобы теперь использовать пользовательский тип
маршрута, добавим следующую конфигурацию в файл конфигурации module.config.php:
'static' => [
'type' => StaticRoute::class,
'options' => [
'dir_name' => __DIR__ . '/../view',
'template_prefix' => 'application/index/static',
'filename_pattern' => '/[a-z0-9_\-]+/',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'static',
],
],
],
В строке 1 приведенной выше конфигурации, мы определяем правило маршрутизации "static".
Параметр type
определяет полностью определенное имя класса StaticRoute
(строка 2).
В массиве options
мы определяем базовый каталог, где будут располагаться "статические"
страницы (строка 4), а также определяем префикс шаблона (строка 5), шаблон имени файла (строка 6)
и массив defaults
, содержащий имя контроллера и действие, которое будет обслуживать
все статические страницы.
Не забудьте вставить следующую строчку в начало класса
module.config.php
:
use Application\Route\StaticRoute;
Последний шаг - создание метода действия в классе IndexController
:
public function staticAction()
{
// Получаем путь к шаблону представления от параметров маршрута
$pageTemplate = $this->params()->fromRoute('page', null);
if($pageTemplate==null) {
$this->getResponse()->setStatusCode(404);
return;
}
// Визуализируем страницу
$viewModel = new ViewModel([
'page'=>$pageTemplate
]);
$viewModel->setTemplate($pageTemplate);
return $viewModel;
}
Действие выше почти идентично действию, которое мы использовали для маршрута Regex.
В строке 4 мы извлекаем из маршрута параметр page
и сохраняем его, как переменную
$pageTemplate
. В строке 11 мы создаем контейнер для переменных ViewModel
, а в
строке 14 мы явно задаем имя шаблона представления для визуализации.
Чтобы увидеть систему в действии, давайте добавим пару "статических" страниц представления:
страницу Help (help.phtml
) и страницу Introduction (intro.phtml
).
Создадим подкаталог static под каталогом view/application/index модуля Application
и поместим туда шаблон представления help.phtml:
<h1>Help</h1>
<p>
See the help <a href="<?= $this->url('static',
['page'=>'/chapter1/intro']); ?>">introduction</a> here.
</p>
Затем создадим подкаталог chapter1 в каталоге static и помещаем туда следующий файл chapter1/intro.phtml:
<h1>Introduction</h1>
<p>
Напишите здесь введение.
</p>
В итоге, мы должны получить такую структуру каталогов (см. рисунок 5.10):
Наконец, откройте следующий URL в вашем браузере: http://localhost/help. Должна появиться страница Help (см. рисунок 5.11). Если вы введете URL http://localhost/chapter1/intro в свой браузер, вы должны будете увидеть страницу Introduction (рисунок 5.12).
Вы можете создавать статические страницы просто добавляя файлы phtml под каталог static, и они автоматически станут доступны для пользователей сайта.
Если вы застряли, можете найти корректно работающую версию этого примера внутри приложения Hello World.