Контроллер

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

Tip

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

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

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

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

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

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

        return new Response(
            '<html><body>Lucky number: '.$number.'</body></html>'
        );
    }
}

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

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

  • Строчка 2: Symfony использует преимущества пространства имён PHP, чтобы указать пространство имён для класса контроллера.
  • Строчка 4: Symfony снова использует преимущества пространства имён PHP: ключевое слово use импортирует класс Response, который должен вернуть контроллер.
  • Строчка 7: Технически, класс можно назвать как угодно, но по соглашению, он должен иметь суффикс Controller.
  • Строчка 12: Методу действия разрешено иметь аргумент $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
10
// src/Controller/LuckyController.php
namespace App\Controller;

+ use Symfony\Bundle\FrameworkBundle\Controller\Controller;

- class LuckyController
+ class LuckyController extends Controller
{
    // ...
}

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

Tip

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

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

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

1
$url = $this->generateUrl('app_lucky_number', array('max' => 10));

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

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

redirectToRoute() и redirect():

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

// ...
public function index()
{
    // пенеправить по маршруту "homepage"
    return $this->redirectToRoute('homepage');

    // redirectToRoute - это ярлык для:
    // вернуть новый RedirectResponse($this->generateUrl('homepage'));

    // совершить постоянное перенаправление - 301
    return $this->redirectToRoute('homepage', array(), 301);

    // перенаправить по маршруту с параметрами
    return $this->redirectToRoute('app_lucky_number', array('max' => 10));

    // перенаправляет внутренне
    return $this->redirect('http://symfony.com/doc');
}

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

Caution

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

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

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

1
2
// отображает templates/lucky/number.html.twig
return $this->render('lucky/number.html.twig', array('name' => $name));

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

Получение сервисов

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

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

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

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

Отлично!

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

1
$ php bin/console debug:autowiring

Если вам необходим контроль над точным значением аргумента, вы можете cвязать аргумент с его именем:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # config/services.yaml
    services:
        # ...
    
        # ясно сконфигурировать сервис
        App\Controller\LuckyController:
            public: true
            bind:
                # for any $logger argument, pass this specific service
                $logger: '@monolog.logger.doctrine'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- 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="App\Controller\LuckyController" public="true">
                <bind key="$logger"
                    type="service"
                    id="monolog.logger.doctrine"
                />
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // config/services.php
    use App\Controller\LuckyController;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->register(LuckyController::class)
        ->setPublic(true)
        ->setBindings(array(
            '$logger' => new Reference('monolog.logger.doctrine'),
        ))
    ;
    

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

Caution

Вы можете передавать сервисы вашим аргументам контроллера только этим способом. Невозможно, к примеру, передать параметр сервиса в качестве аргумента контроллера, даже используя bind. Если вам нужен параметр, используйте сокращение $this->getParameter('kernel.debug') или передайте значение через метод вашего контроллера __construct() и укажите его значение с bind.

Чтобы узнать больше о сервисах, см. статью Service Container.

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

// ...
public function index()
{
    // извлечь объект из БД
    $product = ...;
    if (!$product) {
        throw $this->createNotFoundException('The product does not exist');

        // вышенаписанное - просто сокращение для:
        // вызвать новый NotFoundHttpException('Продукт не существует');
    }

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

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

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

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

В обоих случаях, конечному пользователю отображается страница ошибки, а разработчику отображается полная страница отладки ошибки (например, когда вы в режиме "отладки" - см. The parameters Key: Parameters (Variables)).

Чтобы настроить страницу ошибки, отображаемую пользователю, см. статью How to Customize Error Pages.

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

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

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

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

    // ...
}

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

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

Symfony предоставляет сервис сессии, который вы можете использовать для хранения информации о пользователе между запросами. Хранение сессии и другую конфигурацию можно контролировать в конфигурации framework.session.

Для начала, активируйте сессию, убрав комментарий из ключа session в config/packages/framework.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# config/packages/framework.yaml
framework:
    # ...

-     #сессия:
-     #    # Будет использован родной PHP обработчик сессий
-     #    handler_id: ~
+     сессия:
+         # Будет использован родной PHP обработчик сессий
+         handler_id: ~
    # ...

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

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

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

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

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

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

Tip

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

Чтобы узнать больше, см. Sessions.

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 update(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
    {# templates/base.html.twig #}
    
    {# вы можете прочитать и отобразить только один тип флеш-сообщений... #}
    {% for message in app.flashes('notice') %}
        <div class="flash-notice">
            {{ message }}
        </div>
    {% endfor %}
    
    {# ...или вы можете прочитатьи отобразить все доступные флеш-сообщения #}
    {% 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
    <!-- templates/base.html.php -->
    
    // вы можете прочитать и отобразить только один тип флеш-сообщений...
    <?php foreach ($view['session']->getFlashBag()->get('notice') as $message): ?>
        <div class="flash-notice">
            <?php echo $message ?>
        </div>
    <?php endforeach ?>
    
    // ...или вы можете прочитатьи отобразить все доступные флеш-сообщения
    <?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 ?>
    

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

Tip

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

Объекты Request и Response

Как упоминалось ранее, Symfony передаст объект 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 index(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:

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');

Существуют специальные классы, которые облегчают некоторые виды ответов. Некоторые из них описаны ниже. Чтобы узнать больше о Request и Response (и специальных классах Response), см. документацию компонента HttpFoundation.

Возвращение JSON-ответов

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

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

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

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

Потоковая передача ответов файла

Вы можете использовать помощник 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.