Контроллер

Контроллер - это PHP-функция, которую вы создаёте, чтобы получить информацию из HTTP запроса и на её основе создать HTTP ответ в виде объекта Response. Ответ может быть HTML страницей, XML-документом, сериализованным JSON-массивом, изображением, перенаправлением, ошибкой 404, всем чем угодно, о чём вы только могли мечтать. Контроллер содержит любую логику вашего приложения, необходимую для того, чтобы отобразить содержимое страницы.

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

<?php

use Symfony\Component\HttpFoundation\Response;

public function helloAction()
{
    return new Response('Hello world!');
}

Цель у контроллера всегда одна: создать и вернуть объект Response. Следуя этой цели, контроллер может читать информацию из запроса, загружать ресурсы из базы данных, отправлять email или же записывать информациюю в сессию пользователя. Но всегда, в конечном итоге, контроллер вернёт объект Response, который будет отправлен клиенту.

Здесь нет никакой магии или других требований, о которых стоило бы беспокоиться! Вот несколько типичных примеров:

  • Контроллер A создаёт объект Response, содержащий контент для главной страницы сайта.
  • Контроллер B получает из запроса параметр slug для того, чтобы загрузить запись блога из базы данных и создать объект Response, отображающий этот блог. Если указанный slug не может быть найден в базе, он создаёт и возвращает объект Response со статус-кодом 404 (не найдено).
  • Контроллер C обрабатывает отправленную форму контактов. Он читает информацию о форме из запроса, сохраняет контактную информацию в базу данных и отправляет сообщение вебмастеру. Наконец, он создаёт объект Response, который перенаправляет браузер клиента на страницу “thank you”.

Жизненный цикл Запрос-Контроллер-Ответ

Каждый запрос, обрабатываемый проектом Symfony2, следует одному и тому же простому жизненному циклу. Фреймворк берёт на себя повторяющиеся задачи и, в конце концов выполняет контроллер, который содержит код вашего приложения:

  1. Каждый запрос обрабатывается одним фронт-контроллером (например app.php или app_dev.php), который загружает приложение;
  2. Router читает информацию из запроса (URI к примеру), ищет подходящий маршрут и получает параметр _controller из маршрута;
  3. Контроллер, соответствующий маршруту, выполняется и его код формирует объект Response;
  4. HTTP-заголовки и контент объекта Response отправляются обратно клиенту, отправившему изначальный запрос.

Создание страницы - это по сути создание контроллера (#3) и маршрута, который ставит в соответствие контроллеру некий URL (#2).

Note

Не смотря на то что “фронт-контроллер” и “контроллер” названы похожим образом, они сильно различаются - об этом мы еще поговорим чуть позже в этой главе. Фронт-контроллер - это короткий PHP-файл, который находится в web-директории и который обрабатывает все входящие запросы. Типичное приложение имеет продуктовый контроллер (prod, как правило app.php) и контроллер для разработки (dev, как правило app_dev.php). И вам скорее всего никогда не придется модифицировать или вообще задумываться о фронт-контроллерах в вашем приложении.

Простой контроллер

В то время как контроллер может быть любой PHP-сущностью, которую можно вызвать (функцией, методом объекта, или же замыканием (Closure)), в Symfony2 контроллер - это как правило некий метод объекта контроллера. Контроллеры также называются действиями (actions).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php

// src/Acme/HelloBundle/Controller/HelloController.php

namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction($name)
    {
      return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

Tip

Обратите внимание, что контроллер - это метод indexAction, который расположен внутри класса контроллера (HelloController). Смотрите не путайтесь: класс контроллера - это просто удобный способ сгруппировать несколько контроллеров/действий вместе. Обычно класс контроллера содержит несколько контроллеров/действий (например updateAction, deleteAction и т.д.).

Этом контроллере нет ничего сложного, но давайте разберём подробнее:

  • строка 5: Symfony2 использует преимущества пространств имён PHP 5.3. Ключевое слово use импортирует класс Response, который контроллер должен вернуть.
  • строка 8: Имя класса это результат объединения имени контроллера (Hello) и слова Controller. Это очередная договорённость, которая позволяет обеспечить единообразие в именовании контроллеров и позволяет ссылаться на класс только по первой части наименования (здесь это будет Hello) в конфигурации маршрутизатора.
  • line 10: Каждое действие в классе контроллера имеет суффикс Action и упоминается в настройках маршрутизатора только по имени (index). В следующей секции вы создадите маршрут, который привяжет URI к этому действию. Вы узнаете как заполнитель для имени в маршруте - {name} - станет аргументом метода действия ($name).
  • line 12: Контроллер создаёт и возвращает объект Response.

Соответствие URL Контроллеру

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

Теперь при запросе URI /hello/ryan теперь выполняется контроллер HelloController::indexAction() и присваивает переменной $name значение ryan. Создание страницы по сути подразумевает всего лишь создание метода контроллера и соответствующего маршрута.

Обратите внимание на синтаксис, при помощи которого маршрут ссылается на контроллер: AcmeHelloBundle:Hello:index. Symfony2 использует простую строковую нотацию для создания ссылок на различные контроллеры. Этот очень простой синтаксис сообщает Symfony2 что класс контроллера с именем HelloController расположен в пакете AcmeHelloBundle. Затем выполняется метод indexAction().

Более подробно о формате строк, используемых для создания ссылок на различные контроллеры можно почитать здесь: Шаблон Именования Контроллера.

Note

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

Tip

Подробно вопросы маршрутизации рассматриваются в главе Маршрутизация.

Параметры маршрута в качестве аргументов Контроллера

Вы уже знаете, что параметр _controller со значением AcmeHelloBundle:Hello:index ссылается на метод HelloController::indexAction(), который расположен в пакете AcmeHelloBundle. Также интерес представляют аргументы, которые передаются в этот метод:

<?php
// src/Acme/HelloBundle/Controller/HelloController.php

namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    public function indexAction($name)
    {
      // ...
    }
}

Контроллер имеет единственный аргумент - $name, который соответствует параметру {name} из маршрута (в нашем примере - ryan). Фактически, когда контроллер выполняется, Symfony2 каждому аргументу контроллера ставит в соответствие параметр из маршрута. Взгляните на пример:

Контроллер для этого примера принимает несколько аргументов:

<?php
public function indexAction($first_name, $last_name, $color)
{
    // ...
}

Обратите внимание, что оба заполнителя для переменных ({first_name}, {last_name}), как и переменная по умолчанию color - доступны в качестве аргументов в контроллере. Когда совпадает маршрут, заполнители переменных объединяются с defaults в один массив, который становится доступен в вашем контроллере.

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

  • Порядок аргументов контроллера не имеет значения

    Symfony в состоянии установить соответствие между именами параметров маршрута и сигнатурой метода в контроллере. Другими словами, это работает таким образом, что параметр {last_name} соответствует аргументу $last_name. Аргументы контроллера менять местами и он всё равно будет работать:

    <?php
    public function indexAction($last_name, $color, $first_name)
    {
        // ..
    }
    
  • Каждый обязательный аргумент контроллера должен соответствовать параметру маршрута

    Следующий пример вызовет исключение RuntimeException, так как в маршруте не определён параметр foo:

    <?php
    public function indexAction($first_name, $last_name, $color, $foo)
    {
        // ..
    }
    

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

    <?php
    public function indexAction($first_name, $last_name, $color, $foo = 'bar')
    {
        // ..
    }
    
  • Параметры маршрута не обязательно должны быть представлены в виде аргументов контроллера

    Если, к примеру, параметр last_name не нужен в контроллере, его можно опустить:

    <?php
    public function indexAction($first_name, $color)
    {
        // ..
    }
    

Tip

Каждый маршрут имеет специализировнный параметр _route, который содержит значение равное его имени (например hello). Обычно это значение не используется, но, тем не менее, этот параметр также доступен в качестве аргумента контроллера.

Request как аргумент Контроллера

Для большего удобства, вы также можете передать объект Request в качестве аргумента в ваш контроллер. Это особенно удобно при работе с формами:

<?php
use Symfony\Component\HttpFoundation\Request;

public function updateAction(Request $request)
{
    $form = $this->createForm(...);

    $form->bindRequest($request);
    // ...
}

Базовый класс контроллера

Symfony2 включает базовый класс Controller, который оказывает помощь в выполнении наиболее типичных задач контроллера и предоставляет вашему контроллеру доступ к любому ресурсу, который может портребоваться. Осуществляя наследование от класса Controller вы получите в своё распоряжение некоторое число методов-помощников.

Добавьте выражение use в начале класса контроллера и модифицируйте HelloController, чтобы он наследовался от Controller:

<?php
// src/Acme/HelloBundle/Controller/HelloController.php

namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class HelloController extends Controller
{
    public function indexAction($name)
    {
      return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

Эти изменения на самом деле ничего не меняют в логике работы вашего контроллера. В следующей секции вы узнаете о тех методах-помощниках, которые предоставляет базовый класс. Эти методы по сути являются обёртками для базового функционала Symfony2 который доступен вам в любом случае - с использованием базового класса Controller или же без него. Самый лучший путь для того чтобы увидеть базовые функции в действии - заглянуть в код класса Symfony\Bundle\FrameworkBundle\Controller\Controller самостоятельно.

Tip

Наследование от базового класса совершенно не обязательно в Symfony2. Этот касс содержит удобные методы-ярлыки, но ничего обязательного. Вы также можете отнаследоваться от класса Symfony\Component\DependencyInjection\ContainerAware. Объект service container’а будет доступен черз свойство container.

Note

Вы также можете объявить контроллер в качестве сервиса: </cookbook/controller/service>.

Контроллер, Базовые операции

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

Перенаправление (redirecting)

Если вы хотите перенаправить пользователя на другую страницу, используйте метод redirect():

<?php
public function indexAction()
{
    return $this->redirect($this->generateUrl('homepage'));
}

Метод generateUrl(), это всего-лишь функция помощник, которая генерирует URL для заданного маршрута. Более подробно этот вопрос рассматривается в главе Маршрутизация.

По умолчанию, метод redirect() выполняет перенаправление с HTTP статус-кодом 302 (временное перенаправление). Для того, чтобы выполнить постоянное перенаправление (со статус-кодом 301), необходимо добавить второй аргумент:

<?php
public function indexAction()
{
    return $this->redirect($this->generateUrl('homepage'), 301);
}

Tip

Метод redirect() - это просто ярлычок для операции создания объекта Response, который специализируется на перенаправлении пользователя. Он эквивалентен следующему коду:

<?php
use Symfony\Component\HttpFoundation\RedirectResponse;

return new RedirectResponse($this->generateUrl('homepage'));

Контроллер, Переадресация (forwarding)

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

<?php
public function indexAction($name)
{
    $response = $this->forward('AcmeHelloBundle:Hello:fancy', array(
        'name'  => $name,
        'color' => 'green'
    ));

    // Здесь можно модифицировать $response или же сразу вернуть его пользователю

    return $response;
}

Обратите внимание, что метод forward() использует для указания контроллера тот же формат строки, который используется в конфигурации маршрутов. Таким образом, целью переадресации будет HelloController из пакета AcmeHelloBundle. Массив, передаваемый методу в качестве параметра, будет конвертирован в параметры целевого контроллера. Такой же интерфейс используется при встраивании контроллеров в шаблоны (см. Внедрение контроллеров). Метод целевого контроллера должен выглядеть следующим образом:

<?php
public function fancyAction($name, $color)
{
    // ... create and return a Response object
}

И, как и в случае создания контроллера для маршрута, порядок аргументов для fancyAction не имеет значения. Symfony2 устанавливает соответствие по именам ключей (например name) и именам параметров (например $name). Если вы изменяете порядок следования аргументов, Symfony2 также будет присваивать верные значения каждой переменной.

Tip

Как и прочие методы базового контроллера, метод forward - это просто ярлык к базовому функционалу Symfony2. Переадресация может быть выполнена напрямую через сервис http_kernel. При переадресации возвращается объект Response:

<?php
$httpKernel = $this->container->get('http_kernel');
$response = $httpKernel->forward('AcmeHelloBundle:Hello:fancy', array(
    'name'  => $name,
    'color' => 'green',
));

Рендеринг Шаблонов

Хотя это и не является требованием, большинство контроллеров в конце концов будут отображать (рендерить) шаблон, который отвечает за генерацию HTML (или данных в другом формате) для контроллера. Метод renderView() рендерит шаблон и возвращает его содержимое. Контент из шаблона может быть использован для создания объекта Response:

<?php
$content = $this->renderView('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name));

return new Response($content);

Эти операции могут быть выполнены за один шаг при помощи метода render(), который возвращает объект Response, содержащий контент шаблона:

В обоих случаях, будет отображен шаблон Resources/views/Hello/index.html.twig из пакета AcmeHelloBundle.

Шаблонизатор Symfony более подробно рассматривается в главе о Шаблонах

Tip

Метод renderView - это по сути ярлык для быстрого использования шаблонизатора. Шаблонизатор также можно использовать напрямую:

<?php
$templating = $this->get('templating');
$content = $templating->render('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name));

Доступ к сервисам

При наследовании от базового контроллера, вы можете получить доступ к любому сервису Symfony2 при помощи метода get(). Ниже представлены основные сервисы, которые вам могут быть полезны:

$request = $this->getRequest();

$templating = $this->get('templating');

$router = $this->get('router');

$mailer = $this->get('mailer');

В Symfony2 по умолчанию определена куча сервисов и вы вольны определить ещё столько же собственных. Для того чтобы отобразить список доступных сервисов, используйте консольную команду container:debug:

php app/console container:debug

Больше данных о сервисах вы можете почерпнуть из главы Service container.

Разбираемся с ошибками и 404 страница

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

<?php
public function indexAction()
{
    $product = // тут получаем объект из базы данных
    if (!$product) {
        throw $this->createNotFoundException('Продукт не существует');
    }

    return $this->render(...);
}

Метод createNotFoundException() создаёт особый объект NotFoundHttpException, который в конечном итоге провоцирует возврат HTTP 404 внутри Symfony.

Конечно, вы вольны вызывать любую исключительную ситуацию в вашем контроллере - Symfony2 автоматически вернёт HTTP статус-код 500.

throw new \Exception('Что-то пошло не так!');

В любом случае, пользователь увидит страницу с той или иной ошибкой, а разработчику (при использовании dev-окружения) будет показана страница с полной отладочной информацией. Эти страницы ошибок могут быть изменены. Более подробно об этом написано в “книге рецептов”: “/cookbook/controller/error_pages”.

Работа с Сессиями

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

Сохранение и получение информации из сессии можно использовать из любого контроллера:

$session = $this->getRequest()->getSession();

// store an attribute for reuse during a later user request
$session->set('foo', 'bar');

// in another controller for another request
$foo = $session->get('foo');

// set the user locale
$session->setLocale('fr');

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

Flash-сообщения

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

Например, представьте, что вы обрабатываете отправку формы:

<?php
public function updateAction()
{
    $form = $this->createForm(...);

    $form->bindRequest($this->getRequest());
    if ($form->isValid()) {
        // do some sort of processing

        $this->get('session')->setFlash('notice', 'Your changes were saved!');

        return $this->redirect($this->generateUrl(...));
    }

    return $this->render(...);
}

После обработки запроса контроллер устанавливает flash-сообщение notice и выполняет перенаправление. Имя (notice) не устанавливается жёстко - это лишь обозначение типа сообщения.

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

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

Объект Ответа

К контроллеру предъявляется лишь одно требование - вернуть объект Response. Класс Symfony\Component\HttpFoundation\Response представляет собой PHP-абстракцию HTTP-ответа - текстового сообщения, состоящего из HTTP-заголовков и контента, который возвращается клиенту:

// создаётся простой объект Response со статус-кодом 200 (по умолчанию)
$response = new Response('Hello '.$name, 200);

// создаётся JSON-ответ со статус-кодом 2000
$response = new Response(json_encode(array('name' => $name)));
$response->headers->set('Content-Type', 'application/json');

Tip

headers - это объект Symfony\Component\HttpFoundation\HeaderBag, содержащий методы для чтения и изменения заголовков ответа Response. Имена заголовков нормализованы, так что Content-Type, content-type и даже content_type эквивалентны.

Объект запроса

Помимо значений заполнителей из маршрута, контроллер также имеет доступ к объекту Request, когда он является наследником базового класса Controller:

$request = $this->getRequest();

$request->isXmlHttpRequest(); // is it an Ajax request?

$request->getPreferredLanguage(array('en', 'fr'));

$request->query->get('page'); // get a $_GET parameter

$request->request->get('page'); // get a $_POST parameter

Подобно объекту Response, заголовки запроса хранятся в объекте HeaderBag и также легко доступны.

Заключение

Когда вы создаёте страницу, в конечном итоге должны написать код, который содержит логику этой страницы. В Symfony эта логика называется “контроллером”, и представляет собой PHP-функцию, которая выполняет все необходимые действия для того чтобы вернуть объект Response, который будет отправлен пользователю.

Для того, чтобы сделать жизнь легче, вы можете отнаследоваться от класса Controller, который содержит методы для типичных задач, решаемых контроллером. Например, так как вы должны вернуть HTML код - вы можете использовать метод render() и вернуть контент шаблона.

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

Дополнительно в книге рецептов:

  • /cookbook/controller/error_pages
  • /cookbook/controller/service