Основы контроллеров
Контроллер в Symfony — это callable, который принимает HTTP-запрос и возвращает HTTP-ответ. На экзамене проверяют знание AbstractController, всех типов Response и best practices.
Способы объявления контроллеров
1. Наследование от AbstractController (рекомендуется)
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products', name: 'product_list')]
public function list(): Response
{
return $this->render('product/list.html.twig');
}
}
2. POPO контроллер с #[AsController]
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
#[AsController]
class ApiProductController
{
public function __construct(
private readonly ProductRepository $repository,
) {
}
#[Route('/api/products', name: 'api_product_list')]
public function list(): JsonResponse
{
return new JsonResponse($this->repository->findAll());
}
}
3. Invokable контроллер (single action)
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
#[AsController]
#[Route('/health', name: 'health_check')]
class HealthCheckController
{
public function __invoke(): Response
{
return new Response('OK', Response::HTTP_OK);
}
}
Подвох экзамена: Без
#[AsController]или наследования отAbstractControllerобычный класс НЕ получит возможность инжектить сервисы в action-методы.#[AsController]помечает класс тегомcontroller.service_arguments, что позволяет Symfony инжектить сервисы в параметры action-методов (не только в конструктор).
Методы AbstractController
Rendering
<?php
declare(strict_types=1);
class PageController extends AbstractController
{
public function show(): Response
{
// render() — renders Twig template, returns Response
return $this->render('page/show.html.twig', [
'title' => 'My Page',
'items' => [1, 2, 3],
]);
// renderView() — returns rendered HTML as string (NOT Response)
$html = $this->renderView('email/welcome.html.twig', [
'user' => $user,
]);
// Use when you need HTML string (e.g., for email body)
}
}
JSON ответы
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class ApiController extends AbstractController
{
public function data(): Response
{
// json() — uses Symfony Serializer, supports groups
return $this->json(
data: ['id' => 1, 'name' => 'Widget'],
status: Response::HTTP_OK,
headers: ['X-Custom' => 'value'],
context: ['groups' => ['api:read']],
);
// Direct JsonResponse (no Serializer, just json_encode)
return new JsonResponse(
data: ['status' => 'ok'],
status: 200,
);
// From raw JSON string
return JsonResponse::fromJsonString('{"id": 1}');
}
}
Подвох экзамена:
$this->json()использует Symfony Serializer (если установлен) и поддерживает serialization groups черезcontext.new JsonResponse()использует простойjson_encode()без Serializer. Для сложных объектов (entities) нужен$this->json()с groups.
Редиректы
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Response;
class RedirectController extends AbstractController
{
public function oldPage(): Response
{
// Redirect by route name (HTTP 302)
return $this->redirectToRoute('product_list');
// With route parameters
return $this->redirectToRoute('product_show', ['id' => 42]);
// Permanent redirect (HTTP 301)
return $this->redirectToRoute('product_list', [], Response::HTTP_MOVED_PERMANENTLY);
// Redirect after POST (HTTP 303 See Other)
return $this->redirectToRoute('product_list', [], Response::HTTP_SEE_OTHER);
// Redirect to absolute URL
return $this->redirect('https://example.com');
}
}
Forward (внутренняя переадресация)
<?php
declare(strict_types=1);
class LegacyController extends AbstractController
{
public function oldEndpoint(): Response
{
// Forward creates an internal sub-request
// URL in browser does NOT change
return $this->forward(NewController::class . '::newAction', [
'id' => 42,
]);
}
}
Подвох экзамена:
forward()создаёт sub-request внутри Symfony — URL в браузере НЕ меняется, клиент не знает о переадресации.redirect()/redirectToRoute()отправляет HTTP 302/301, браузер делает НОВЫЙ запрос по другому URL.
Файлы и потоки
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
class DownloadController extends AbstractController
{
// Sending file to client
public function download(): BinaryFileResponse
{
// Display inline (in browser)
return $this->file('/path/to/file.pdf');
// Force download with custom name
return $this->file(
'/path/to/file.pdf',
'report.pdf',
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
);
}
// Streaming large data (memory efficient)
public function exportCsv(): StreamedResponse
{
$response = new StreamedResponse(function (): void {
$handle = fopen('php://output', 'w');
fputcsv($handle, ['ID', 'Name', 'Email']);
foreach ($this->getUsers() as $user) {
fputcsv($handle, [$user->getId(), $user->getName(), $user->getEmail()]);
flush();
}
fclose($handle);
});
$response->headers->set('Content-Type', 'text/csv');
$response->headers->set('Content-Disposition', 'attachment; filename="users.csv"');
return $response;
}
}
Объект Request
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
class SearchController extends AbstractController
{
#[Route('/search', name: 'search')]
public function search(Request $request): Response
{
// Query string (?q=symfony&page=2)
$query = $request->query->get('q', '');
$page = $request->query->getInt('page', 1);
$active = $request->query->getBoolean('active', true);
// POST data
$name = $request->request->get('name');
// JSON body
$data = $request->toArray();
// Headers (case-insensitive)
$contentType = $request->headers->get('Content-Type');
$auth = $request->headers->get('Authorization');
// Request metadata
$method = $request->getMethod(); // GET, POST, etc.
$path = $request->getPathInfo(); // /search
$host = $request->getHost(); // example.com
$isSecure = $request->isSecure(); // true/false
$isAjax = $request->isXmlHttpRequest(); // true/false
$clientIp = $request->getClientIp(); // 192.168.1.1
// Route parameters (internal bag)
$routeName = $request->attributes->get('_route');
return $this->json(['query' => $query, 'page' => $page]);
}
}
Property Bags таблица
| Свойство | Источник | PHP-эквивалент | Тип Bag |
|---|---|---|---|
$request->query |
URL query string | $_GET |
InputBag |
$request->request |
POST body | $_POST |
InputBag |
$request->cookies |
Cookies | $_COOKIE |
InputBag |
$request->files |
Uploaded files | $_FILES |
FileBag |
$request->server |
Server vars | $_SERVER |
ServerBag |
$request->headers |
HTTP headers | Из $_SERVER |
HeaderBag |
$request->attributes |
Route params, internal | Нет аналога | ParameterBag |
Подвох экзамена:
$request->attributes— НЕ HTTP-данные. Это внутренний bag для передачи данных между компонентами Symfony (route parameters, resolved objects). Route parameters доступны через$request->attributes, а НЕ через$request->query.
HTTP Exceptions
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ProductController extends AbstractController
{
#[Route('/products/{id}', name: 'product_show')]
public function show(int $id): Response
{
$product = $this->findProduct($id);
if (!$product) {
// Helper creates exception, throw sends it
throw $this->createNotFoundException('Product not found');
}
return $this->json($product);
}
public function restricted(): Response
{
// 403 Forbidden
throw $this->createAccessDeniedException('No access');
// 400 Bad Request
throw new BadRequestHttpException('Invalid input');
// Any HTTP status code
throw new \Symfony\Component\HttpKernel\Exception\HttpException(429, 'Too many requests');
}
}
Подвох экзамена:
createNotFoundException()иcreateAccessDeniedException()только СОЗДАЮТ объект исключения, но НЕ бросают его. Обязательно нуженthrow. Написать$this->createNotFoundException()безthrow— ничего не произойдёт, код продолжит выполняться.
Полная таблица методов AbstractController
| Метод | Возвращает | Описание |
|---|---|---|
render() |
Response |
Рендер Twig-шаблона |
renderView() |
string |
Рендер шаблона как HTML-строка |
json() |
JsonResponse |
Сериализация в JSON (через Serializer) |
redirect() |
RedirectResponse |
Редирект по URL |
redirectToRoute() |
RedirectResponse |
Редирект по имени маршрута |
forward() |
Response |
Внутренний sub-request |
file() |
BinaryFileResponse |
Отправка файла |
generateUrl() |
string |
Генерация URL по имени маршрута |
addFlash() |
void |
Flash-сообщение в сессию |
isGranted() |
bool |
Проверка прав доступа |
denyAccessUnlessGranted() |
void |
403 если нет доступа |
getUser() |
?UserInterface |
Текущий аутентифицированный пользователь |
getParameter() |
mixed |
Параметр DI-контейнера |
createNotFoundException() |
NotFoundHttpException |
Создание 404 exception |
createAccessDeniedException() |
AccessDeniedException |
Создание 403 exception |
isCsrfTokenValid() |
bool |
Проверка CSRF-токена |
createForm() |
FormInterface |
Создание формы |
createFormBuilder() |
FormBuilderInterface |
Form builder |