Контроллер

Контроллер

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

Давайте убедимся, насколько всё просто, посмотрев на контроллер Symfony в действии. Нижеследующий контроллер отображает счастливое (случайное) число:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Controller/LuckyController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

class LuckyController
{
    /**
     * @Route("/lucky/number")
     */
    public function numberAction()
    {
        $number = mt_rand(0, 100);

        return new Response(
            '<html><body>Счастливое число: '.$number.'</body></html>'
        );
    }
}

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

Tip

Если вы еще не создали свою первую рабочую страницу, просмотрте главу создание страницы и потом возвращайтесь!

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Controller/LuckyController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class LuckyController
{
    /**
     * @Route("/lucky/number/{max}")
     */
    public function numberAction($max)
    {
        $number = mt_rand(0, $max);

        return new Response(
            '<html><body>Счастливое число: '.$number.'</body></html>'
        );
    }
}

Контроллер - это метод numberAction(), который расположен внутри класса контроллера LuckyController.

Этот контроллер достаточно прямолинеен:

  • Строка 2: Symfony использует преимущества пространства имён PHP, чтобы указать пространство имён для класса контроллера.
  • Строка 4: Symfony снова использует преимущества пространства имён PHP: ключевое слово use импортирует класс Response, который должен вернуть контроллер.
  • Строка 7: Технически, класс можно назвать как угодно, но его название должно заканчиваться словом Controller (это не является обязательным, но другие могут полагаться на это).
  • Строка 12: Каждый метод действия в классе контроллера имеет суффикс Action (опять-таки, это не обязательно, но некоторые полагаются на это). Этот метод может иметь аргумент $max благодаря {max}шаблону подстановки в маршруте.
  • Строка 16: Контроллер создает и возвращает объект Response.

Связывание URL с контроллером

Для того, чтобы увидеть результат этого контроллера, вам понадобится привязать URL к нему с помощью маршрута. Это было сделано выше с помощью аннотации @Route("/lucky/number/{max}").

Чтобы увидеть вашу страницу, перейдите на этот URL в вашем браузере:

Для того, чтобы узнать больше о маршрутизации, см. главу Маршрутизация.

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

Для удобства Symfony включает в себя два опциональных базовых класса Controller и AbstractController. Вы можете наследовать любой из них, чтобы получить доступ к различным методам-помощникам.

Добавьте выражение use сверху класса Controller и потом измените LuckyController, чтобы наследовать его:

1
2
3
4
5
6
7
8
9
// src/AppBundle/Controller/LuckyController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class LuckyController extends Controller
{
    // ...
}

Вот и все! Теперь у вас есть доступ к таким методам как $this->render() и многим другим, о которых вы узнаете далее.

Tip

Вы можете наследовать либо Controller, либо AbstractController. Разница в том, что если вы наследуете AbstractController, вы не сможете получить доступ к сервисам напрямую через $this->get() или $this->container->get(). Это принудит вас написать более надежный код для доступа к сервисам. Но если вам нужен прямой доступ к контейнеру, вы можете использовать Controller.

New in version 3.3: Класс AbstractController был добавлен в Symfony 3.3.

Генерирование URL

Метод generateUrl() - это просто метод-помощник, который генерирует URL для заданного маршрута:

1
$url = $this->generateUrl('blog_show', array('slug' => 'slug-value'));

Перенаправление

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

redirectToRoute() и redirect():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public function indexAction()
{
    // редирект по маршруту "домашней страницы"
    return $this->redirectToRoute('homepage');

    // постоянный 301-й редирект
    return $this->redirectToRoute('homepage', array(), 301);

    // редирект по маршруту с параметрами
    return $this->redirectToRoute('blog_show', array('slug' => 'my-page'));

    // редирект по внешней ссылке
    return $this->redirect('http://symfony.com/doc');
}

Чтобы узнать больше, см. статью Маршрутизация.

Caution

Метод redirect() никак не проверяет свое место назначеня. Если вы перенаправляете по URL, предоставленному конечными пользователями, ваше приложение может быть открыто к уязвимости безопасности невалидированных редиректов.

Tip

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

1
2
3
4
5
6
use Symfony\Component\HttpFoundation\RedirectResponse;

public function indexAction()
{
    return new RedirectResponse($this->generateUrl('homepage'));
}

Отображение шаблонов

Если вы работате с HTML, вам пригодится умение отображать шаблоны. Метод render() отображает шаблон и помещает его содержимое в объект Response для вашего удобства:

1
2
// рендерит app/Resources/views/lucky/number.html.twig
return $this->render('lucky/number.html.twig', array('name' => $name));

Шаблоны также могут находиться в подкаталогах более глубокого уровня. Попытайтесь не создавать излишне глубокую структуру подкаталогов:

1
2
3
4
// рендерит app/Resources/views/lottery/lucky/number.html.twig
return $this->render('lottery/lucky/number.html.twig', array(
    'name' => $name,
));

Система шаблонов Symfony и Twig обяснены в статье Создание и использование шаблонов.

Сервисы получения в качестве аргументов контроллера

New in version 3.3: Возможность использовать указание типов в аргументе контроллера для получения сервиса была добавлена в Symfony 3.3.

Symfony по умолчанию наполнена большим количеством полезных объектов, называемых сервисами. Они используются для отображения шаблонов, отправки email'ов, запросов к базе данных и любой другой "работы", которую вы можете себе представить.

Если вам нужен сервис в контроллере, просто укажите класс или интерфейс аргумента. Symfony автоматически передаст вам необходимый сервис:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use Psr\Log\LoggerInterface
// ...

/**
 * @Route("/lucky/number/{max}")
 */
public function numberAction($max, LoggerInterface $logger)
{
    $logger->info('We are logging!');
    // ...
}

Отлично!

Какие еще сервисы можно подключить с помощью указания типа? Чтобы увидеть их, запустите консольную команду debug:container:

1
$ php bin/console debug:container --types

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    # app/config/services.yml
    services:
        # ...
    
        # ручная конфигурация сервиса
        AppBundle\Controller\LuckyController:
            public: true
            tags:
                # добавьте несколько тегов для контроля нескольких аргументов
                - name: controller.service_arguments
                  action: numberAction
                  argument: logger
                  # передайте необходимый идентификатор сервиса
                  id: monolog.logger.doctrine
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <!-- Ручная конфигурация сервиса -->
            <service id="AppBundle\Controller\LuckyController" public="true">
                <tag
                    name="controller.service_arguments"
                    action="numberAction"
                    argument="logger"
                    id="monolog.logger.doctrine"
                />
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/services.php
    use AppBundle\Controller\LuckyController;
    
    $container->register(LuckyController::class)
        ->setPublic(true)
        ->addTag('controller.service_arguments', [
            'action' => 'numberAction',
            'argument' => 'logger',
            'id' => 'monolog.logger.doctrine',
        ])
    ;
    

Конечно же вы можете использовать обычное внедрение через конструктор в ваших контроллерах.

Для того, чтобы узнать больше о сервисах, см. статью Cервис-контейнер.

Note

Если это не работает, убедитесь в том, что ваш контроллер зарегистрирован в качестве сревиса, является автоконфигурируемым и наследует либо класс Controller, либо AbstractController. Если вы используете настройки services.yml из Symfony Standard Edition, то ваши контроллеры уже зарегистрированы в качестве сервисов и сконфигурированы автоматически.

Если вы не используете конфигурацию по умолчанию, вы можете вручную тегировать ваш сервис с помощью controller.service_arguments.

Прямой доступ к контейнеру

Если вы наследуете базовый класс Controller, вы можете получить доступ к любому сервису Symfony с помощью метода get(). Вот некоторые общие сервисы, которые могут вам понадобиться:

1
2
3
4
5
6
7
8
$templating = $this->get('templating');

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

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

// вы также можете получать параметры
$someParameter = $this->getParameter('some_parameter');

Если вы получите ошибку в виде:

1
2
# Вы запросили несуществующий сервис "my_service_id"
You have requested a non-existent service "my_service_id"

Убедитесь в том, что сервис существует (используйте debug:container) и в том, что он публичный.

Управление ошибками и 404 страницами

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function indexAction()
{
    // получение объект из базы данных
    $product = ...;
    if (!$product) {
        throw $this->createNotFoundException('Продукт не найден');
    }

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

Метод createNotFoundException() - это лишь сокращение для создания специального объекта NotFoundHttpException, который в конечном счете запускает ответ 404 внутри Symfony.

Конечно же, вы можете использовать любой класс Exception в вашем контроллере - Symfony автоматически вернет HTTP-код ответа 500.

1
throw new \Exception('Something went wrong!');

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

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

Объект Request в качестве аргумента контроллера

Что вы будете делать, если вам понадобится узнать параметры запроса, заголовок запроса или получить доступ к загруженному файлу? Вся эта информация в Symfony содержится в объекте Request. Чтобы получить доступ к этой информации в контроллере, просто добавьте его в качестве аргумента и выполните типизирование по классу Request:

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request, $firstName, $lastName)
{
    $page = $request->query->get('page', 1);

    // ...
}

Продолжайте читать для более детальной информации об использовании объекта Request.

Управление сессиями

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

New in version 3.3: Возможность запрашивать экземпляр Session в контроллерах была добавлена в Symfony 3.3.

Чтобы получить доступ к сессии, добавьте типизирование SessionInterface к вашему аргументу и Symfony предоставит вам сессию:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use Symfony\Component\HttpFoundation\Session\SessionInterface;

public function indexAction(SessionInterface $session)
{
    // сохранить атрибут для повторного использования при следующем запросе пользователя
    $session->set('foo', 'bar');

    // получить атрибут, присвоенный другим контроллером во время другого запроса
    $foobar = $session->get('foobar');

    // использовать значение по умолчанию, если атрибута не существует
    $filters = $session->get('filters', array());
}

Сохраненные атрибуты будут соответствовать сессии пользователя до конца этой сессии.

Tip

Поддерживаются все реализации SessionInterface. Если у вас есть ваша собственная реализация, типизируйте ее в аргументах.

Flash-сообщения

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use Symfony\Component\HttpFoundation\Request;

public function updateAction(Request $request)
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        // выполните какую-то обработку

        $this->addFlash(
            'notice',
            'Изменения сохранены!'
        );
        // $this->addFlash() это эквивалент $request->getSession()->getFlashBag()->add()

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

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

После обработки запроса контроллер устанавливает flash-сообщение в сессии и затем выполняет редирект. Ключ к сообщению (notice в нашем примере) может быть любым: вы будете его использовать только для того, чтобы получить доступ к самому сообщению.

В шаблоне следующей страницы (или ещё лучше, в вашем базовом шаблоне макета страницы), прочитайте любое flash-сообщение из сессии используя app.flashes():

  • Twig
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    {# app/Resources/views/base.html.twig #}
    
    {# вы можете прочитать и отобразить только один тип flash-сообщения... #}
    {% for message in app.flashes('notice') %}
        <div class="flash-notice">
            {{ message }}
        </div>
    {% endfor %}
    
    {# ...или вы можете прочитать и отобразить каждое доступное flash-сообщение #}
    {% for label, messages in app.flashes %}
        {% for message in messages %}
            <div class="flash-{{ label }}">
                {{ message }}
            </div>
        {% endfor %}
    {% endfor %}
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- app/Resources/views/base.html.php -->
    
    // вы можете прочитать и отобразить только один тип flash-сообщения...
    <?php foreach ($view['session']->getFlashBag()->get('notice') as $message): ?>
        <div class="flash-notice">
            <?php echo $message ?>
        </div>
    <?php endforeach ?>
    
    // ...или вы можете прочитать и отобразить каждое доступное flash-сообщение
    <?php foreach ($view['session']->getFlashBag()->all() as $type => $flash_messages): ?>
        <?php foreach ($flash_messages as $flash_message): ?>
            <div class="flash-<?php echo $type ?>">
                <?php echo $message ?>
            </div>
        <?php endforeach ?>
    <?php endforeach ?>
    

New in version 3.3: Функция Twig app.flashes() была представлена в Symfony 3.3. Раньше вам нужно было использовать app.session.flashBag().

Note

Общепринятой практикой является использование ключей notice, warning и error в качестве разных типов flash-сообщений, но вы можете использовать любой ключ, который вам подходит.

Tip

В качестве альтернативы вы можете использовать метод peek() для того, чтобы получить сообщение, не удаляя его.

Объекты Request и Response

Как упоминалось ранее, фреймворк передаст объект Request любому аргументу контроллера, который будет типизирован по классу Request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $request->isXmlHttpRequest(); //  это запрос Ajax?

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

    // получить переменные GET и POST соответственно
    $request->query->get('page');
    $request->request->get('page');

    // получить переменные SERVER’а
    $request->server->get('HTTP_HOST');

    // получить объект UploadedFile по ключу foo
    $request->files->get('foo');

    // получить значение COOKIE
    $request->cookies->get('PHPSESSID');

    // получить заголовок запроса HTTP с нормализированными ключами строчными буквами
    $request->headers->get('host');
    $request->headers->get('content_type');
}

У класса Request есть несколько общедоступных свойств и методов, которые возвращают любую нужную вам информацию о запросе.

Как и у Request, у объекта Response также есть общедоступное свойство заголовков (headers). Речь идёт об объекте ResponseHeaderBag, который содержит методы для чтения и изменения заголовков ответов. Имена заголовков нормализованны таким образом, чтобы использование Content-Type было эквивалентно использованию content-type и даже content_type.

Единственное, что требуется от контроллера - это возвращать объект Response. Класс Response - это абстракция вокруг ответа HTTP - текстового сообщения, наполненного заголовками и контентом, который возвращается обратно клиенту:

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\Response;

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

// создать CSS-ответ со статус-кодом 200
$response = new Response('<style> ... </style>');
$response->headers->set('Content-Type', 'text/css');

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

Теперь, когда вы знаете основы, вы можете продолжить свое исследование объектов Symfony Request и Response в Документации компонента HttpFoundation.

Помощник JSON

Чтобы вернуть JSON из контроллера, используйте метод-помощник json() базового контроллера. Это возвращает специальный объект JsonResponse, который автоматически шифрует данные:

1
2
3
4
5
6
7
8
9
// ...
public function indexAction()
{
    // возвращает '{"username":"jane.doe"}' и устаналивает соответствующий заголовок Content-Type
    return $this->json(array('username' => 'jane.doe'));

    // сокращение определяет три необязательных аргумента
    // return $this->json($data, $status = 200, $headers = array(), $context = array());
}

Если в вашем приложении работает сериализатор, то содержимое передается в метод json() и им же шифруются. В противном случае используется функция json_encode.

Помощник File

New in version 3.2: Помощник ``file()` был добавлен в версии Symfony 3.2.

Вы можете использовать помощник file() для того, чтобы вызвать файл из контроллера:

1
2
3
4
5
public function fileAction()
{
    // отправить содержимое файла и заставить браузер загрузить его
    return $this->file('/path/to/some_file.pdf');
}

Помощник file() позволяет некоторым аргументам самим осуществить настройки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

public function fileAction()
{
    // загрузить файл из файловой системы
    $file = new File('/path/to/some_file.pdf');

    return $this->file($file);

    // переименовать загруженный файл
    return $this->file($file, 'custom_name.pdf');

    // отобразить содержимое файла в браузере вместо загрузки
    return $this->file('invoice_3241.pdf', 'my_invoice.pdf', ResponseHeaderBag::DISPOSITION_INLINE);
}

Заключение

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

Чтобы упростить себе жизнь, вы можете наследовать класс Controller, который содержит методы-сокращения (как render() и redirectToRoute()).

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

Продолжайте!

Далее, узнайте все об Отображении шаблонов с помощью Twig.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.