A free and open-source book on ZF3 for beginners


5.11. Написание собственного типа маршрута

Хотя ZF3 предоставляет вам множество типов маршрута, в некоторых ситуациях вам необходимо будет написать свой собственный тип.

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

5.11.1. RouteInterface

Как мы знаем, каждый класс маршрута должен реализовывать интерфейс Zend\Router\Http\RouteInterface. Методы этого интерфейса представлены в таблице 5.4:

Таблица 5.4. Методы RouteInterface
Имя метода Описание
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.

5.11.2. Пользовательский класс маршрута

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

Кроме того, класс, который мы создадим будет более мощным, потому что будет не только распознавать URL, начинающиеся с "/doc" и заканчивающиеся на ".html". Вместо этого, он будет распознавать общие URL, такие как /help" или "/support/chapter1/introduction".

Чего мы хотим добиться:

Для начала создайте подкаталог Route под корневым каталогом модуля и поместите туда файл StaticRoute.php (рисунок 5.9).

Рисунок 5.9. Файл StaticRoute.php Рисунок 5.9. Файл StaticRoute.php

Вставьте приведенный ниже кусок кода в этот файл:

<?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 доступны следующие опции:

После разбора опций, в строках 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):

Рисунок 5.10. Статические страницы Рисунок 5.10. Статические страницы

Наконец, откройте следующий URL в вашем браузере: http://localhost/help. Должна появиться страница Help (см. рисунок 5.11). Если вы введете URL http://localhost/chapter1/intro в свой браузер, вы должны будете увидеть страницу Introduction (рисунок 5.12).

Figure 5.11. Страница Help Figure 5.11. Страница Help

Figure 5.12. Страница Introduction Figure 5.12. Страница Introduction

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

Если вы застряли, можете найти корректно работающую версию этого примера внутри приложения Hello World.


Top