MidТеория4 min

Content Negotiation и Language Detection

Accept, Accept-Language, Content-Type, quality values — согласование контента

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 через _format parameter. В 7.4 это нужно делать вручную.