<? phpukraine ДОКУМЕНТАЦІЯ
Пошук по платформі
Документація українською

Переклад силами спільноти. Кожен розділ показує стан готовності — недоперекладене відкрито помічене, а не приховане.

ВЕРСІЯ
АКТУАЛЬНА Актуальний реліз. Переклад наздоганяє оригінал — розділи з низьким відсотком позначені у змісті.
ОСНОВИ · ПЕРЕКЛАДЕНО оновлено 5 вересня 2026

Контролери — Symfony

Контролер — це PHP-функція, яку ви створюєте; вона читає інформацію з обʼєкта Request і створює та повертає обʼєкт Response. Відповіддю може бути HTML-сторінка, JSON, XML, завантаження файлу, редирект, помилка 404 чи будь-що інше. Контролер виконує довільну логіку, потрібну вашому застосунку, щоб відрендерити вміст сторінки.

Порада

Якщо ви ще не створили свою першу робочу сторінку, перегляньте Створення сторінки, а тоді повертайтесь!

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

Хоч контролером може бути будь-яка PHP-callable-структура (функція, метод обʼєкта або замикання Closure), зазвичай контролер — це метод усередині класу-контролера:

// 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 має необовʼязковий базовий клас контролера — Symfony\Bundle\FrameworkBundle\Controller\AbstractController. Його можна розширити, щоб отримати доступ до допоміжних методів.

Додайте інструкцію use на початку класу контролера, а тоді змініть LuckyController, щоб він його розширював:

  // src/Controller/LuckyController.php
  namespace App\Controller;

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

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

Ось і все! Тепер вам доступні методи на кшталт $this->render() та багато інших, про які ви дізнаєтесь далі.

Генерація URL

Метод Symfony\Bundle\FrameworkBundle\Controller\AbstractController::generateUrl — це просто допоміжний метод, який генерує URL для заданого маршруту:

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

Редиректи

Якщо потрібно перенаправити користувача на іншу сторінку, використовуйте методи redirectToRoute() і redirect():

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

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

    // редирект на маршрут із параметрами
    return $this->redirectToRoute('app_lucky_number', ['max' => 10]);
    // _fragment — спеціальний параметр, який вказує напряму на визначений якір
    return $this->redirectToRoute('app_lucky_number', ['_fragment' => 'result']);

    // перенаправляє на маршрут і зберігає оригінальні параметри рядка запиту
    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');
}

Небезпечно

Метод redirect() жодним чином не перевіряє призначення. Якщо ви перенаправляєте на URL, наданий кінцевими користувачами, ваш застосунок може бути вразливим до вразливості безпеки «неперевірені редиректи».

Рендеринг шаблонів

Якщо ви віддаєте HTML, вам знадобиться відрендерити шаблон. Метод render() рендерить шаблон і кладе цей вміст в обʼєкт Response за вас:

// рендерить templates/lucky/number.html.twig
return $this->render('lucky/number.html.twig', ['number' => $number]);

Шаблонізація й Twig докладніше пояснені у статті Створення й використання шаблонів.

Отримання сервісів

Symfony напакована безліччю корисних класів і можливостей, які називають сервісами. Вони використовуються для рендерингу шаблонів, надсилання листів, запитів до бази даних і будь-якої іншої «роботи», яку тільки можна уявити.

Якщо вам потрібен сервіс у контролері, вкажіть тип аргумента через імʼя його класу (або інтерфейсу) — і Symfony впровадить його автоматично. Для цього потрібно, щоб ваш контролер був зареєстрований як сервіс:

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:

$ php bin/console debug:autowiring

Порада

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

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

Докладніше про цей атрибут можна почитати в Атрибут Autowire.

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

Докладніше про сервіси див. статтю Контейнер сервісів.

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

Щоб зекономити час, можна встановити Symfony Maker і попросити Symfony згенерувати новий клас контролера:

$ php bin/console make:controller BrandNewController

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

Якщо ви хочете згенерувати цілий CRUD із сутності Doctrine, використовуйте:

$ 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. Для цього киньте спеціальний тип винятку:

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

        // наведене вище — просто скорочення для:
        // throw new NotFoundHttpException('The product does not exist');
    }

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

Метод Symfony\Bundle\FrameworkBundle\Controller\AbstractController::createNotFoundException — це просто скорочення для створення спеціального обʼєкта Symfony\Component\HttpKernel\Exception\NotFoundHttpException, який зрештою спричиняє HTTP-відповідь 404 усередині Symfony.

Якщо ви кинете виняток, що розширює Symfony\Component\HttpKernel\Exception\HttpException або є його екземпляром, Symfony використає відповідний код стану HTTP. Інакше відповідь матиме код стану HTTP 500:

// цей виняток зрештою генерує помилку зі статусом 500
throw new \Exception('Something went wrong!');

У будь-якому разі кінцевому користувачеві показується сторінка помилки, а розробнику — повна сторінка помилки з налагоджувальною інформацією (тобто коли ви в режимі «Debug» — див. Оточення).

Щоб налаштувати сторінку помилки, яку бачить користувач, див. статтю Сторінки помилок.

Обʼєкт Request як аргумент контролера

А що, коли вам потрібно прочитати параметри рядка запиту, отримати заголовок запиту чи доступ до завантаженого файлу? Ця інформація зберігається в обʼєкті Request Symfony. Щоб дістатися до неї у своєму контролері, додайте його як аргумент і вкажіть тип — клас Request:

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. Завдяки атрибуту Symfony\Component\HttpKernel\Attribute\MapQueryParameter аргументи дії вашого контролера можуть заповнюватись автоматично:

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 підтримує такі типи аргументів:

  • \BackedEnum
  • array
  • bool
  • float
  • int
  • string
  • Обʼєкти, що розширюють Symfony\Component\Uid\AbstractUid

#[MapQueryParameter] може приймати необовʼязковий аргумент filter. Ви можете використовувати константи фільтрів валідації, визначені в PHP:

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 з необовʼязковими обмеженнями валідації:

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,
    ) {
    }
}

Тоді у своєму контролері ви можете використати атрибут Symfony\Component\HttpKernel\Attribute\MapQueryString:

use App\Model\UserDto;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

// ...

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

Ви можете налаштувати групи валідації, які використовуються під час звʼязування, а також HTTP-статус, який повертається, якщо валідація не пройшла:

use Symfony\Component\HttpFoundation\Response;

// ...

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

Код стану, що повертається за замовчуванням, якщо валідація не пройшла, — 404.

Якщо ви хочете звʼязати свій обʼєкт із вкладеним масивом у своєму запиті за певним ключем, задайте опцію key в атрибуті #[MapQueryString]:

use App\Model\SearchDto;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

// ...

public function dashboard(
    #[MapQueryString(key: 'search')] SearchDto $searchDto
): Response
{
    // ...
}

Якщо вам потрібен валідний DTO навіть тоді, коли рядок запиту порожній, задайте значення за замовчуванням для аргументів свого контролера:

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), дані користувача зберігаються не в рядку запиту, а безпосередньо в корисному навантаженні запиту, ось так:

{
    "firstName": "John",
    "lastName": "Smith",
    "age": 28
}

У цьому випадку також можна напряму звʼязати це корисне навантаження зі своїм DTO за допомогою атрибута Symfony\Component\HttpKernel\Attribute\MapRequestPayload:

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: App\Resolver\UserDtoResolver
    )]
    UserDto $userDto
): Response
{
    // ...
}

Ви також можете налаштувати використовувані групи валідації, код стану, що повертається, якщо валідація не пройшла, а також підтримувані формати корисного навантаження:

use Symfony\Component\HttpFoundation\Response;
// ...

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

Код стану, що повертається за замовчуванням, якщо валідація не пройшла, — 422.

Ви також можете використовувати вирази, щоб визначати групи валідації динамічно на основі аргументів контролера:

use Symfony\Component\ExpressionLanguage\Expression;
// ...

#[Route('/user/{id}', methods: ['PUT'])]
public function update(
    User $user,
    #[MapRequestPayload(
        validationGroups: [new Expression('args["user"].getType()')]
    )] UpdateUserDto $dto
): Response
{
    // ...
}

У цьому прикладі група валідації визначається із сутності User. Змінна args дає доступ до всіх аргументів контролера за іменем.

Нововведення 8.1

Підтримку виразів у validationGroups було впроваджено в Symfony 8.1.

Порада

Якщо ви будуєте JSON API, обовʼязково оголосіть свій маршрут як такий, що використовує формат JSON. Тоді обробка помилок видаватиме JSON-відповідь у разі помилок валідації, а не HTML-сторінку:

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

Обовʼязково встановіть phpstan/phpdoc-parser і phpdocumentor/type-resolver, якщо хочете звʼязати вкладений масив конкретних DTO:

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

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

Замість повертати масив обʼєктів DTO, ви можете сказати Symfony перетворити кожен обʼєкт DTO на масив і повернути щось таке:

[
    {
        "firstName": "John",
        "lastName": "Smith",
        "age": 28
    },
    {
        "firstName": "Jane",
        "lastName": "Doe",
        "age": 30
    }
]

Щоб це зробити, використайте варіативний аргумент і дайте Symfony автоматично звʼязати кожен елемент корисного навантаження з екземпляром DTO:

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

Нововведення 8.1

Підтримку варіативних аргументів із #[MapRequestPayload] було впроваджено в Symfony 8.1.

Зверніть увагу

Як альтернативу варіативним аргументам ви можете звʼязати параметр як масив і налаштувати тип кожного елемента через опцію type атрибута:

public function dashboard(
    #[MapRequestPayload(type: UserDto::class)] array $users
): Response
{
    // ...
}

Symfony надає атрибут #[MapUploadedFile] для звʼязування завантажених файлів, але ви також можете використовувати #[MapRequestPayload], щоб звʼязати файли, включені в корисне навантаження запиту.

Нововведення 8.1

Підтримку звʼязування файлів із #[MapRequestPayload] було впроваджено в Symfony 8.1.

Обробляючи запити multipart/form-data, Symfony автоматично обʼєднує параметри запиту й завантажені файли перед десеріалізацією корисного навантаження. Це дозволяє звʼязувати як скалярні значення, так і екземпляри UploadedFile в один обʼєкт передачі даних.

Наприклад, ви можете визначити обʼєкт запиту, що містить і скалярні значення, і завантажені файли:

use Symfony\Component\HttpFoundation\File\UploadedFile;

class ProductRequest
{
   public ?string $name = null;
   public ?UploadedFile $image = null;
}

І звʼязати його напряму в дії контролера:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;

public function upload(
     #[MapRequestPayload] ProductRequest $data,
): Response
{
    // $data->name містить параметри запиту
    // $data->image містить завантажений файл як екземпляр UploadedFile

    return new Response('OK');
}

Зверніть увагу

Завантажені файли беруться з $request->files і обʼєднуються з параметрами запиту перед десеріалізацією. Це працює зі стандартними відправленнями форм і з multipart-запитами.

Звʼязування порожніх даних

За замовчуванням резолвер повертає null, не викликаючи серіалізатор, коли рядок запиту або тіло запиту порожні, а параметр допускає null або має значення за замовчуванням. Це означає, що власні денормалізатори не викликаються.

Якщо вам потрібно, щоб денормалізація відбувалася навіть тоді, коли даних немає (наприклад, щоб дозволити власному денормалізатору заповнити деякі поля з контексту безпеки чи сесії), задайте опції mapWhenEmpty значення true:

use App\Model\SearchFilters;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

// ...

public function search(
    #[MapQueryString(mapWhenEmpty: true)] SearchFilters $filters
): Response
{
    // ...
}

Ця опція також працює з #[MapRequestPayload]. Коли mapWhenEmpty дорівнює true, резолвер передає порожній масив у метод denormalize() серіалізатора, дозволяючи власним денормалізаторам заповнити обʼєкт.

Нововведення 8.1

Опцію mapWhenEmpty було впроваджено в Symfony 8.1.

Звʼязування завантажених файлів

Symfony надає атрибут #[MapUploadedFile], щоб звʼязувати один або кілька обʼєктів UploadedFile з аргументами контролера:

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\Routing\Attribute\Route;

class UserController extends AbstractController
{
    #[Route('/user/picture', methods: ['PUT'])]
    public function changePicture(
        #[MapUploadedFile] UploadedFile $picture,
    ): Response {
        // ...
    }
}

У цьому прикладі відповідний резолвер аргументів отримує UploadedFile на основі імені аргумента ($picture). Якщо файл не надіслано, кидається HttpException. Це можна змінити, зробивши аргумент контролера таким, що допускає null:

#[MapUploadedFile]
?UploadedFile $document

Атрибут #[MapUploadedFile] також дозволяє передати список обмежень, які застосовуються до завантаженого файлу:

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Constraints as Assert;

class UserController extends AbstractController
{
    #[Route('/user/picture', methods: ['PUT'])]
    public function changePicture(
        #[MapUploadedFile([
            new Assert\File(mimeTypes: ['image/png', 'image/jpeg']),
            new Assert\Image(maxWidth: 3840, maxHeight: 2160),
        ])]
        UploadedFile $picture,
    ): Response {
        // ...
    }
}

Обмеження валідації перевіряються перед впровадженням UploadedFile в аргумент контролера. Якщо є порушення обмеження, кидається HttpException, а дія контролера не виконується.

Якщо вам потрібно завантажити колекцію файлів, звʼяжіть їх із масивом або варіативним аргументом. Задане обмеження застосується до всіх файлів, і якщо якийсь із них його не пройде, кидається HttpException:

#[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))]
array $documents

#[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))]
UploadedFile ...$documents

Використовуйте опцію name, щоб перейменувати завантажений файл на власне значення:

#[MapUploadedFile(name: 'something-else')]
UploadedFile $document

Крім того, ви можете змінити код стану HTTP-винятку, який кидається за наявності порушень обмежень:

#[MapUploadedFile(
    constraints: new Assert\File(maxSize: '2M'),
    validationFailedStatusCode: Response::HTTP_REQUEST_ENTITY_TOO_LARGE
)]
UploadedFile $document

Звʼязування заголовків запиту

Нововведення 8.1

Атрибут #[MapRequestHeader] було впроваджено в Symfony 8.1.

Атрибут Symfony\Component\HttpKernel\Attribute\MapRequestHeader звʼязує заголовок HTTP-запиту з аргументом контролера:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;

// ...

public function dashboard(
    #[MapRequestHeader] string $acceptLanguage
): Response {
    // ...
}

За замовчуванням імʼя заголовка перетворюється з kebab-case на camelCase, щоб відповідати імені аргумента (наприклад, заголовок accept-language звʼязується з аргументом $acceptLanguage). Ви також можете передати імʼя HTTP-заголовка явно як першу опцію атрибута:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;

// ...

public function dashboard(
    #[MapRequestHeader(name: 'x-custom-token')] string $token,
): Response {
    // ...
}

Атрибут підтримує такі типи аргументів:

  • string: повертає значення заголовка як рядок;
  • array: повертає значення заголовка як масив. Для заголовків accept, accept-charset, accept-language та accept-encoding значення розбираються автоматично (наприклад, accept-language: en-us,en;q=0.5 повертає ['en_US', 'en']);
  • Symfony\Component\HttpFoundation\AcceptHeader: повертає розібраний обʼєкт AcceptHeader для просунутої роботи зі значеннями якості (quality-value).

Якщо заголовка немає, а аргумент не має значення за замовчуванням і не допускає null, повертається відповідь 400 Bad Request. Цей код стану можна налаштувати опцією validationFailedStatusCode:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;

// ...

public function dashboard(
    #[MapRequestHeader(validationFailedStatusCode: Response::HTTP_UNPROCESSABLE_ENTITY)] string $accept,
): Response {
    // ...
}

Керування сесією

Symfony надає сервіс сесії для зберігання інформації про користувача між запитами. Доступ до сесії можна отримати через обʼєкт Request (у сервісах — впровадьте сервіс RequestStack):

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

public function index(Request $request): Response
{
    $session = $request->getSession();

    // зберегти атрибут для повторного використання під час пізнішого запиту користувача
    $session->set('user_id', 42);

    // отримати атрибут із необовʼязковим значенням за замовчуванням
    $userId = $session->get('user_id', 0);

    // ...
}

Прочитайте документацію про сесії, щоб дізнатися більше про налаштування й використання сесій.

Flash-повідомлення

Flash-повідомлення — це спеціальні повідомлення сесії, призначені для одноразового використання: вони автоматично зникають із сесії, щойно ви їх отримали. Це робить їх ідеальними для зберігання сповіщень користувачу:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

public function update(Request $request): Response
{
    // ... виконуємо якусь обробку даних

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

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

Обʼєкти Request і Response

Як згадувалося раніше, Symfony передасть обʼєкт Request у будь-який аргумент контролера, для якого вказано тип Request:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

public function index(Request $request): Response
{
    $request->isXmlHttpRequest(); // це Ajax-запит?

    $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. Цей обʼєкт має тип Symfony\Component\HttpFoundation\ResponseHeaderBag і надає методи для отримання й задання заголовків відповіді. Імена заголовків нормалізуються. Як наслідок, імʼя Content-Type еквівалентне імені content-type чи content_type.

У Symfony контролер зобовʼязаний повертати обʼєкт Response:

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.

Зверніть увагу

Технічно контролер може повернути значення, відмінне від Response. Однак ваш застосунок відповідає за перетворення цього значення на обʼєкт Response. Це обробляється за допомогою подій (зокрема події kernel.view) — просунутої можливості, про яку ви дізнаєтесь пізніше.

Доступ до значень конфігурації

Щоб отримати значення будь-якого параметра конфігурації з контролера, використовуйте допоміжний метод getParameter():

// ...
public function index(): Response
{
    $contentsDir = $this->getParameter('kernel.project_dir').'/contents';
    // ...
}

Повернення JSON-відповіді

Щоб повернути JSON із контролера, використовуйте допоміжний метод json(). Він повертає обʼєкт JsonResponse, який кодує дані автоматично:

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 = []);
}

Якщо у вашому застосунку увімкнено сервіс serializer, він використовуватиметься для серіалізації даних у JSON. Інакше використовується функція json_encode.

Автоматична серіалізація значень, що повертає контролер

Замість того щоб вручну викликати серіалізатор і будувати відповідь, ви можете додати до методу свого контролера атрибут Symfony\Component\HttpKernel\Attribute\Serialize. Тоді контролер може повертати будь-який обʼєкт чи масив, а Symfony серіалізує його автоматично на основі формату запиту (за замовчуванням — JSON):

use Symfony\Component\HttpKernel\Attribute\Serialize;

class ProductController
{
    #[Serialize]
    public function show(): Product
    {
        return new Product(1, 'Asus UX550');
    }
}

Ви також можете налаштувати код стану HTTP, заголовки та контекст серіалізації:

use Symfony\Component\HttpKernel\Attribute\Serialize;

class ProductController
{
    #[Serialize(code: 201, headers: ['X-Custom' => 'value'], context: ['groups' => ['read']])]
    public function create(): ProductCreated
    {
        // ... створюємо продукт

        return new ProductCreated(1);
    }
}

Атрибут #[Serialize] приймає такі аргументи:

code : Код стану HTTP відповіді (за замовчуванням: 200).

headers : Асоціативний масив додаткових HTTP-заголовків, які додаються до відповіді.

context : Контекст серіалізації, що передається в Serializer (наприклад, ['groups' => ['read']]).

Формат відповіді визначається форматом запиту ($request->getRequestFormat()), який за замовчуванням дорівнює json. Заголовок Content-Type задається автоматично на основі формату. Якщо формат не підтримується серіалізатором, повертається відповідь 415 Unsupported Media Type.

Зверніть увагу

Атрибут #[Serialize] вимагає, щоб компонент Serializer був встановлений і увімкнений.

Нововведення 8.1

Атрибут #[Serialize] було впроваджено в Symfony 8.1.

Потокові відповіді з файлами

Щоб віддати файл із контролера, ви можете скористатися помічником Symfony\Bundle\FrameworkBundle\Controller\AbstractController::file:

use Symfony\Component\HttpFoundation\BinaryFileResponse;
// ...

public function download(): BinaryFileResponse
{
    // надіслати вміст файлу і змусити браузер завантажити його
    return $this->file('/path/to/some_file.pdf');
}

Помічник file() надає кілька аргументів для налаштування своєї поведінки:

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

Надсилання Early Hints

Ви можете покращити продуктивність, надсилаючи відповіді 103 Early Hints, щоб попросити браузер почати завантажувати ресурси ще до того, як буде готова повна відповідь. Подробиці див. у Early Hints.

Потокова передача Server-Sent Events

Server-Sent Events (SSE) — це стандарт, який дозволяє серверу надсилати оновлення клієнту через одне HTTP-зʼєднання. Він дає ефективний спосіб надсилати оновлення в реальному часі із сервера в браузер — наприклад, живі сповіщення, оновлення прогресу чи стрічки даних.

Клас Symfony\Component\HttpFoundation\EventStreamResponse дозволяє передавати події клієнту потоком за протоколом SSE. Він автоматично задає потрібні заголовки (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive) і надає API для надсилання подій:

use Symfony\Component\HttpFoundation\EventStreamResponse;
use Symfony\Component\HttpFoundation\ServerEvent;

// ...

public function liveNotifications(): EventStreamResponse
{
    return new EventStreamResponse(function (): iterable {
        foreach ($this->getNotifications() as $notification) {
            yield new ServerEvent($notification->toJson());

            sleep(1); // імітуємо затримку між подіями
        }
    });
}

Клас Symfony\Component\HttpFoundation\ServerEvent — це DTO, що представляє SSE-подію згідно зі специфікацією SSE від WHATWG. Ви можете налаштувати кожну подію через аргументи її конструктора:

// базова подія лише з даними
yield new ServerEvent('Some message');

// подія з власним типом (клієнти слухають через addEventListener('my-event', ...))
yield new ServerEvent(
    data: json_encode(['status' => 'completed']),
    type: 'my-event'
);

// подія з ID (корисно для відновлення потоків через заголовок Last-Event-ID)
yield new ServerEvent(
    data: 'Update content',
    id: 'event-123'
);

// подія, яка каже клієнту повторити спробу через певний час (у мілісекундах)
yield new ServerEvent(
    data: 'Retry info',
    retry: 5000
);

// подія з коментарем (можна використовувати для keep-alive)
yield new ServerEvent(comment: 'keep-alive');

Для випадків, коли генератори непрактичні, ви можете використовувати метод Symfony\Component\HttpFoundation\EventStreamResponse::sendEvent для ручного контролю:

use Symfony\Component\HttpFoundation\EventStreamResponse;
use Symfony\Component\HttpFoundation\ServerEvent;

// ...

public function liveProgress(): EventStreamResponse
{
    return new EventStreamResponse(function (EventStreamResponse $response): void {
        $redis = new \Redis();
        $redis->connect('127.0.0.1');
        $redis->subscribe(['message'], function (/* ... */, string $message) use ($response): void {
            $response->sendEvent(new ServerEvent($message));
        });
    });
}

На боці клієнта ви можете слухати події за допомогою нативного API EventSource:

const eventSource = new EventSource('/live-notifications');

// слухати всі події (без конкретного типу)
eventSource.onmessage = (event) => {
    console.log('Received:', event.data);
};

// слухати події з конкретним типом
eventSource.addEventListener('my-event', (event) => {
    console.log('My event:', JSON.parse(event.data));
});

// обробляти помилки зʼєднання
eventSource.onerror = (error) => {
    console.error('SSE error:', error);
    eventSource.close();
};

Попередження

EventStreamResponse розроблено для застосунків з обмеженою кількістю одночасних зʼєднань. Оскільки SSE тримає HTTP-зʼєднання відкритими, воно споживає серверні ресурси (памʼять і ліміти зʼєднань) для кожного підключеного клієнта.

Для високонавантажених застосунків, яким потрібно транслювати оновлення багатьом клієнтам одночасно, розгляньте Mercure — він побудований поверх SSE, але використовує спеціальний хаб, щоб ефективно керувати зʼєднаннями.

Відокремлення контролерів від Symfony

Розширення базового класу AbstractController спрощує розробку контролерів і рекомендоване для більшості застосунків. Однак деякі досвідчені користувачі віддають перевагу тому, щоб повністю відокремити свої контролери від Symfony (наприклад, щоб покращити тестованість або дотримуватися дизайну, більш незалежного від фреймворку). Symfony надає інструменти, які допоможуть це зробити.

Щоб відокремити контролери, Symfony надає всі помічники з AbstractController через інший клас — Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper, де кожен помічник доступний як публічний метод:

use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;
use Symfony\Component\HttpFoundation\Response;

class MyController
{
    public function __construct(
        #[AutowireMethodOf(ControllerHelper::class)]
        private \Closure $render,
        #[AutowireMethodOf(ControllerHelper::class)]
        private \Closure $redirectToRoute,
    ) {
    }

    public function showProduct(int $id): Response
    {
        if (!$id) {
            return ($this->redirectToRoute)('product_list');
        }

        return ($this->render)('product/show.html.twig', ['product_id' => $id]);
    }
}

Ви можете впровадити весь клас ControllerHelper, якщо вам так більше подобається, але використання атрибута AutowireMethodOf, як у попередньому прикладі, дозволяє впровадити лише ті помічники, які вам справді потрібні, роблячи ваш код ефективнішим.

Оскільки #[AutowireMethodOf] працює також з інтерфейсами, ви можете визначити інтерфейси для цих допоміжних методів:

interface RenderInterface
{
    // це сигнатура помічника render()
    public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response;
}

Тоді оновіть свій контролер, щоб він використовував інтерфейс замість замикання:

use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;

class MyController
{
    public function __construct(
        #[AutowireMethodOf(ControllerHelper::class)]
        private RenderInterface $render,
    ) {
    }

    // ...
}

Використання інтерфейсів, як у попередньому прикладі, дає повну користь від статичного аналізу й автодоповнення без додаткового шаблонного коду.

Підсумкові думки

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

Щоб полегшити розробку контролерів, Symfony надає AbstractController. Його можна використати, щоб розширити клас контролера, отримавши доступ до деяких часто вживаних утиліт, як-от render() і redirectToRoute(). AbstractController також надає утиліту createNotFoundException(), яка використовується, щоб повернути відповідь «сторінку не знайдено».

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

Рухаймося далі!

Далі дізнайтесь усе про рендеринг шаблонів із Twig.

Дізнайтеся більше про контролери

ЯК ЦЯ СТОРІНКА ВИГЛЯДАЄ В ПОШУКУ
phpukraine.com/docs/symfony/controllers
Контролери — Symfony документація українською
Контролери у Symfony 8.1: переклад офіційної документації українською. Оновлено 5 вересня 2026. Приклади коду, пояснення та посилання на питання зі співбесід.
Переклад робить спільнота

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

1
Перекладачів
79%
Готовності
0
Вільних розділів
Глосарій термінів