# ContextSwitch и AjaxContext

Помощник действий `ContextSwitch` предназначен для облегчения возврата ответов в различных форматах. Помощник `AjaxContext` является специализированной версией `ContextSwitch`, которая облегчает возврат ответа на запросы, произведенные посредством XmlHttpRequest.

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

- Выключает макеты, если они включены.

- Устанавливает альтернативный суффикс вида, это позволяет эффективно разделять скрипты видов для различных контекстов.

- Отправляет HTTP-заголовки ответа, требуемые в данном контексте.

- Опционально вызывает предопределенные функции обратного вызова для установки контекста и/или пост-обработки.

В качестве примера рассмотрим следующий контроллер:

    class NewsController extends Zend_Controller_Action
    {
        /**
         * Начальная страница, производится переход к listAction()
         */
        public function indexAction()
        {
            $this->_forward('list');
        }

        /**
         * Выводит список новостей
         */
        public function listAction()
        {
        }

        /**
         * Просмотр одной новости
         */
        public function viewAction()
        {
        }
    }

Предположим, нам нужно, чтобы действие `listAction()` было доступно и в формате XML. Вместо того, чтобы создавать новое действие, мы можем указать, что действие `listAction()` может возвращать и ответ в формате XML:

    class NewsController extends Zend_Controller_Action
    {
        public function init()
        {
            $contextSwitch = $this->_helper->getHelper('contextSwitch');
            $contextSwitch->addActionContext('list', 'xml')
                          ->initContext();
        }

        // ...
    }

Этим будет:

- Установлен заголовок ответа 'Content-Type' со значением 'text/xml'.

- Изменено значение суффикса вида на 'xml.phtml' (или 'xml.[your suffix]', если вы используете другой суффикс вида).

Теперь нужно создать новый скрипт вида - 'news/list.xml.phtml', в котором формируется код XML.

Для определения того, нужно ли переключение контекста, помощник проверяет метку в объекте запроса. По умолчанию он проверяет параметр 'format', но это поведение может быть изменено. Это значит, что в большинстве случаев для того, чтобы инициировать переключение контекста, достаточно добавить параметр 'format' в запрос одним из двух способов:

- Через параметр URL: `/news/list/format/xml` (напоминаем, используемый по умолчанию механизм маршрутизации разрешает добавление произвольных пар ключ/значение после имени действия в URL)

- Через параметр GET: `/news/list?format=xml`

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

## Доступные по умолчанию контексты

По умолчанию через помощник `ContextSwitch` используются два контекста: json и xml.

- JSON. Контекст JSON устанавливает заголовок ответа 'Content-Type' в значение 'application/json' и суффикс скрипта вида в значение 'json.phtml'.

  Но по умолчанию использование скриптов вида не обязательно. Контекст просто сериализует все переменные вида и сразу возвращает ответ в формате JSON.

  Это поведение может быть отменено путем отключения автоматической сериализации JSON:

      $this->_helper->contextSwitch()->setAutoJsonSerialization(false);

- XML. Контекст XML устанавливает заголовок ответа 'Content-Type' в значение 'text/xml' и суффикс скрипта вида в значение 'xml.phtml'. Для этого контекста нужно создавать скрипты вида.

## Создание своего контекста

Иногда доступных по умолчанию контекстов может быть недостаточно. Например, нужно возвращать данные в формате YAML, сериализованный PHP, ленты RSS, ATOM и т.д. `ContextSwitch` позволяет добавлять новые контексты.

Наиболее простым способом добавления нового контекста является использование метода `addContext()`. Этот метод принимает два аргумента - имя контекста и массив спецификации. Массив должен включать в себя один или более элементов из следующих:

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

- headers: массив пар заголовок/значение, которые требуется отправлять в ответе.

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

  Вызов функции инициализации производится сразу после того, как контекст определен помощником `ContextSwitch`. Вы можете использовать его для выполнения произвольной логики. Например, контекст JSON использует функцию обратного вызова для отключения ViewRenderer, если включена автоматическая сериализация JSON.

  Вызов функции пост-обработки производится во время операции `postDispatch()` и может использоваться для выполнения произвольной логики. Например, контекст JSON использует функцию обратного вызова для определения того, включена ли автоматическая сериализация JSON; если включена, то помощник сериализует переменные вида в JSON и отправляет ответ, иначе включается ViewRenderer.

Для взаимодействия с контекстом есть несколько методов:

- `addContext($context, array $spec)`: добавляет новый контекст. Бросает исключение, если контекст уже существует.

- `setContext($context, array $spec)`: добавляет новый контекст или переопределяет существующий. Использует ту же спецификацию, что и `addContext()`.

- `addContexts(array $contexts)`: добавляет несколько контекстов одновременно. Массив `$contexts` должен содержать пары контекст/спецификация. Если какой-либо из контекстов уже существует, то бросается исключение.

- `setContexts(array $contexts)`: добавляет новые контексты и переопределяет существующие. Использует ту же спецификацию, что и `addContexts()`.

- `hasContext($context)`: если контекст с этим именем уже существует, то метод возвращает true, иначе false.

- `getContext($context)`: возвращает контекст по его имени в виде массива, следующего спецификации, используемой в `addContext()`.

- `getContexts()`: возвращает все контексты в виде массива пар контекст/спецификация.

- `removeContext($context)`: удаляет контекст по его имени. Возвращает true в случае успеха, false - если контекст не найден.

- `clearContexts()`: удаляет все контексты.

## Установка контекстов для действий

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

`addActionContext()` является основным методом для добавления связей действие/контекст. Он принимает два аргумента: действие, к которому добавляется контекст, и имя контекста (либо массив контекстов). Для примера рассмотрим следующий класс контроллера:

    class FooController extends Zend_Controller_Action
    {
        public function listAction()
        {
        }

        public function viewAction()
        {
        }

        public function commentsAction()
        {
        }

        public function updateAction()
        {
        }
    }

Предположим, что мы хотим добавить контекст XML к действию 'list', а к действию 'comments' - контексты XML и JSON. В этом случае мы можем добавить следующий код в метод `init()`:

    class FooController extends Zend_Controller_Action
    {
        public function init()
        {
            $this->_helper->contextSwitch()
                 ->addActionContext('list', 'xml')
                 ->addActionContext('comments', array('xml', 'json'))
                 ->initContext();
        }
    }

Мы можем также просто определить свойство `$contexts` в контроллере:

    class FooController extends Zend_Controller_Action
    {
        public $contexts = array(
            'list'     => array('xml'),
            'comments' => array('xml', 'json')
        );

        public function init()
        {
            $this->_helper->contextSwitch()->initContext();
        }
    }

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

Следующие методы могут использоваться для построения связей действие/контекст:

- `addActionContext($action, $context)`: помечает один или более контекстов как доступные для действия $action. Если связи уже существуют, то производится добавление к ним. `$context` может быть как одним контекстом, так и массивом контекстов.

  Если `$context` имеет значение `TRUE`, то все доступные в `ContextSwitch` контексты помечаются как доступные для действия.

  Пустое значение аргумента `$context` отключит все контексты для данного действия.

- `setActionContext($action, $context)`: помечает один или более контекстов как доступные для действия $action. Если связи уже существуют, то метод заменяет их указанными. `$context` может быть как одним контекстом, так и массивом контекстов.

- `addActionContexts(array $contexts)`: добавляет одновременно несколько пар действие/контекст. `$contexts` должен быть ассоциативным массивом пар действие/контекст. Использует метод `addActionContext()`, это означает, что если связи для данного действия уже существуют, то указанные в `$contexts` связи добавляются к ним.

- `setActionContexts(array $contexts)`: действует аналогично `addActionContexts()`, но переопределяет пары действие/контекст, если они уже существуют.

- `hasActionContext($action, $context)`: используется для определения того, имеет ли действие $action данный контекст.

- `getActionContexts($action = null)`: возвращает все контексты для действия $action или все пары действие/контекст.

- `removeActionContext($action, $context)`: удаляет один или более контекстов из действия $action. `$context` может быть как одним контекстом, так и массивом контекстов.

- `clearActionContexts($action = null)`: удаляет все контексты из действия $action или из всех действий с контекстами.

## Инициализация переключения контекста

Для того, чтобы инициализировать переключение контекста, необходимо вызвать метод `initContext()` в контроллере действий:

    class NewsController extends Zend_Controller_Action
    {
        public function init()
        {
            $this->_helper->contextSwitch()->initContext();
        }
    }

В некоторых случаях может потребоваться принудительное использование контекста - например, использовать только контекст XML, если переключение контекста включено. Вы можете осуществить это передачей контекста методу `initContext()`:

    $contextSwitch->initContext('xml');

## Дополнительный функционал

Для управления помощником `ContextSwitch` могут использоваться различные методы. Эти методы включают в себя:

- `setAutoJsonSerialization($flag)`: По умолчанию контекст JSON будет сериализовать все переменные вида согласно нотации JSON и возвращать их в качестве ответа. Если вы хотите самостоятельно формировать ответ, то должны отключить автоматическую сериализацию через данный метод; это нужно делать до вызова `initContext()`.

      $contextSwitch->setAutoJsonSerialization(false);
      $contextSwitch->initContext();

  Вы можете получить значение этого флага через метод `getAutoJsonSerialization()`.

- `setSuffix($context, $suffix, $prependViewRendererSuffix)`: Используя этот метод, вы можете задать другой суффикс для использования в контексте $context. Третий аргумент используется для указания того, добавлять ли или нет суффикс из помощника ViewRenderer к новому суффиксу; этот флаг установлен по умолчанию.

  Передача пустого значения в качестве суффикса приведет к тому, что будет использоваться только суффикс из ViewRenderer.

- `addHeader($context, $header, $content)`: Добавляет заголовок ответа для контекста $context. `$header` является именем заголовка, а `$content` - значением для этого заголовка.

  Любой контекст может иметь несколько заголовков. `addHeader()` добавляет дополнительные заголовки в стек заголовков для данного контекста.

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

- `setHeader($context, $header, $content)`: `setHeader()` действует аналогично `addHeader()` за исключением того, что позволяет переопределять существующие для данного контекста заголовки.

- `addHeaders($context, array $headers)`: Добавляет несколько заголовков к контексту $context. Использует `addHeader()`, поэтому если заголовок уже существует, то бросается исключение. `$headers` является массивом пар заголовок/значение.

- `setHeaders($context, array $headers.)`: действует аналогично `addHeaders()` за исключением того, что использует `setHeader()`, что позволяет перезаписывать существующие заголовки.

- `getHeader($context, $header)`: возвращает значение заголовка для данного контекста. Если заголовок не найден, то возвращается null.

- `removeHeader($context, $header)`: удаляет один заголовок для данного контекста.

- `clearHeaders($context)`: удаляет все заголовки для данного контекста.

- `setCallback($context, $trigger, $callback)`: устанавливает функцию обратного вызова для триггера $trigger и контекста $context. Триггерами могут быть 'init' или 'post' (выбор триггера определяет, когда будет вызвана функция - во время инициализации контекста или операции `postDispatch()`). `$callback` должен быть действующей PHP-функцией обратного вызова.

- `setCallbacks($context, array $callbacks)`: устанавливает несколько функций обратного вызова для данного контекста. `$callbacks` должен быть массивом пар триггер/функция обратного вызова. В действительности можно зарегистрировать максимум две функции обратного вызова, одна для инициализации, другая для пост-обработки.

- `getCallback($context, $trigger)`: возвращает функцию обратного вызова для триггера $trigger и контекста $context.

- `getCallbacks($context)`: возвращает все функции обратного вызова в виде массива пар триггер/функция обратного вызова.

- `removeCallback($context, $trigger)`: удаляет функцию обратного вызова для триггера $trigger и контекста $context.

- `clearCallbacks($context)`: удаляет все функции обратного вызова для данного контекста.

- `setContextParam($name)`: устанавливает параметр запроса, используемый для переключения контекста. По умолчанию установлено значение 'format', но его можно изменить с помощью данного аксессора.

  `getContextParam()` может использоваться для получения текущего значения.

- `setAutoDisableLayout($flag)`: По умолчанию макеты отключаются, когда производится переключение контекста. Это потому, что макеты, как правило, используются только с обычным выводом и их использование не имеет смысла для альтернативного контекста. Тем не менее, если вы хотите использовать макеты, то можете изменить это поведение, передав значение `FALSE` методу `setAutoDisableLayout()`. Вы должны делать это до вызова `initContext()`.

  Для получения значения этого флага используйте аксессор `getAutoDisableLayout()`.

- `getCurrentContext()` может использоваться для определения того, какой контекст был установлен. Возвращает null, если не было переключения контекста, или метод был вызван до `initContext()`.

## Функционал AjaxContext

Помощник `AjaxContext` наследует от `ContextSwitch`, поэтому весь функционал, описанный для `ContextSwitch`, доступен и для него. Но есть несколько ключевых отличий.

Во-первых, он использует другое свойство контроллера для определения контекстов, а именно `$ajaxable`. Поэтому вы можете одновременно использовать различные контексты для AJAX и обычных HTTP-запросов. Все методы `*ActionContext*()` помощника `AjaxContext` будут записывать в это свойство.

Во-вторых, он будет запускаться только тогда, когда производится запрос XmlHttpRequest. Для определения того, был ли произведен запрос XmlHttpRequest, используется метод `isXmlHttpRequest()` объекта запроса. Таким образом, если параметр контекста ('format') был передан в запросе, но сам запрос был произведен не через XmlHttpRequest, то не будет произведено переключение контекста.

И в-третьих, `AjaxContext` добавляет дополнительный контекст - HTML. В этом контексте он устанавливает суффикс в значение 'ajax.phtml' для отделения этого контекста от обычных запросов. Дополнительные заголовки не возвращаются.

### Установка действий для ответов на AJAX-запросы

В следующем примере мы указываем, что действия 'view', 'form' и 'process' могут отвечать на AJAX-запросы. В действиях 'view' и 'form' мы будем возвращать куски HTML-кода для обновления страницы, в действии 'process' - данные в формате JSON.

    class CommentController extends Zend_Controller_Action
    {
        public function init()
        {
            $ajaxContext = $this->_helper->getHelper('AjaxContext');
            $ajaxContext->addActionContext('view', 'html')
                        ->addActionContext('form', 'html')
                        ->addActionContext('process', 'json')
                        ->initContext();
        }

        public function viewAction()
        {
            // Извлекает один комментарий для просмотра
            // Если определен контекст AjaxContext, то используется
            // скрипт вида comment/view.ajax.phtml
        }

        public function formAction()
        {
            // Рендерит форму для добавления нового комментария
            // Если определен контекст AjaxContext, то используется
            // скрипт вида comment/form.ajax.phtml
        }

        public function processAction()
        {
            // Обрабатывает новый комментарий
            // Возвращает результат в формате JSON. Просто присвойте значения
            // переменным вида и эти значения будет возвращены в формате JSON.
        }
    }

На клиентской стороне посредством AJAX-библиотеки производятся запросы к конечным точкам '/comment/view', '/comment/form', '/comment/process' и передается параметр 'format': '/comment/view/format/html', '/comment/form/format/html', '/comment/process/format/json' (можно также передавать параметр через строку запроса, например: "?format=json").

Если ваша AJAX-библиотека передает заголовок 'X-Requested-With: XmlHttpRequest', то эти действия будут возвращать ответ в требуемом формате.