Контролер

Дата оновлення перекладу 2024-06-07

Контролер

Контролер - це створена вами 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\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
25
26
27
28
29
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;

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

    // redirectToRoute - скорочення для:
    // повернути новий RedirectResponse($this->generateUrl('homepage'));

    // робить постійне 301 перенаправлення
    return $this->redirectToRoute('homepage', [], 301);
    // якщо ви хочете, ви можете використати PHP-константи замість жорстко закодованих цифр
    return $this->redirectToRoute('homepage', [], Response::HTTP_MOVED_PERMANENTLY);

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

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

    // перенаправлення на поточний маршрут (наприклад, для патерну Post/Redirect/Get):
    return $this->redirectToRoute($request->attributes->get('_route'));

    // перенаправлення на зовнішній сайт
    return $this->redirect('http://symfony.com/doc');
}

Danger

Метод 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

Якщо вам необхідний контроль над точним значенням аргументу, ви можете use the #[Autowire] attribute:

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 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!');
            // ...
        }
    }

Ви можете прочитати більше про цей атрибут у :ref:`autowire-attribute`.

Як і з усіма сервісами, ви можете використовувати звичайне впровадження через конструктор у ваших контролерах.

Щоб дізнатися більше про сервіси, див. статтю Сервіс-контейнер.

Генерування контролерів

Для економії часу, ви можете встановити 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 , запускайте:

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
{
    // добути об'єкт з бази даних
    $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('Щось пішло не так!');

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

Для налаштування сторінки помилки, що відображається користувачу, див. статтю Як налаштувати сторінки помилок.

Об'єкт Request в якості аргументу контролера

Що ви будете робити, якщо вам знадобиться дізнатися параметри запиту, заголовок запиту або отримати доступ до завантаженого файлу? Вся ця інформація в Symfony міститься в об'єкті Request. Щоб отримати доступ до цієї інформації в контролері, просто додайте його в якості аргументу та додайте тип 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);

    // ...
}

Продовжуйте читати для детальнішої інформації про використання об'єкту Запит.

Автоматичне мапування запиту

Ви можете автоматично мапувати корисне навантаження запиту та/або параметри запиту з аргументами дій вашого контролера за допомогою атрибутів.

Індивідуальне мапування параметрів запиту

Припустимо, користувач надсилає вам запит з наступним рядком: 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:

1
2
3
4
5
6
7
8
9
10
public function dashboard(
    #[MapRequestPayload(
        serializationContext: ['...'],
        resolver: App\Resolver\UserDtoResolver
    )]
    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

Якщо ви будуєте JSON API, переконайтеся, що ви оголошуєте свій маршрут як такий, що використовує формат 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 = []
    ) {}
}

Управління сесією

Ви можете зберігати спеціальні повідомлення, які називаються "флеш-повідомленнями", в сесії користувача. За проектом, флеш-повідомлення мають бути використані лише один раз: вони зникають із сесії автоматично, як тільки ви їх отримуєте. Ця функція робить "флеш-повідомлення" особливо чудовими для зберігання сповіщень користувачів.

Наприклад, уявіть, що ви обробляєте відправку форми:

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:

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->getPayload()->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 зі статус-кодом 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\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. Метод SendEarlyHints() створює об'єкт Response з цим статус-кодом і негайно надсилає його заголовки.

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

Висновок

В Symfony контролер - це зазвичай метод класу, який використовується для прийому запитів та видачі об'єкту Response. Якщо зв'язати його з URL, контролер стає доступним і його відповідь можна побачити.

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

В інших статтях ви дізнаєтеся, як використовувати спеціальні сервіси всередині вашого контролера, що допоможе вам зберігати та отримувати об'єкти з бази даних, обробляти відправлені форми, працювати з кешем тощо.