# Контроллеры действий

## Введение

`Zend_Controller_Action` - абстрактный класс, который можно использовать для реализации контроллеров действий для последующего их использования с фронт-контроллером при разработке сайта, основанного на паттерне Model-View-Controller (MVC).

Для того, чтобы использовать `Zend_Controller_Action`, нужно создать его подкласс в своей действующей директории контроллеров (или расширить его для создания своего базового класса контроллеров действий). Работа с ним в основном сводится к созданию его подкласса и написании методов действий, соответствующих различным действиям, которые должен обрабатывать этот контроллер. Маршрутизатор и диспетчер компоненты `Zend_Controller` будут считать за методы действий все методы в классе контроллера с именем, заканчивающимся на 'Action'.

Для примера предположим, что ваш класс определен следующим образом:

    class FooController extends Zend_Controller_Action
    {
        public function barAction()
        {
            // делает что-нибудь
        }

        public function bazAction()
        {
            // делает что-нибудь
        }
    }

Приведенный выше класс `FooController` (контроллер `foo`) определяет два действия - `bar` и `baz`.

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

### Поведение по умолчанию

По умолчанию фронт-контроллер активирует помощника действий ViewRenderer. Этот помощник обеспечивает добавление объекта вида в контроллер и автоматический рендеринг видов. Вы можете отключить его в своем контроллере действия, используя один из следующих методов:

    class FooController extends Zend_Controller_Action
    {
        public function init()
        {
            // Локально, только для данного контроллера:
            $this->_invokeArgs['noViewRenderer'] = true;

            // Глобально:
            $this->_helper->removeHelper('viewRenderer');

            // Тоже глобально, но должен использоваться вместе
            // с локальной версией для того, чтобы распространить
            // действие на данный контроллер:
            Zend_Controller_Front::getInstance()
                ->setParam('noViewRenderer', true);
        }
    }

`initView()`, `getViewScript()`, `render()` и `renderScript()` служат посредниками для `ViewRenderer`, пока этот помощник находится в брокере помощников и не установлен флаг `noViewRenderer`.

Вы можете также отключить рендеринг для отдельного вида посредством установки флага `noRender` в `ViewRenderer`:

    class FooController extends Zend_Controller_Action
    {
        public function barAction()
        {
            // отключение авторендеринга для этого действия:
            $this->_helper->viewRenderer->setNoRender();
        }
    }

Основные причины для отключения `ViewRenderer` - вам просто не нужен объект вида или если вы не производите рендеринг через скрипты вида (например, когда используется контроллер действий для обслуживания протоколов веб-сервисов, таких, как SOAP, XML-RPC, или REST). В большинстве случаев не нужно будет глобально отключать `ViewRenderer`, только избирательно в отдельных контроллерах или действиях.

## Инициализация объекта

Несмотря на то, что вы всегда можете переопределить конструктор контроллера действий, мы не рекомендуем делать это. `Zend_Controller_Action::__construct()` выполняет некоторые важные задачи, такие, как регистрация объектов запроса и ответа, аргументов вызова, переданных из фронт-контроллера. Если необходимо переопределить контроллер, то всегда вызывайте конструктор родительского класса `parent::__construct($request, $response, $invokeArgs)` в конструкторе подкласса.

Более подходящим способом настройки инстанцирования является использование метода `init()`, который вызывается в конце выполнения `__construct()`. Например, если вы хотите устанавливать соединение с БД при инстанцировании:

    class FooController extends Zend_Controller_Action
    {
        public function init()
        {
            $this->db = Zend_Db::factory('Pdo_Mysql', array(
                'host'     => 'myhost',
                'username' => 'user',
                'password' => 'XXXXXXX',
                'dbname'   => 'website'
            ));
        }
    }

## Перехватчики Pre- и Post-Dispatch

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

## Аксессоры

С объектом контроллера регистрируется несколько объектов и переменных, они имеют свои методы-аксессоры.

- Объект запроса: через метод `getRequest()` извлекается объект запроса, который использовался для вызова данного действия.

- Объект ответа: через метод `getResponse()` извлекается объект ответа, объединяющий в себе заголовки и содержимое ответа. Некоторые типичные вызовы могут выглядеть следующим образом:

      $this->getResponse()->setHeader('Content-Type', 'text/xml');
      $this->getResponse()->appendBody($content);

- Аргументы вызова: фронт-контроллер может добавлять параметры в маршрутизатор, диспетчер и контроллер действий. Для их получения используйте `getInvokeArg($key)`, можно также извлечь весь список аргументов, используя метод `getInvokeArgs()`.

- Параметры запроса: Объект запроса заключает в себе параметры запроса, такие, как значения _GET, _POST, или пользовательские параметры, определенные в пути URL. Для их получения используйте `_getParam($key)` или `_getAllParams()`. Вы можете также установить параметры запроса, используя метод `_setParam()`, это полезно при перенаправлении на другие действия через метод `_forward()`.

  Для определения того, существует ли параметр или нет (полезно для логического ветвления), используйте `_hasParam($key)`.

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

      // Используется значение по умолчанию 1, если id не установлен
      $id = $this->_getParam('id', 1);

      // Вместо:
      if ($this->_hasParam('id') {
          $id = $this->_getParam('id');
      } else {
          $id = 1;
      }

## Интеграция вида

### По умолчанию интеграция вида производится через ViewRenderer

Изложенное в этом разделе действительно только в том случае, если вы явным образом отключили ViewRenderer. Иначе вы можете спокойно пропустить этот раздел.

`Zend_Controller_Action` предоставляет простейший и гибкий механизм интеграции видов. Два метода осуществляют это: `initView()` и `render()`. Первый метод выполняет отложенную загрузку открытого свойства `$view`, второй выполняет рендеринг вида, основываясь на запрошенном в данный момент действии, используя иерархию директорий для определения пути к скрипту.

### Инициализация вида

`initView()` инициализирует объект вида. `render()` вызывает `initView()` для извлечения объекта вида, но этот объект может быть инициализирован в любое время. По умолчанию `initView()` заполняет свойство `$view` объектом `Zend_View`, но может также использоваться любой класс, реализующий интерфейс `Zend_View_Interface`. Если `$view` уже инициализирован, то просто возвращается это свойство.

Реализация, используемая по умолчанию, делает следующие предположения по структуре директорий:

    applicationOrModule/
        controllers/
            IndexController.php
        views/
            scripts/
                index/
                    index.phtml
            helpers/
            filters/

Другими словами, предполагается, что скрипты вида находятся в поддиректории `views/scripts/` и поддиректория `views` должна содержать родственный функционал того же уровня (это могут быть помощники, фильтры). Когда определяется имя и путь к скрипту вида, то в качестве базового пути используется директория `views/scripts/` с директориями, именованными в соответствии с отдельными контроллерами, что дает иерархию скриптов вида.

### Рендеринг видов

`render()` имеет следующую сигнатуру:

    string render(string $action = null,
                  string $name = null,
                  bool $noController = false);

`render()` рендерит скрипт вида. Если не были переданы аргументы, то предполагается, что запрашивается скрипт `[controller]/[action].phtml` (где `.phtml` - значение свойства `$viewSuffix`). Передача значения для `$action` вызовет генерацию этого шаблона в поддиректории `[controller]`. Для того, чтобы отменить использование поддиректории `[controller]`, передавайте значение true для `$noController`. Шаблоны рендерятся в объект ответа, если же вы хотите сохранить результат в именованный сегмент объекта ответа, то передавайте значение для `$name`.

Поскольку имена контроллера и действия могут содержать символы-ограничители слов, такие, как '_', '.', и '-', то `render()` нормализует их к '-', когда определяет имя скрипта. Внутри себя для такой нормализации он использует ограничители слов и путей для диспетчера. Таким образом, запрос к `/foo.bar/baz-bat` приведет к рендерингу скрипта `foo-bar/baz-bat.phtml`. Если ваш метод действия содержит camelCase, то следует иметь в виду, что при определении имени скрипта вида результатом будут разделенные '-' слова.

Некоторые примеры:

    class MyController extends Zend_Controller_Action
    {
        public function fooAction()
        {
            // Рендеринг my/foo.phtml
            $this->render();

            // Рендеринг my/bar.phtml
            $this->render('bar');

            // Рендеринг baz.phtml
            $this->render('baz', null, true);

            // Рендеринг my/login.phtml в сегмент 'form' объекта ответа
            $this->render('login', 'form');

            // Рендеринг site.phtml в сегмент 'page' объекта ответа,
            // при этом не используется поддиректория 'my/'
            $this->render('site', 'page', true);
        }

        public function bazBatAction()
        {
            // Рендеринг my/baz-bat.phtml
            $this->render();
        }
    }

## Сервисные методы

Кроме аксессоров и методов интеграции видов, `Zend_Controller_Action` имеет несколько сервисных методов для выполнения распространенных зачач в методах действий (или в методах pre- и post-dispatch).

- `_forward($action, $controller = null, $module = null, array $params = null)`: выполяет другое действие. Если был вызван в `preDispatch()`, то запрошенноое в данный момент действие будет пропущено в пользу нового. Иначе действие, запрошенное в _forward(), будет выполнено после того, как было выполнено текущее действие.

- `_redirect($url, array $options = array())`: производит перенаправление по другому адресу. Этот метод принимает URL и опционально набор опций. По умолчанию он производит перенаправление HTTP 302.

  Опции могут включать в себя одну или более из следующих:

  - exit: производить или нет выход после этого. Если установлена, то будет произведены надлежащее закрытие всех открытых сессий и перенаправление.

    Вы можете установить эту опцию глобально в контроллере, используя аксессор `setRedirectExit()`.

  - prependBase: добавлять или нет базовый URL из объекта запроса в начало данного URL.

    Вы можете установить эту опцию глобально в контроллере, используя аксессор `setRedirectPrependBase()`.

  - code: какой код HTTP использовать при перенаправлении. По умолчанию используется HTTP 302. Могут использоваться любые коды от 301 до 306.

    Вы можете установить эту опцию глобально в контроллере, используя аксессор `setRedirectCode()`.

## Создание подклассов контроллера действий

Задумано, что в порядке создания контроллеров действий должны создаваться подклассы от `Zend_Controller_Action`. Как минимум, вам нужно будет определить методы действий, которые может вызывать контроллер.

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

### Обрабаботка обращений к несуществующим действиям

Если сделан запрос к контроллеру, который содержит в себе не определенный в контроллере метод действия, то вызывается метод `Zend_Controller_Action::__call()`. `__call()` является магическим методом для перегрузки методов в PHP.

По умолчанию этот метод бросает исключение `Zend_Controller_Action_Exception`, означающее, что требуемый метод не найден в контроллере. Если требуемый метод заканчивается строкой 'Action', то предполагается, что было запрошено действие и оно не существует; такая ошибка приводит к исключению с кодом 404. В остальных случаях бросается исключение с кодом 500. Это позволяет легко дифференцировать в обработчике ошибок случаи, когда страница не найдена, и когда произошла ошибка приложения.

Например, если вы хотите выводить сообщение об ошибке, то можете написать нечто подобное:

    class MyController extends Zend_Controller_Action
    {
        public function __call($method, $args)
        {
            if ('Action' == substr($method, -6)) {
                // Если метод действия не найден, то рендерится шаблон ошибки
                return $this->render('error');
            }

            // все другие методы бросают исключение
            throw new Exception('Invalid method "'
                                . $method
                                . '" called',
                                500);
        }
    }

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

    class MyController extends Zend_Controller_Action
    {
        public function indexAction()
        {
            $this->render();
        }

        public function __call($method, $args)
        {
            if ('Action' == substr($method, -6)) {
                // Если метод действия не был найден, то производится
                // переход к действию index
                return $this->_forward('index');
            }

            // все другие методы бросают исключение
            throw new Exception('Invalid method "'
                                . $method
                                . '" called',
                                500);
        }
    }

Как и метод `__call()`, любые аксессоры, сервисные методы, методы инициализации, вида и перехвата, упомянутые ранее в этом разделе, могут быть переопределены для того, чтобы приспособить свои контроллеры под конкретные нужды. Например, если вы храните свои объекты вида в реестре, то можете модифицировать свой метод `initView()`:

    abstract class My_Base_Controller extends Zend_Controller_Action
    {
        public function initView()
        {
            if (null === $this->view) {
                if (Zend_Registry::isRegistered('view')) {
                    $this->view = Zend_Registry::get('view');
                } else {
                    $this->view = new Zend_View();
                    $this->view->setBasePath(dirname(__FILE__) . '/../views');
                }
            }

            return $this->view;
        }
    }

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