7.10. Плагины

7.10.1. Введение

Архитектура контроллеров включает в себя систему плагинов, которая позволяет добавлять свой код, который будет вызываться при определенных событиях в процессе жизни контроллера. Фронт-контроллер использует брокер плагинов (plugin broker) в качестве реестра пользовательских плагинов, брокер плагинов также обеспечивает вызов методов событий в каждом плагине, зарегистрированном через фронт-контроллер.

Методы событий определены в абстрактном классе Zend_Controller_Plugin_Abstract, от которого должны наследовать все пользовательские плагины:

  • routeStartup() вызывается до того, как Zend_Controller_Front вызовет маршрутизатор для сопоставления запроса с зарегистрированными маршрутами.

  • routeShutdown() вызывается после того, как маршрутизатор завершит обработку запроса.

  • dispatchLoopStartup() вызывается до того, как Zend_Controller_Front войдет в цикл диспетчеризации.

  • preDispatch() вызывается до того, как диспетчером будет вызвано действие. Этот обратный вызов (callback) позволяет реализовать поведение посредника или фильтра. Через изменение запроса и сброс его флага диспетчеризации (методом Zend_Controller_Request_Abstract::setDispatched(false)) текущее действие может быть пропущено и/или заменено на другое.

  • postDispatch() вызывается после того, как действие было вызвано диспетчером. Этот обратный вызов позволяет реализовать поведение фильтра или посредника. Через изменение запроса и сброс его флага диспетчеризации (методом Zend_Controller_Request_Abstract::setDispatched(false)) может быть определено новое действие для диспетчеризации.

  • dispatchLoopShutdown() вызывается после выхода Zend_Controller_Front из его цикла диспетчеризации.

7.10.2. Написание плагинов

Для того, чтобы написать класс плагина, просто включите и расширьте абстрактный класс Zend_Controller_Plugin_Abstract:

<?php
require_once 'Zend/Controller/Plugin/Abstract.php';
 class MyPlugin extends Zend_Controller_Plugin_Abstract
{
    // ...
}
        

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

Zend_Controller_Plugin_Abstract также делает объекты запроса и ответа доступными плагинам контроллеров через методы getRequest() и getResponse(), соответственно.

7.10.3. Использование плагинов

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

<?php
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/Router.php';
require_once 'Zend/Controller/Plugin/Abstract.php';
 class MyPlugin extends Zend_Controller_Plugin_Abstract
{
    public function routeStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>routeStartup() called</p>\n");
    }

    public function routeShutdown(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>routeShutdown() called</p>\n");
    }

    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>dispatchLoopStartup() called</p>\n");
    }

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>preDispatch() called</p>\n");
    }

    public function postDispatch(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>postDispatch() called</p>\n");
    }

    public function dispatchLoopShutdown()
    {
        $this->getResponse()->appendBody("<p>dispatchLoopShutdown() called</p>\n");
    }
}

$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory('/path/to/controllers')
      ->setRouter(new Zend_Controller_Router_Rewrite())
      ->registerPlugin(new MyPlugin());
$front->dispatch();
        

При условии, что вызываемые действия не производят вывод, и что вызвано только одно действие, с плагином выше должен получиться следующий вывод:

<p>routeStartup() called</p>
<p>routeShutdown() called</p>
<p>dispatchLoopStartup() called</p>
<p>preDispatch() called</p>
<p>postDispatch() called</p>
<p>dispatchLoopShutdown() called</p>
        
[Замечание] Замечание

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

7.10.4. Извлечение и работа с плагинами

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

  • getPlugin($class) позволяет извлекать плагин по имени класса. Если не найден соответствующий плагин, то возвращается false. Если зарегистрировано более одного плагина этого класса, то будет возвращен массив.

  • getPlugins() возвращает весь стек плагинов.

  • unregisterPlugin($plugin) производит удаление плагина из стека. Вы можете передавать объект плагина или имя класса плагина, регистрацию которого вы хотите отменить. Если вы передаете имя класса, то будут удалены все плагины этого класса.

7.10.5. Плагины, включенные в стандартную поставку

Zend Framework в его стандартной поставке включает в себя плагин для обработки ошибок.

7.10.5.1. ActionStack

Плагин ActionStack позволяет управлять стеком запросов и действует как плагин postDispatch. Если в текущем объекте запроса уже задан переход на другое действие, то ничего не делается. Иначе плагин проверяет свой стек, тянет самый верхний элемент и производит переход на действие, заданное в этом запросе. Стек обрабатывается в порядке "последний вошел - первый вышел" (last-in-first-out, LIFO).

Вы можете извлечь плагин из фронт-контроллера в любой момент времени, используя Zend_Controller_Front::getPlugin('Zend_Controller_Plugin_ActionStack'). Имея объект плагина, вы можете использовать различные методы для управления им.

  • getRegistry() и setRegistry(). Внутри себя ActionStack использует экземпляр Zend_Registry для хранения стека. Вы можете с помощью этих аксессоров заменить экземпляр реестра на другой или извлечь его.

  • getRegistryKey() и setRegistryKey(). Они могут использоваться для определения того, какой ключ реестра используется при извлечении стека. По умолчанию используется ключ 'Zend_Controller_Plugin_ActionStack'.

  • getStack() позволяет извлечь стек действий целиком.

  • pushStack() и popStack() позволяют соответственно добавлять и извлекать из стека. pushStack() принимает объект запроса.

Дополнительный метод forward() ожидает объект запроса в качестве аргумента и устанавливает состояние текущего объекта запроса во фронт-контроллере в состояние переданного объекта запроса, помечая его как необработанный диспетчером (что приводит к еще одной итерации цикла диспетчеризации).

7.10.5.2. Zend_Controller_Plugins_ErrorHandler

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

Основные назначения этого плагина:

  • Перехват исключений, вызванных отсутствием контроллера или метода действия

  • Перехват исключений, брошенных в контроллерах действий

Другими словами, плагин ErrorHandler спроектирован для обработки HTTP-ошибок типа 404 (отсутствует страница) и 500 (внутренняя ошибка). Он не предназначен для отлова исключений, сгенерированных в других плагинах или в процессе маршрутизации.

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

  • setErrorHandlerModule() устанавливает модуль, на который производится переход.

  • setErrorHandlerController() устанавливает контроллер, на который производится переход.

  • setErrorHandlerAction() устанавливает действие, на которое производится переход.

  • setErrorHandler() принимает ассоциативный массив, который может содержать любые из ключей 'module', 'controller' или 'action'.

Кроме этого, вы можете опционально передать конструктору ассоциативный массив, который будет в свою очередь передан setErrorHandler().

Zend_Controller_Plugin_ErrorHandler регистрирует перехватчик postDispatch() и проверяет, есть ли зарегистрированые исключения в объекте ответа. Если есть, то производится попытка перехода на действие, зарегистрированное в качестве обработчика ошибок.

Если во время диспетчеризации обработчика ошибок произошло исключение, то плагин скажет фронт-контроллеру, чтобы тот бросил исключения, и повторно бросит последнее исключение, зарегистрированное в объекте ответа.

7.10.5.2.1. Использование ErrorHandler в качестве обработчика ошибки 404

Поскольку плагин захватывает не только ошибки приложения, но и ошибки в цепочке контроллеров, вызванные отсутствием класса контроллера и/или метода действия, то он может использоваться в качестве обработчика ошибки 404. В этом случае нужно, чтобы ваш контроллер ошибок проверял тип исключения.

Перехваченные исключения журналируются в объекте, зарегистрированном в запросе. Для его получения используйте метод Zend_Controller_Action::_getParam('error_handler'):

<?php class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');
    }
}
        

Имея объект ошибки, вы можете получить тип ошибки через $errors->type. Тип ошибки может быть одним из следующих:

  • Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER, означает, что контроллер не был найден.

  • Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION, означает, что запрошенное действие не было найдено.

  • Zend_Controller_Plugin_ErrorHandler::EXCEPTION_OTHER, обозначает другие исключения.

Вы можете производить проверку на первые два типа и в случае положительного ответа указывать страницу 404:

<?php class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // ошибка 404 - не найден контроллер или действие
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');

                // ... получение данных для отображения...
                break;
            default:
                // ошибка приложения; выводим страницу ошибки,
                // но не меняем код статуса
                break;
        }
    }
}
        

Вы можете извлекать исключение, которое инициировало вызов обработчика ошибок, через свойство exception объекта error_handler:

<?php
public function errorAction()
{
        $errors = $this->_getParam('error_handler');


        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // ошибка 404 - не найден контроллер или действие
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');

                // ... получение данных для отображения...
                break;
            default:
                // ошибка приложения; выводим страницу ошибки,
                // но не меняем код статуса

                // ...

                // Журналируем исключение:
                $exception = $errors->exception;
                $log = new Zend_Log(new Zend_Log_Writer_Stream('/tmp/applicationException.log'));
                $log->debug($exception->getMessage() . "\n" .  $exception->getTraceAsString());
                break;
        }
}
        
7.10.5.2.2. Управление сгенерированным ранее выводом

Если в процессе обработки запроса вызывается несколько действий, или ваше действие несколько раз вызывает метод render(), то возможно, что объект ответа уже содержит в себе сохраненные данные для вывода. Это может привести к тому, что выведется смесь из ожидаемого содержимого и содержимого ошибки.

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

<?php
$this->getResponse()->clearBody();
        
7.10.5.2.3. Примеры использования плагина

Пример 7.16. Стандартное использование

<?php
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/Plugin/ErrorHandler.php';

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Zend_Controller_Plugin_ErrorHandler());
            

Пример 7.17. Установка другого обработчика ошибок

<?php
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/Plugin/ErrorHandler.php';

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Zend_Controller_Plugin_ErrorHandler(array(
    'module'     => 'mystuff',
    'controller' => 'static',
    'action'     => 'error'
)));
            

Пример 7.18. Использование аксессоров

<?php
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/Plugin/ErrorHandler.php';

$plugin = new Zend_Controller_Plugin_ErrorHandler();
$plugin->setErrorHandlerModule('mystuff')
       ->setErrorHandlerController('static')
       ->setErrorHandlerAction('error');

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin($plugin);
            

7.10.5.2.4. Пример контроллера ошибок

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

<?php class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // ошибка 404 - не найден контроллер или действие
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');

                $content =<<<EOH
<h1>Ошибка!</h1>
<p>Запрошенная вами страница не найдена.</p>
EOH;
                break;
            default:
                // ошибка приложения
                $content =<<<EOH
<h1>Ошибка!</h1>
<p>При обработке вашего запроса произошла непредвиденная ошибка. Пожалуйста, попробуйте позднее.</p>
EOH;
                break;
        }

        // Удаление добавленного ранее содержимого
        $this->getResponse()->clearBody();

        $this->view->content = $content;
    }
}
        
    Поддержать сайт на родительском проекте КГБ