Контроллер

Дата обновления перевода: 2023-01-19*

Контроллер

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

Tip

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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(int $max): Response
    {
        $number = random_int(0, $max);

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

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

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

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

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

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

Чтобы увидеть вашу страницу, перейдите на этот URL в вашем браузере: http://localhost:8000/lucky/number/100

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

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

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

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

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

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

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

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

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

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

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

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

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

redirectToRoute() и redirect():

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
26
use Symfony\Component\HttpFoundation\RedirectResponse;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;

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

    // redirectToRoute - это сокращение для:
    // return new RedirectResponse($this->generateUrl('homepage'));

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

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

    // перенаправляет по пути и сохраняет изначальные параметры запроса
    return $this->redirectToRoute('blog_show', $request->query->all());

    // перенаправляет на внешний сайт
    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', ['number' => $number]);

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

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

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

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

1
2
3
4
5
6
7
8
9
10
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
// ...

#[Route('/lucky/number/{max}')]
public function number(int $max, LoggerInterface $logger): Response
{
    $logger->info('We are logging!');
    // ...
}

Отлично!

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

1
$ php bin/console debug:autowiring

Tip

Если вам нужен контроль над точным значением аргумента, или потребовать параметр, вы можете использовать атрибут #[Autowire]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;

class LuckyController extends AbstractController
{
    public function number(
        int $max,

        // внедрить конкретный сервис логгера
        #[Autowire(service: 'monolog.logger.request')]
        LoggerInterface $logger,

        // или внедрить значения параметра
        #[Autowire('%kernel.project_dir%')]
        string $projectDir
    ): Response
    {
        $logger->info('We are logging!');
        // ...
    }
}

Вы можете прочитать больше об этом атрибуте в .

6.1

Атрибут #[Autowire] был представлен в Symfony 6.1.

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

Чтобы узнать больше о сервисах, см. статью Сервис-контейнер.

Генерирование контроллеров

Для экономии времени, вы можете установить Symfony Maker и сказать Symfony сгенерировать новый класс контроллера:

1
2
3
4
$ php bin/console make:controller BrandNewController

created: src/Controller/BrandNewController.php
created: templates/brandnew/index.html.twig

Если вы хотите сгенеритьвать полный CRUD с привязкой к Doctrine entity, запускайте:

1
2
3
4
5
6
7
8
9
10
$ php bin/console make:crud Product

created: src/Controller/ProductController.php
created: src/Form/ProductType.php
created: templates/product/_delete_form.html.twig
created: templates/product/_form.html.twig
created: templates/product/edit.html.twig
created: templates/product/index.html.twig
created: templates/product/new.html.twig
created: templates/product/show.html.twig

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

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

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

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

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

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

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

Если вы вызовете исключение, расширяющее экземпляр HttpException, Symfony будет использовать соответствующий статус-код HTTP. Иначе ответ будет выдавать статус-код HTTP 500:

1
2
// это исключение сгенерирует ошибку с HTTP 500
throw new \Exception('Что-то пошло не так!');

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

Для настройки страницы ошибки, отображаемую пользователю, см. статью Как настроить страницы ошибок.

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

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

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

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

    // ...
}

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

Управление сессией

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

Хранение сессии и другую конфигурацию можно контролировать в конфигурации framework.session в config/packages/framework.yaml.

Для получения доступа к сессии добавьте аргумент и обозначьте его тип как SessionInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
// ...

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

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

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

    // ...
}

Сохранённые атрибуты остаются в сессии до окончания сессии пользователя.

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

Флеш-сообщения

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

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

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

public function update(Request $request): Response
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        // do some sort of processing

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
        // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add()

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

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

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

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

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
26
{# templates/base.html.twig #}

{# прочитать и отобразить только один тип флеш-сообщения #}
{% for message in app.flashes('notice') %}
    <div class="flash-notice">
        {{ message }}
    </div>
{% endfor %}

{# прочитать и отобразить несколько типов флеш-сообщений #}
{% for label, messages in app.flashes(['success', 'warning']) %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

{# прочитать и отобразить все флеш-сообщения #}
{% for label, messages in app.flashes %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

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

Tip

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

Объекты Запрос и Ответ

Как упоминалось ранее , 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
26
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

public function index(Request $request): Response
{
    $request->isXmlHttpRequest(); // is it an Ajax request?

    $request->getPreferredLanguage(['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.

В Symfony, контроллер должен возвращать объект Response:

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\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.

Доступ к значениям конфигурации

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

1
2
3
4
5
6
// ...
public function index(): Response
{
    $contentsDir = $this->getParameter('kernel.project_dir').'/contents';
    // ...
}

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

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

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpFoundation\Response;
// ...

public function index(): Response
{
    // возвращает '{"username":"jane.doe"}' и устанавливает правильный заголовок Content-Type
    return $this->json(['username' => 'jane.doe']);

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

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

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

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

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

public function download(): Response
{
    // отправить содержание файла и заставить браузер скачать его
    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
17
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
// ...

public function download(): Response
{
    // загрузить файл из файловой системы
    $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, контроллер - это обычно метод класса, который используется для приёма запросов и выдачи объекта Response. Если связать его с URL, контроллер становится доступным и его ответ можно увидеть.

Для помощи в разработке контроллеров, Symfony предоставляет AbstractController. Он может быть использован для расширения класса контроллера давая доступ к часто используемым функциям такие как render() и redirectToRoute(). AbstractController также предоставляет метод createNotFoundException(), который используется для возврата ответа "404. Не найдено"

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