Контроллер

Дата обновления перевода 2024-07-29

Контроллер

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

Tip

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

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

В то время как контроллер может быть любой 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\Attribute\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
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!');
        // ...
    }
}

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

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

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

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

Для экономии времени, вы можете установить 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.

Автоматическое сопоставление запроса

Можно автоматически сопоставить полезную нагрузку запроса и/или параметры запроса с аргументами действий вашего контроллера с помощью атрибутов.

Индивидуальное сопоставление парамтеров запроса

Допустим, пользователь отправляет вам запрос со следующей строкой запроса: https://example.com/dashboard?firstName=John&lastName=Smith&age=27. MapQueryParameter, аргументы действия вашего контроллера могут быть автоматически выполнены:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;

// ...

public function dashboard(
    #[MapQueryParameter] string $firstName,
    #[MapQueryParameter] string $lastName,
    #[MapQueryParameter] int $age,
): Response
{
    // ...
}

Параметр #[MapQueryParameter] может принимать необязательный аргумент под названием filter. Вы можете использовать константы Validate Filters, определенные в PHP:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;

// ...

public function dashboard(
    #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\w+$/'])] string $firstName,
    #[MapQueryParameter] string $lastName,
    #[MapQueryParameter(filter: \FILTER_VALIDATE_INT)] int $age,
): Response
{
    // ...
}

Сопоставление всей строки запроса

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace App\Model;

use Symfony\Component\Validator\Constraints as Assert;

class UserDTO
{
    public function __construct(
        #[Assert\NotBlank]
        public string $firstName,

        #[Assert\NotBlank]
        public string $lastName,

        #[Assert\GreaterThan(18)]
        public int $age,
    ) {
    }
}
Затем вы можете использовать атрибут MapQueryString

в вашем контроллере:

1
2
3
4
5
6
7
8
9
10
11
12
use App\Model\UserDto;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

// ...

public function dashboard(
    #[MapQueryString] UserDTO $userDto
): Response
{
    // ...
}

Вы можете настроить группы валидации, используемые при сопоставлении, а также HTTP-статус, возвращаемый при неудачной валидации:

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

// ...

public function dashboard(
    #[MapQueryString(
        validationGroups: ['strict', 'edit'],
        validationFailedStatusCode: Response::HTTP_UNPROCESSABLE_ENTITY
    )] UserDTO $userDto
): Response
{
    // ...
}

Код состояния по умолчанию возвращаемый при неудачной проверке - 404.

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

1
2
3
4
5
6
7
8
9
10
11
12
use App\Model\UserDto;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

// ...

public function dashboard(
    #[MapQueryString] UserDTO $userDto = new UserDTO()
): Response
{
    // ...
}

Сопоставление полезной нагрузки запроса

При создании API и работе с методами HTTP, отличными от GET (такими как POST или PUT), данные пользователя хранятся не в строке запроса, а непосредственно в полезной нагрузке запроса, например, так:

1
2
3
4
5
{
    "firstName": "John",
    "lastName": "Smith",
    "age": 28
}

В этом случае можно также напрямую сопоставить эту полезную нагрузку с вашим DTO, используя атрибут MapRequestPayload:

1
2
3
4
5
6
7
8
9
10
11
12
use App\Model\UserDto;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;

// ...

public function dashboard(
    #[MapRequestPayload] UserDTO $userDto
): Response
{
    // ...
}

Этот атрибут позволяет настроить контекст сериализации, а также класс, отвечающий за сопоставление запроса и вашего DTO:

public function dashboard(
#[MapRequestPayload(
serializationContext: ['...'], resolver: AppResolverUserDtoResolver

)] UserDTO $userDto

): Response { // ... }

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

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

// ...

public function dashboard(
    #[MapRequestPayload(
        acceptFormat: 'json',
        validationGroups: ['strict', 'read'],
        validationFailedStatusCode: Response::HTTP_NOT_FOUND
    )] UserDTO $userDto
): Response
{
    // ...
}

Статус-код по умолчанию, возвращаемый при неудачной валидации, - 422.

Tip

Если вы создаете API на основе JSON, обязательно объявите свой маршрут как использующий JSON формат . Это заставит обработку ошибок выводить JSON-ответ, а не HTML-страницу в случае ошибок валидации:

1
#[Route('/dashboard', name: 'dashboard', format: 'json')]

Обязательно установите phpstan/phpdoc-parser и phpdocumentor/type-resolver, если вы хотите отобразить вложенный массив определенных DTO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function dashboard(
    #[MapRequestPayload()] EmployeesDTO $employeesDto
): Response
{
    // ...
}

final class EmployeesDTO
{
    /**
     * @param UserDTO[] $users
     */
    public function __construct(
        public readonly array $users = []
    ) {}
}

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

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

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

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()) {
        // сделать какую-то обработку

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
        // $this->addFlash() эквивалентно $request->getSession()->getFlashBag()->add()

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

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

Прочитайте это , чтобы узнать больше об использовании сессий.

Объект Request и Response

Как уже упоминалось раньше , Symfony будет передавать объект Request любому аргументу контроллера, тип которого указан с помощью класса Request:

use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse;

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;

// создаёт простой Response со статус-кодом (по умолчанию)
$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, который шифрует данные автоматически:

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

public function index(): JsonResponse
{
    // возвращает '{"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\BinaryFileResponse;
// ...

public function download(): BinaryFileResponse
{
    // отправить содержание файла и заставить браузер скачать его
    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(): BinaryFileResponse
{
    // загрузить файл из файловой системы
    $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);
}

Отправка ранних подсказок

Ранние подсказки указывают браузеру на необходимость начать загрузку некоторых ресурсов еще до того, как приложение отправляет содержание ответа. Это улучшает воспринимаемую производительность, поскольку браузер может предварительно получить ресурсы, которые понадобятся после отправки полного ответа. Эти ресурсы обычно представляют собой файлы Javascript или CSS, но это могут быть любые типы ресурсов.

Note

Для работы SAPI, который вы используете, должен поддерживать эту функцию, например FrankenPHP.

Вы можете отправлять ранние подсказки из действия контроллера благодаря методу sendEarlyHints():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\WebLink\Link;

class HomepageController extends AbstractController
{
    #[Route("/", name: "homepage")]
    public function index(): Response
    {
        $response = $this->sendEarlyHints([
            new Link(rel: 'preconnect', href: 'https://fonts.google.com'),
            (new Link(href: '/style.css'))->withAttribute('as', 'stylesheet'),
            (new Link(href: '/script.js'))->withAttribute('as', 'script'),
        ]);

        // подготовить содержание ответа...

        return $this->render('homepage/index.html.twig', response: $response);
    }
}

Технически, ранние подсказки - это информационный HTTP-ответ со статус-кодом 103. Метод endEarlyHints() создает объект Response с этим статус-кодом и немедленно отправляет его заголовки.

Таким образом, браузеры могут сразу же начать загрузку ресурсов; например,
файлы style.css и cript.js в примере выше.
Метод endEarlyHints() также возвращает объект Response, который вы должны использовать для создания полного ответа, отправленного действием контроллера.

Заключение

В Symfony, контроллер - это обычно метод класса, который используется для приёма запросов и выдачи объекта Response. Если связать его с URL, контроллер становится доступным и его ответ можно увидеть.

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

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