7.3. Создание подклассов

7.3.1. Введение

Система Zend_Controller построена с расчетом на расширение посредством написания новых классов, которые реализуют интерфейсы Zend_Controller_Router_Interface и Zend_Controller_Dispatcher_Interface, либо расширения классов Zend_Controller_Request_Abstract, Zend_Controller_Response_Abstract и Zend_Controller_Action.

Возможные причины для создания своих классов:

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

  • Вам нужно реализовать маршрутизацию для чего-либо совершенно иного. Класс Zend_Controller_Router работает только с URI. Возможно, вы захотите использовать паттерн MVC для разработки других типов программ — таких, как консольное или CGI приложение. В случае консольного приложения специализированный объект запроса может обрабатывать аргументы в командной строке.

  • Механизм, предлагаемый Zend_Controller_Dispatcher является неподходящим. Конфигурация по умолчанию предполагает, что контроллеры являются классами, а действия — методами в этих классах. Однако есть много других стратегий для реализации этого. Примером такой стратегии может быть та, где контроллеры являются каталогами, а действия — файлами внутри этих каталогов.

  • Вы хотите обеспечить дополнительные возможности, которые будут унаследованы всеми вашими контроллерами. Например, Zend_Controller_Action по умолчанию не интегрирован с Zend_View. Однако вы можете расширить свой собственный контроллер для реализации этого, и его использование не потребует изменения находящегося в поставке Zend_Controller_Router или Zend_Controller_Dispatcher

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

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

7.3.2. Соглашения

Сильно рекомендуется следование этим соглашениям по именованию и хранению файлов при создании подклассов любых классов Zend_Controller. Следование этим соглашениям будет гарантировать легкое понимание вашего проекта другим программистом, знакомым с Zend Framework.

7.3.2.1. Префикс

Классы, входящие в Zend Framework, следуют соглашению, по которому имя каждого класса начинается с "Zend_". Это префикс. Мы рекомендуем называть все свои классы аналогичным образом, т.е. если ваша компания называется Widget Inc., префиксом должен быть"Widget_".

7.3.2.2. Структура категорий

Классы Zend_Controller хранятся в каталоге библиотек, как показано ниже:

/library
  /Zend
    /Controller
      Action.php
      Dispatcher.php
      Router.php

Когда создаете наследников от классов Zend_Controller, то рекомендуется сохранять их в такой же структуре, под вашим префиксом. Это облегчит их поиск для тех, кто просматривает код вашего приложения.

Например, проект компании Widget Inc., который реализует только свой маршрутизатор, может выглядеть следующим образом:

/library
  /Zend
  /Widget
    /Controller
      Router.php
      README.txt
         

Обратите внимание, что в этом примере каталог Widget/Controller/ копирует каталог Zend/Controller/ везде, где это возможно. В данном случае это класс Widget_Controller_Router, который может быть либо подклассом Zend_Controller_Router, либо замещать его, реализуя интерфейс Zend_Controller_Router_Interface.

Еще обратите внимание на то, что в этом примере в каталоге Widget/Controller/ размещен файл README.txt. Zend сильно приветствует документирование проектов посредством отдельных тестов и документации для клиентов. Несмотря на это, рекомендуется еще размещать простой файл README.txt, в котором кратко описываются изменения и то, как они работают.

7.3.3. Фронт-контроллер

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

При создании его подкласса, нужно, как минимум, переопределить его метод getInstance().

class My_Controller_Front extends Zend_Controller_Front
{
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }
}

Переопределение метода getInstance() необходимо для того, чтобы последующие вызовы Zend_Controller_Front::getInstance() возвращали экземпляры вашего нового подкласса вместо экземпляра Zend_Controller_Front - это особенно полезно для некоторых альтернативных маршрутизаторов и помощников представления (view helpers).

Кроме getInstance(), есть много методов, которые можно переопределить:

    /**
     * Удаляет все свойства экземпляра синглетона
     *
     * Используется в основном для тестирования; может использоваться для
     * цепочки фронт-контроллеров
     * 
     * @return void
     */
    public function resetInstance();

    /**
     * Функция для удобства, вызывает setControllerDirectory()->setRouter()->dispatch()      
     *
     * В PHP 5.1.x, вызов статического метода никогда не заполняет $this,
     * поэтому run() может вызываться после настройки своего фронт-контроллера
     *      
     * @param string|array $controllerDirectory Путь ко классам контроллеров
     * или массив таких путей
     * @return void
     * @throws Zend_Controller_Exception если вызывается из объекта
     */
    public static function run($controllerDirectory);

    /**
     * Добавляет директорию контроллеров в стек директорий контроллеров
     *
     * Если передан агрумент $args и он является строкой, то он исползуется
     * как ключ массива для данной директории
     * 
     * @param string $directory 
     * @param mixed $args Необязательный аргумент; Если является строкой,
     * то используется как ключ массива
     * @return Zend_Controller_Front
     */
    public function addControllerDirectory($directory, $args = null);

    /**
     * Устанавливает директорию контроллеров
     *
     * Сохраняет директорию контроллеров для передачи диспетчеру. Параметром
     * могут быть массив или строка, содержащая единственную директорию
     *
     * @param string|array $directory Путь ко классам контроллеров или массив
     * таких путей
     * @return Zend_Controller_Front
     */
    public function setControllerDirectory($directory);

    /**
     * Возвращает директории контроллеров
     *
     * Возвращает сохраненные директории контроллеров
     *
     * @return string|array
     */
    public function getControllerDirectory();

    /**
     * Устанавливает контроллер, используемый по умолчанию (неформатированная строка)
     *
     * @param string $controller
     * @return Zend_Controller_Front
     */
    public function setDefaultController($controller);

    /**
     * Возвращает контроллер, используемый по умолчанию (неформатированная строка)
     *
     * @return string
     */
    public function getDefaultController();

    /**
     * Устанавливает действие, используемое по умолчанию (неформатированная строка)
     *
     * @param string $action
     * @return Zend_Controller_Front
     */
    public function setDefaultAction($action);

    /**
     * Возвращает действие, используемое по умолчанию (неформатированная строка)
     *
     * @return string
     */
    public function getDefaultAction();

    /**
     * Устанавливает класс/объект запроса
     *
     * Устанавливает объект запроса. Объект запроса содержит переменные запроса.
     *
     * If a class name is provided, it will instantiate it
     *
     * @param string|Zend_Controller_Request_Abstract $request
     * @throws Zend_Controller_Exception Генерируется в случае неверного класса запроса
     * @return Zend_Controller_Front
     */
    public function setRequest($request);

    /**
     * Возвращает объект запроса
     *
     * @return null|Zend_Controller_Request_Abstract
     */
    public function getRequest();

    /**
     * Устанавливает объект/класс маршрутизатора
     *
     * Устанавливает объект маршрутизатора. Маршрутизатор должен обрабатывать
     * запросы на нахождение соответствующих контроллера и действия
     *
     * Если передано имя класса, то инстанцируется маршрутизатор с любыми
     * параметрами, зарегистрированными через {@link setParam()} или
     * {@link setParams()}.
     *
     * @param string|Zend_Controller_Router_Interface $router
     * @throws Zend_Controller_Exception Генерируется в случае неверного класса маршрутизатора
     * @return Zend_Controller_Front
     */
    public function setRouter($router);

    /**
     * Возвращает объект маршрутизатора.
     *
     * Инстанцирует Zend_Controller_Router, если маршрутизатор не был установлен.
     *
     * @return null|Zend_Controller_Router_Interface
     */
    public function getRouter();

    /**
     * Устанавливает базовый URL, используемый для запросов
     *
     * Используется для установки базовой части URL в REQUEST_URI, которая
     * используется позднее при определении PATH_INFO и т.д., Примеры:
     * - /admin
     * - /myapp
     * - /subdir/index.php
     *
     * URL не должне включать полный URI. Не используйте:
     * - http://example.com/admin
     * - http://example.com/myapp
     * - http://example.com/subdir/index.php
     *
     * Если передано значение null, то используется автоопределение (по умолчанию).
     * 
     * @param string $base
     * @return Zend_Controller_Front
     * @throws Zend_Controller_Exception для $base, не являющегося строкой
     */
    public function setBaseUrl($base = null);

    /**
     * Возвращает установленный на данный момент базовый URL
     * 
     * @return string
     */
    public function getBaseUrl();

    /**
     * Устанавливает объект диспетчера. Диспетчер должен принимать объект
     * запроса, инстанцировать контроллер, и вызывать метод действия в этом
     * контроллере
     *
     * @param Zend_Controller_Dispatcher_Interface $dispatcher
     * @return Zend_Controller_Front
     */
    public function setDispatcher(Zend_Controller_Dispatcher_Interface $dispatcher);

    /**
     * Возвращает объект диспетчера.
     *
     * @return Zend_Controller_DispatcherInteface
     */
    public function getDispatcher();

    /**
     * Устанавливает объект/класс ответа
     *
     * Устанавливает объект ответа. Объект ответа является контейнером для
     * ответа действия и заголовков.
     *
     * Если передано имя класса, то инстанцируется объект ответа.
     *
     * @param string|Zend_Controller_Response_Abstract $response
     * @throws Zend_Controller_Exception если передан неверный класс ответа
     * @return Zend_Controller_Front
     */
    public function setResponse($response);

    /**
     * Возвращает объект ответа.
     *
     * @return null|Zend_Controller_Response_Abstract
     */
    public function getResponse();

    /**
     * Добавляет или изменяет параметр, используемый при инстанцировании
     * контроллера действий
     *
     * @param string $name
     * @param mixed $value
     * @return Zend_Controller_Front
     */
    public function setParam($name, $value);

    /**
     * Устанавливает параметры для передачи конструктору контроллера действий
     *
     * @param array $params
     * @return Zend_Controller_Front
     */
    public function setParams(array $params);

    /**
     * Возвращает один параметр из стека параметров контроллера
     * 
     * @param string $name 
     * @return mixed
     */
    public function getParam($name);

    /**
     * Возвращает параметры инстанцирования контроллера действий.
     *
     * @return array
     */
    public function getParams();

    /**
     * Очищает стек параметров контроллера действий.
     *
     * По умолчанию удаляет все параметры. Если передано имя параметра, то
     * удаляется только этот параметр; если передан массив имен параметров, то
     * удаляются все эти параметры.
     * 
     * @param null|string|array один ключ или массив ключей параметров
     * для удаления
     * @return Zend_Controller_Front
     */
    public function clearParams($name = null);

    /**
     * Регистрирует плагин.
     *
     * @param Zend_Controller_Plugin_Abstract $plugin
     * @return Zend_Controller_Front
     */
    public function registerPlugin(Zend_Controller_Plugin_Abstract $plugin);

    /**
     * Отменяет регистрацию плагина.
     *
     * @param Zend_Controller_Plugin_Abstract $plugin
     * @return Zend_Controller_Front
     */
    public function unregisterPlugin(Zend_Controller_Plugin_Abstract $plugin);

    /**
     * Устанавливает, должны ли исключения, происходящие в цикле
     * диспетчеризации, генерироваться как обычно или отлавливаться и включаться
     * в объект ответа.
     *
     * Поведение по умолчанию - отлов исключений и включение в объект ответа;
     * вызывайте этот метод, чтобы они генерировались как обычно.
     *
     * @param boolean $flag По умолчанию равно true
     * @return boolean Возвращает текущую установку
     */
    public function throwExceptions($flag = null);

    /**
     * Устанавливает, должен ли {@link dispatch()} возвращать объект ответа без
     * вывода. По умолчанию вывод производится автоматически и метод dispatch()
     * ничего не возвращает.
     * 
     * @param boolean $flag 
     * @return boolean Возвращает текущую установку
     */
    public function returnResponse($flag = null);

    /**
     * Производит обработку HTTP запроса 
     *
     * @param Zend_Controller_Request_Abstract|null $request
     * @param Zend_Controller_Response_Abstract|null $response
     * @return void|Zend_Controller_Response_Abstract Возвращает объект ответа,
     * если returnResponse() == true
     */
    public function dispatch(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null);

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

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

7.3.4. Абстрактный класс запроса

Абстрактный класс Zend_Controller_Request_Abstract определяет некоторые методы:

    /**
     * @return string
     */
    public function getControllerName();

    /**
     * @param string $value 
     * @return self
     */
    public function setControllerName($value);

    /**
     * @return string
     */
    public function getActionName();

    /**
     * @param string $value 
     * @return self
     */
    public function setActionName($value);

    /**
     * @return string
     */
    public function getControllerKey();

    /**
     * @param string $key 
     * @return self
     */
    public function setControllerKey($key);

    /**
     * @return string
     */
    public function getActionKey();

    /**
     * @param string $key 
     * @return self
     */
    public function setActionKey($key);

    /**
     * @param string $key 
     * @return mixed
     */
    public function getParam($key);

    /**
     * @param string $key 
     * @param mixed $value 
     * @return self
     */
    public function setParam($key, $value);

    /**
     * @return array
     */
     public function getParams();

    /**
     * @param array $array 
     * @return self
     */
    public function setParams(array $array);

    /**
     * @param boolean $flag 
     * @return self
     */
    public function setDispatched($flag = true);

    /**
     * @return boolean
     */
    public function isDispatched();
}

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

7.3.5. Интерфейс маршрутизатора

Интерфейс Zend_Controller_Router_Interface определяет только один метод:

<?php

  /**
   * @param  Zend_Controller_Request_Abstract $request
   * @throws Zend_Controller_Router_Exception
   * @return Zend_Controller_Request_Abstract
   */
  public function route(Zend_Controller_Request_Abstract $request);

?>   

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

7.3.6. Интерфейс диспетчера

Zend_Controller_Front сначала вызывает маршрутизатор для получения первого доступного диспетчеризации действия в запросе. Затем он входит в цикл диспетчеризации

В этом цикле он сначала устанавливает флаг диспетчеризации в объекте запроса и затем обрабатывает запрос (инстанцирует контроллер, вызывает его метод действия) Если метод действия (или методы pre/postDispatch в установленном плагине) сбрасывают флаг диспетчеризации, то фронт-контроллер будет выполнять другую итерацию в цикле диспетчеризации с тем действием, которое установлено на данный момент в объекте запроса. Это позволяет последовательно обрабатывать действия до тех пор, пока вся работа не будет выполнена.

Интерфейс Zend_Controller_Dispatcher_Interface определяет два метода:

<?php

/**
 * @param  Zend_Controller_Request_Abstract $request
 * @return boolean
 */
public function isDispatchable(Zend_Controller_Request_Abstract $request);

?>   

isDispatchable() проверяет, является ли запрос пригодным к обработке. Если является, то возвращается TRUE, иначе FALSE. Определение того, что пригодно для обработки диспетчером, зависит от класса, реализующего интерфейс. В случае реализации по умолчанию (Zend_Controller_Dispatcher) это означает, что файл контроллера существует, класс существует в этом файле и метод, реализующий действие, существует в этом классе.

<?php

/**
 * @param  Zend_Controller_Request_Abstract $route
 * @return Zend_Controller_Request_Abstract
 */
public function dispatch(Zend_Controller_Request_Abstract $request);

?>   

dispatch() является основным рабочим методом. Этот метод должен выполнять действие контроллера. Он должен возвращать объект диспетчеризации.

7.3.7. Контроллер действий

Zend_Controller_Action осуществляет контроль за различными действиями в приложении. Абстрактный класс предоставляет следующие методы:

    /**
     * @param Zend_Controller_Request_Abstract $request Объект запроса
     * @param Zend_Controller_Response_Abstract $response Объект ответа
     * @param array $args Необязательный ассоциативный массив
     * конфигурационных переменных и переменных среды
     */
    public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $args = array());

    /**
     * @return void
     */
    public function init();

    /**
     * @return Zend_Controller_Request_Abstract
     */
    public function getRequest();

    /**
     * @param Zend_Controller_Request_Abstract $request 
     * @return self
     */
    public function setRequest(Zend_Controller_Request_Abstract $request);

    /**
     * @return Zend_Controller_Response_Abstract
     */
    public function getResponse();

    /**
     * @param Zend_Controller_Response_Abstract $response 
     * @return self
     */
    public function setResponse(Zend_Controller_Response_Abstract $response);

    /**
     * @return array
     */
    public function getInvokeArgs();

    /**
     * @return mixed
     */
    public function getInvokeArg($name);

    public function preDispatch();

    public function postDispatch();

    /**
     * @param string $methodName
     * @param array $args
     */
    public function __call($methodName, $args);

    /**
     * @param null|Zend_Controller_Request_Abstract $request Объект запроса для
     * использования (необязательный)
     * @param null|Zend_Controller_Response_Abstract $response Объект запроса 
     * для использования (необязательный)
     * @return Zend_Controller_Response_Abstract
     */
    public function run(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null);

Конструктор требует передачи объектов запроса и ответа, также он принимает массив любых дополнительных конфигурационных аргументов в качестве третьего аргумента. Этот массив содержит параметры, зарегистрированные через методы setParam() или setParams() фронт-контроллера. В конце своего выполнения конструктор передает управление методу init().

Несмотря на то, что вы можете переопределять конструктор, мы советуем выполнять любые действия по инициализации в методе init() для того, чтобы обеспечить должную регистрацию объектов запроса и ответа.

Получить доступ к любым конфигурационным аргументам, переданным конструктору, можно через методы getInvokeArg() и getInvokeArgs(). Рекомендуется использовать эти аргументы для передачи в такие объекты, как объект вида, аутентификации/авторизации или реестра объектов. Например:

$front = Zend_Controller_Front::getInstance();
$front->setParam('view', new Zend_View())
      ->setControllerDirectory($config->controller->directory);
$response = $front->dispatch();

// Пример контроллера действия:
class FooController extends Zend_Controller_Action
{
    protected $_view = null;

    public function init()
    {
        $this->_view = $this->getInvokeArg('view');
    }
}

Когда действие запущено диспетчером, можно выполнять обработку до и после действия с помощью методов preDispatch() и postDispatch(). По умолчанию они не выполняют никакой работы.

Метод __call() принимает вызовы незарегистрированных в классе действий. По умолчанию он вызывает исключение, если действие не определено. Это должно происходить только тогда, когда не определен метод действия по умолчанию.

По умолчанию соглашение по именованию методов действий подразумевает имя метода следующего вида: lowercaseAction, где 'lowercase' обозначает имя действия, а 'Action' обозначает, что этот метод является методом действия. Таким образом, URL http://framework.zend.com/foo/bar будет вызывать действие FooController::barAction().

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

$controller = new FooController(
    new Zend_Controller_Request_Abstract(),
    new Zend_Controller_Response_Abstract()
);
$controller->run();
[Замечание] Использование фронт-контроллера и контроллеров действии

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

7.3.8. Объект ответа

Объект ответа служит для сбора содержимого и заголовков из вызываемых действий и возвращает их клиенту. Он имеет следующие методы:

    /**
     * @param string $name Имя заголовка
     * @param string $value Значение заголовка
     * @param boolean $replace Должен ли или нет заменять собой уже
     * зарегистрированный в объекте заголовок с тем же именем
     * @return self
     */
    public function setHeader($name, $value, $replace = false);

    /**
     * @return array
     */
    public function getHeaders();

    /**
     * @return void
     */
    public function clearHeaders();

    /**
     * Отправляет все заголовки
     * @return void
     */
    public function sendHeaders();

    /**
     * @param string $content
     * @return self
     */
    public function setBody($content);

    /**
     * @param string $content
     * @return self
     */
    public function appendBody($content);

    /**
     * @return string
     */
    public function getBody();

    /**
     * Выводит контент
     * @return void
     */
    public function outputBody();

    /**
     * @param Exception $e 
     * @return self
     */
    public function setException(Exception $e);

    /**
     * @return null|Exception
     */
    public function getException();

    /**
     * @return boolean
     */
    public function isException();

    /**
     * @param boolean $flag
     * @return boolean
     */
    public function renderExceptions($flag = null);

    /**
     * @return string
     */
    public function __toString();

Метод setBody() заменяет весь контент; мы советуем использовать вместо него метод appendBody(). __toString() должен выводить весь контент и отправлять все заголовки.

В объекте ответа также отлавливаются и регистрируются исключения из контроллера действий (до тех пор, пока не будет включено Zend_Controller_Front::throwExceptions()). Метод isException() должен возвращать булево значение, показывающее, было ли сгенерировано исключение или нет. renderExceptions() используется для определения того, должен ли метод __toString() выводить исключение, если оно было поймано.