framework.zend.com
Stable релиз 2.0 / 1.12

Плагины Front контроллера в Zend Framework

К комментариям

Содержание

От переводчиков

Данный текст - перевод статьи Front Controller Plugins in Zend Framework, автором которой является Matthew Weier O'Phinney. Оригинал статьи был опубликован 14 апреля 2008 года. Статья написанапо Zend Framework версии 1.5, но сохраняет актуальность и для последующих релизов. На текущий момент последний стабильный релиз Zend Framework версии 1.8.1.

Глоссарий

Action Helpers - помощники действий.
Front Controller - Front-контроллер.
Routing (request) - маршрутизация запроса.
Dispatch (request) - диспетчеризация запроса (имеется ввиду выполнение кода указанного в определенном действии контроллера).

Вступление

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

Что такое плагин Front-контроллера?

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

  • routeStartup(): вызывается перед началом маршрутизации запроса
  • routeShutdown(): после окончания маршрутизации запроса
  • dispatchLoopStartup(): перед входом в цикл диспетчеризации
  • preDispatch(): перед началом маршрутизации отдельного действия
  • postDispatch(): после окончания маршрутизации отдельного действия
  • dispatchLoopShutdown(): после завершения цикла маршрутизации

Когда вы начнете думать о перехватчиках, представленных выше, у вас может возникнуть несколько вопросов, таких как, "Для чего здесь routeShutdown() и dispatchLoopStartup() перехватчики, ведь между ними ничего не происходит?". Основная причина кроется в семантике: вы можете захотеть изменить результаты маршрутизации после ее окончания, или вам может понадобиться модифицировать диспетчер перед входом в цикл диспетчеризации, а эти операции семантически разные. Наличие перехватчиков позволяет правильно разделять эти операции.

Другой вопрос, на который я отвечу, "Почему одновременно существуют dispatchLoopStartup/Shutdown() перехватчики и pre/postDispatch() перехватчики?". В ZF, как вы помните, есть цикл диспетчеризации, который позволяет вам использовать роутер для создания множественных запросов для диспетчеризации или запрашивать в ваших контроллерах дополнительные действия (actions). Поэтому мы имеем перехватчики на обоих концах всего цикла (dispatchLoopStartup() и dispatchLoopShutdown()), и на концах одной отдельно взятой итерации цикла диспетчеризации (preDispatch() and postDispatch()).

Плагин в ZF сейчас представляет собой просто класс, который наследуется от друого класса Zend_Controller_Plugin_Abstract. Этот абстрактный класс определяет пустые методы для каждого из перехватчиков. А уже конкретный плагин просто перезаписывает любой из этих методов для реализации его функционала. Во всех случаях, за исключением dispatchLoopShutdown(), методы перехватчиков принимают единственный аргумент $request - объект класса Zend_Controller_Request_Abstract (базовый класс запросов ZF MVC):

public function preDispatch(Zend_Controller_Request_Abstract $request)
{
}

Я часто употребляю выражения "ранне-запускаемые плагины" или "поздне-запускаемые плагины". Первые реализуются с помощью routeStartup(), routeShutdown(), и dispatchLoopStartup() - перехватчики, которые вызываются перед тем как начнется цикл диспетчеризации, и поэтому должны содержать логику, относящуюся ко всему приложению. Поздне-запускаемые плагины реализуются с помощью postDispatch() и dispatchLoopShutdown() - перехватчики, которые вызываются после того, как действие будет выполнено (postDispatch) или будет закончен цикл диспетчеризации (dispatchLoopShutdown).

Регистрация плагинов во Front-контроллере

Для того, что бы плагины начали работать, после инициализации их необходимо зарегистрировать во Front-контроллере. За это отвечает метод Zend_Controller_Front::registerPlugin():

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

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

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

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

$front->registerPlugin(new FooPlugin(), 1);   // будет вызван рано
$front->registerPlugin(new FooPlugin(), 100); // будет вызван поздно

Получение Плагинов из Front-контроллера

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

$front = Zend_Controller_Front::getInstance();
$fooPlugin = $front->getPlugin('FooPlugin');

Как плагины используются в Zend Framework

Теперь, когда мы знаем что такое плагин и как его регистрировать во front-контроллере, у нас появляется один волнующий вопрос: какие существуют применения плагинам? Для ответа на этот вопрос, давайте сначала рассмотрим как плагины используются в уже существующих ZF компонентах.

Zend_Layout

Zend_Layout, по необходимости, может использоваться с MVC компонентами. В этом случае он регистрирует плагин во front-контроллере. Плагин следит за перехватчиком postDispatch(), и регистрируется с поздним индексом стека, для того что бы он запускался после выполнения всех остальных плагинов.

Плагин "Layout" позволяет нам реализовать Двухэтапное представление (Two Step View pattern) в Zend Framework. Он захватывает содержание из объекта ответа и передает его в layout-объект для вставки этого содержания в определенный макет (layout view script).

Обработка ошибок

Другой пример - это плагин "ErrorHandler", который следит за перехватчиком postDispatch(), зарегистрированный также с поздним индексом стека. Он проверяет, было ли зарегистрировано исключение (exception) в программе с объектом ответа, и, если так, то он вызывает другое действия, завершая им цикл диспетчеризации. Это действие "error" в контроллере "error".

Потенциальные возможности использования Плагинов в ваших приложениях

Теперь, когда вы увидели конкретные примеры того, как используются плагины, какие потенциальные возможности использование вы можете предположить для своих приложений? Ниже перечислены некоторые примеры:

  • Инициализация приложения
  • Кэширование
  • Инициализация и кастомизация маршрутизации
  • Аутентификация и авторизация
  • Фильтр вывода финального кода XHTML

Пример: Плагин инициализации приложения

(В Zend Framework 1.8 появился новый компонент Zend_Application призванный облегчить инициализацию приложения, пр. переводчика) Давайте рассмотрим первую идею - инициализацию приложения. В большинстве примеров Zend Framework MVC приложений мы видим файл начальной загрузки (bootstrap-файл), который содержит инициализацию приложения - загружает конфигурацию, загружает все плагины, инициализирует представление и базу данных, и тд. Эта методика работает хорошо, но в скором может привести к небрежному разросшемуся файлу. Она же может привести к потенциальной утечке важной информации о вашей системе - в том случае если вдруг исходный код вашего файла отобразиться в броузере (все мы помним подобное фиаско Facebook в прошлом году :) )

Мы можем избежать этих недостатков, перенеся инициализацию в ранне-запускаемый плагин - routeStartup() плагин. Вот пример:

 /**
* Плагин инициализации приложения
*
* @uses Zend_Controller_Plugin_Abstract
*/
class My_Plugin_Initialization extends Zend_Controller_Plugin_Abstract
{
/**
* Конструктор
*
* @param string $env Execution environment
* @return void
*/
public function __construct($env)
{
$this->setEnv($env);
}

/**
* Перехватчик начала маршрутизации
*
* @param Zend_Controller_Request_Abstract $request
* @return void
*/
public function routeStartup(Zend_Controller_Request_Abstract $request)
{
$this->loadConfig()
->initView()
->initDb()
->setRoutes()
->setPlugins()
->setActionHelpers()
->setControllerDirectory();
}

// ...
}

Ваш файл начальной загрузки теперь будет выглядеть так:

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new My_Plugin_Initialization('production'));
$front->dispatch();

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

Пример: Кэширующий плагин

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

Для кэширования мы будем использовать следующие критерии:

  • Конфигурация кэширования будет передавать в конструктор
  • Кэшироваться будут только GET запросы
  • Перенаправления не будут кэшироваться
  • Любое action-действие контроллера может сказать плагину, что его не нужно кэшировать

Для этого плагина нам нужно реализовать два различных перехватчика. Первый будет останавливать цикл диспетчеризации в случае если в кэше есть страница соответствующая параметрам маршрутизации. Второй будет кэшировать страницу когда все действия из цикла диспетчеризации завершены. Итак, нам нужно реализовать dispatchLoopStartup() и dispatchLoopShutdown() перехватчики для завершения нашей задачи.

/**
* Кэширующий плагин
*
* @uses Zend_Controller_Plugin_Abstract
*/
class My_Plugin_Caching extends Zend_Controller_Plugin_Abstract
{
/**
* @var bool Нужно или нет запретить кэширование
*/
public static $doNotCache = false;

/**
* @var Zend_Cache_Frontend
*/
public $cache;

/**
* @var string Ключ кэширования
*/
public $key;

/**
* Конструктор: инициализируем кэш
*
* @param array|Zend_Config $options
* @return void
* @throws Exception
*/
public function __construct($options)
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
}
if (!is_array($options)) {
throw new Exception('Неверные опции кэширования; переменная $options должна быть массивом или Zend_Config объектом');
}

if (array('frontend', 'backend', 'frontendOptions', 'backendOptions') != array_keys($options)) {
throw new Exception('Неверные опции кэширования');
}

$options['frontendOptions']['automatic_serialization'] = true;

$this->cache = Zend_Cache::factory(
$options['frontend'],
$options['backend'],
$options['frontendOptions'],
$options['backendOptions']
);
}

/**
* Начинаем кэширование
*
* Определяем, закэширован ли объект ответа. Если так, то выводим его содержание
* пользователю, иначе начинаем кэширование.
*
* @param Zend_Controller_Request_Abstract $request
* @return void
*/
public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
{
if (!$request->isGet()) {
self::$doNotCache = true;
return;
}

$path = $request->getPathInfo();

$this->key = md5($path);
if (false !== ($response = $this->getCache())) {
$response->sendResponse();
exit;
}
}

/**
* Сохраняем кэш
*
* @return void
*/
public function dispatchLoopShutdown()
{
if (self::$doNotCache
|| $this->getResponse()->isRedirect()
|| (null === $this->key)
) {
return;
}

$this->cache->save($this->getResponse(), $this->key);
}

/**
* Получаем кэш
*
* @return text
*/
public function getCache()
{
return $this->cache->load($this->key));
}

}

В перехватчике dispatchLoopStartup() плагин совершает несколько вещей. Во-первых, он проверяет прислан ли текущий запрос методом GET (это одно из условий нашего кэширования). Затем он устанавливает ключ кэширования, основанный на параметрах текущего запроса, и проверяет закэширована ли страница. Если так, то он отправляет пользователю содержание объекта ответа. В dispatchLoopShutdown() перехватчике, мы проверяем несколько условий: установлен ли флаг запрета кэширование, произведен ли сейчас редирект, отсутствует ли ключ кэширования. Если хотя бы одно условие срабатывает, то мы завершаем работу перехватчика, иначе сохраняем объект ответа в кэше.

Как можно запретить кэширование действия? Вам нужно установить в true статический атрибут $doNotCache. К примеру, в нужном действии (action) вы можете:

My_Plugin_Caching::$doNotCache = true;

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

Внимательные читатели могут удивиться - почему я использую перехватчик dispatchLoopStartup() вместо routeStartup(), особенно если мне нужно только $request объект. Основная причина, это будущие усовершенствования, которые я захочу внедрить. Я могу легко расширить этот плагин, что бы можно было указывать специфические маршруты (routes), модули, контроллеры, действия (actions), которые не должные кешироваться. Или добавить возможность указывать альтернативные ключи кэширования для кастомизированных маршрутов (routes), и тд. Все эти действия могут выполняться только после завершения процесса маршрутизации.

Однако, основное назначение этого примера - это использование нескольких перехватчиков плагина для достижения нашей цели - кэширования.

Перенаправление на другие действия (actions)

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

Объект запроса содержит эту информация в специальном флаге isDispatched. Если этот флаг установлен в false, то это означает что текущий запрос еще не был диспетчеризирован; другими словами - это новый запрос (обычно этот флаг пребывает в состоянии false до входа в цикл диспетчеризации или после вызова функции _forward() в действии (action)). Если флаг установлен в true, то это означает, что текущий запросуже был диспетчеризирован.

Таким образом, для диспетчеризации другого действия, просто обновите состояние запроса, и установите этот флаг в false. Как пример, для перенаправления на SearchController::formAction(), в вашем перехватчике плагина должен быть расположен похожий код:

$request->setModuleName('default')
->setControllerName('search'))
->setActionName('form')
->setDispatched(false);
}

Для проверки того был ли диспетчеризирован запрос, сделайте следующее:

if ($requst->isDispatched()) {
// запрос уже был обработан
} else {
// новый запрос, еще не был диспетчеризирован
}

Замечание, возможно вы захотите посмотреть плагин ActionStack и хелпер ActionStack, добавленные в версии 1.5.0, которые позволяют вам добавлять действия в стек. Плагин забирает из стека по одному действию (action) в каждой итерации цикла диспетчеризации (кроме случаев, когда другое действие ожидает диспетчеризации), позволяя вам передавать различные действия (actions) в цикл диспетчеризации во время выполнения приложения.

Другие размышления

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

Однако, если функционал имеет дело со всем сайтом, такой как представленные в примерах плагины инициализации и кэширования, то плагины будут более подходящимрешением.

Заключение

Буду надеяться этот туториал показал вам, что плагины не являются сложной темой. Плагины предоставляют замечательную возможность добавить функционал в некоторые ключевые точки Zend Framework MVC приложений, и обеспечивают вашему приложению большие возможности связывания и конфигурирования.

Тема для обсуждения на форуме

Лучший способ следить за обновлениями сайта это подписаться на RSS
Если информация была полезной для вас, вы можете поддержать сайт.
Комментарии:
Alexey 21.05.2009 07:34 #
Спасибо за статью. Теперь кое-что прояснилось в голове.
Ответить
deniska 21.05.2009 14:13 #
Спасибо! Очень доступно.
Ответить
Bratok 25.08.2009 11:22 #
Классный пост! Спасибо большое и РЕСПЕКТ афтору.
Ответить
Dock 29.09.2009 02:14 #
Есть не большой вопрос: Если делать инициализацию роутов в routeStartup, они упорно не добавляются или отрабатывают не правильно. В чем может быть проблема?
P.S. Загрузка роутов правильная.
Ответить
freeneutron 17.12.2010 18:15 #
Полезная статья. Только здесь не совсем понятно:
dispatchLoopStartup(): перед входом в цикл диспетчеризации
dispatchLoopShutdown(): после завершения цикла маршрутизации
Цикл один, а зовется по разному
Ответить
Александр Махомет 17.12.2010 18:21 #
Опечатка
Ответить
Уважаемые пользователи. Комментарии не для того чтобы:
  1. Спрашивать почему у вас не работает код, для этого есть тема форума закрепленная за статьей.
  2. Спрашивать как реализовать ту или иную функциональность, для этого необходимо создать свою тему на форуме.

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

Комментарии имеют древовидную структуру.
Если вы хотите ответить на определенный комментарий - нажмите на ссылку "Ответить" возле этого комментария.

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