SymfonyMiddleTechnical

Что такое Symfony controller и что он делает?

Symfony-контроллер — callable, принимающий Request и возвращающий Response; фреймворк внедряет зависимости через DI, разрешает аргументы через ArgumentResolver и обрабатывает результат в kernel.view.

Что такое Symfony Controller

Контроллер в Symfony — это PHP-класс (или callable), который принимает объект Request и возвращает объект Response. Фреймворк не навязывает базовый класс: можно наследоваться от AbstractController для удобных хелперов, а можно писать plain PHP-класс — главное соответствие контракту callable(Request): Response.

Место в Request Lifecycle

HttpKernel обрабатывает запрос в несколько этапов:

  • kernel.request — firewall, locale listener, router читает URL и сохраняет атрибуты в Request::$attributes.
  • kernel.controller — ControllerResolver разрешает callable по атрибуту _controller; ArgumentResolver инжектирует аргументы (сервисы из DI, route-параметры, тело запроса через #[MapRequestPayload]).
  • Контроллер выполняется и возвращает Response или любое значение.
  • kernel.view — если вернули не Response (например, массив или DTO), ViewListener сериализует его.
  • kernel.response — финальные модификации (кеш-заголовки, CORS и т.д.).

Базовый пример

<?php
// src/Controller/UserController.php
namespace App\Controller;

use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class UserController extends AbstractController
{
    #[Route('/users/{id}', name: 'user_show', methods: ['GET'])]
    public function show(User $user): JsonResponse
    {
        // EntityValueResolver автоматически загружает User по {id}
        return $this->json([
            'id'    => $user->getId(),
            'email' => $user->getEmail(),
        ]);
    }
}

Атрибут #[Route] регистрирует маршрут прямо на методе — альтернатива YAML/XML-конфигурации. AbstractController::json() использует Symfony Serializer (или json_encode как fallback) и устанавливает Content-Type: application/json.

Инъекция зависимостей в контроллер

<?php
use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class UserController extends AbstractController
{
    public function __construct(
        private readonly UserRepository $users,
    ) {}

    #[Route('/users', methods: ['GET'])]
    public function index(Request $request): Response
    {
        $page  = $request->query->getInt('page', 1);
        $users = $this->users->findPaginated($page, limit: 20);

        return $this->json($users);
    }
}

Symfony DI автоматически внедряет UserRepository через constructor autowiring — никакого new или Service Locator не нужно.

Валидация и обработка ошибок

<?php
use App\Dto\CreateUserDto;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpFoundation\JsonResponse;

#[Route('/users', methods: ['POST'])]
public function create(
    #[MapRequestPayload] CreateUserDto $dto,
): JsonResponse {
    // $dto уже провалидирован через Validator + Symfony Constraints
    // При ошибке Symfony автоматически вернёт 422 Unprocessable Entity
    $user = $this->users->createFromDto($dto);
    return $this->json($user, status: 201);
}

Подводные камни

  • Толстый контроллер — бизнес-логика в контроллере затрудняет тестирование; выносите её в сервисы или command handlers.
  • EntityValueResolver и 404 — если сущность не найдена, Symfony бросает NotFoundHttpException; убедитесь, что это нужное поведение, иначе используйте Repository вручную.
  • Порядок маршрутов — при YAML/XML-конфигурации маршрут /users/me должен стоять перед /users/{id}, иначе «me» будет интерпретировано как UUID/id.
  • Сериализация циклических ссылок$this->json() упадёт с CircularReferenceException при двусторонних связях Doctrine без @Ignore или группы сериализации.
  • Отсутствие Response-типа — возврат массива без kernel.view listener вызовет LogicException; убедитесь, что установлен TwigBundle или SensioFrameworkExtraBundle.
  • Тестирование — используйте WebTestCase::createClient() и $client->request('GET', '/users/1'); не мокайте сам контроллер, мокайте зависимости.
  • AbstractController vs plain classAbstractController предоставляет $this->json(), $this->render(), $this->getUser(), но тянет зависимость от FrameworkBundle; для библиотечного кода лучше plain class.

Common mistakes

  • Сводить controllers к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет controllers через конкретную точку lifecycle в Symfony.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics