Content Negotiation позволяет клиенту и серверу договориться о формате данных. На экзамене Symfony проверяют понимание заголовков Accept, Accept-Language, quality values и механизмов определения языка.
Заголовок Accept
Клиент указывает желаемые форматы ответа через заголовок Accept:
Accept: text/html, application/json;q=0.9, */*;q=0.1
Quality Values (q-factor)
Quality values определяют приоритет форматов от 0 до 1 (по умолчанию 1.0):
Accept: application/json;q=1.0, text/xml;q=0.8, text/plain;q=0.5, */*;q=0.1
| MIME-тип | q-value | Приоритет |
|---|---|---|
application/json |
1.0 (default) | Наивысший |
text/xml |
0.8 | Высокий |
text/plain |
0.5 | Средний |
*/* |
0.1 | Низший (fallback) |
Подвох экзамена: Если q-value не указан, он равен 1.0 (максимум).
Accept: text/html, application/jsonозначает, что оба формата имеют одинаковый приоритет 1.0. Сервер может выбрать любой из них.
Content Negotiation в Symfony
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
public function show(int $id, Request $request): Response
{
$product = $this->repository->find($id);
// Get preferred content type from Accept header
$acceptableTypes = $request->getAcceptableContentTypes();
// ['text/html', 'application/json', '*/*']
// Check specific format
$format = $request->getPreferredFormat('html');
return match ($format) {
'json' => $this->json($product),
'xml' => new Response(
$this->serializer->serialize($product, 'xml'),
200,
['Content-Type' => 'application/xml']
),
default => $this->render('product/show.html.twig', [
'product' => $product,
]),
};
}
}
Использование _format
<?php
declare(strict_types=1);
use Symfony\Component\Routing\Attribute\Route;
class ApiController extends AbstractController
{
// _format special parameter
#[Route('/products/{id}.{_format}', name: 'product_show',
defaults: ['_format' => 'html'],
requirements: ['_format' => 'html|json|xml']
)]
public function show(int $id, string $_format): Response
{
$product = $this->repository->find($id);
// Symfony sets Content-Type automatically based on _format
// /products/1.json → Content-Type: application/json
// /products/1.xml → Content-Type: application/xml
// /products/1.html → Content-Type: text/html
return match ($_format) {
'json' => $this->json($product),
'xml' => new Response($this->serializer->serialize($product, 'xml')),
default => $this->render('product/show.html.twig', ['product' => $product]),
};
}
}
Accept-Language
Формат заголовка
Accept-Language: ru-RU, ru;q=0.9, en-US;q=0.8, en;q=0.7, de;q=0.3
| Тег языка | q-value | Описание |
|---|---|---|
ru-RU |
1.0 | Русский (Россия) |
ru |
0.9 | Русский (любой регион) |
en-US |
0.8 | Английский (США) |
en |
0.7 | Английский (любой) |
de |
0.3 | Немецкий |
Language Detection в Symfony
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
// Get preferred language
$request = Request::createFromGlobals();
// Get ordered list of preferred languages
$languages = $request->getLanguages();
// ['ru_RU', 'ru', 'en_US', 'en', 'de']
// Get single preferred language matching available ones
$preferredLanguage = $request->getPreferredLanguage(['en', 'ru', 'de']);
// 'ru' (matches ru-RU/ru with highest q-value)
Locale в Symfony
<?php
declare(strict_types=1);
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
// Automatic locale detection listener
#[AsEventListener(event: KernelEvents::REQUEST, priority: 20)]
class LocaleListener
{
public function __construct(
private readonly string $defaultLocale = 'en',
private readonly array $supportedLocales = ['en', 'ru', 'de'],
) {
}
public function __invoke(RequestEvent $event): void
{
$request = $event->getRequest();
// Priority: URL param > session > Accept-Language > default
$locale = $request->attributes->get('_locale')
?? $request->getSession()?->get('_locale')
?? $request->getPreferredLanguage($this->supportedLocales)
?? $this->defaultLocale;
$request->setLocale($locale);
}
}
Подвох экзамена: В Symfony
_locale— специальный route parameter. Если он присутствует в маршруте, Symfony автоматически устанавливает locale запроса. Приоритет определения locale: 1) route parameter, 2) session, 3) Accept-Language header, 4) default_locale из конфигурации.
Content-Type
Типы контента
| MIME-тип | Описание | Расширение |
|---|---|---|
text/html |
HTML-документ | .html |
application/json |
JSON-данные | .json |
application/xml |
XML-документ | .xml |
text/plain |
Простой текст | .txt |
multipart/form-data |
Файлы + данные формы | — |
application/x-www-form-urlencoded |
Данные формы | — |
application/octet-stream |
Бинарные данные | — |
application/pdf |
PDF-документ | .pdf |
Content-Type в Request vs Response
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// REQUEST: Content-Type describes the BODY of the request
// (what the client is SENDING)
$contentType = $request->headers->get('Content-Type');
// 'application/json' for JSON API
// Shorthand
$format = $request->getContentTypeFormat();
// 'json', 'xml', 'html', etc.
// Get decoded JSON body
$data = $request->toArray(); // Decodes JSON body
// RESPONSE: Content-Type describes the BODY of the response
// (what the server is SENDING)
$response = new Response();
$response->headers->set('Content-Type', 'application/json');
// JsonResponse sets Content-Type automatically
$response = $this->json(['data' => $items]);
// Content-Type: application/json is set automatically
multipart/form-data vs x-www-form-urlencoded
<?php
declare(strict_types=1);
// x-www-form-urlencoded — default for HTML forms
// Data: name=John&age=30&city=Moscow
// Used for: simple forms without files
// multipart/form-data — required for file uploads
// Data: boundary-separated parts with headers
// Used for: forms with file inputs
// In Symfony, both are accessed the same way:
$name = $request->request->get('name');
$file = $request->files->get('avatar');
Подвох экзамена:
multipart/form-dataОБЯЗАТЕЛЕН для загрузки файлов. Сx-www-form-urlencodedфайлы не передаются. HTML-форма сenctype="multipart/form-data"посылает данные в многочастном формате. Безenctypeиспользуетсяx-www-form-urlencodedпо умолчанию.
Заголовок Vary
<?php
declare(strict_types=1);
// Vary header tells caches which request headers affect the response
$response->setVary(['Accept', 'Accept-Language']);
// Without Vary: cache returns same response regardless of Accept
// With Vary: cache stores separate versions per Accept value
// Common Vary values:
// Vary: Accept — different format per client
// Vary: Accept-Encoding — compressed vs uncompressed
// Vary: Accept-Language — different locale
// Vary: Cookie — different per user session
Symfony 8.0: Symfony 8.0 автоматически добавляет
Vary: Acceptпри использовании content negotiation через_formatparameter. В 7.4 это нужно делать вручную.