Управление cookies и HTTP-кэширование — важные темы на экзамене Symfony. На вопросах проверяют знание атрибутов cookie, стратегий кэширования и правильного использования заголовков.
Cookies
Атрибуты Cookie
| Атрибут | Описание | Значение |
|---|---|---|
Name=Value |
Имя и значение | session=abc123 |
Domain |
Домен, для которого cookie действует | .example.com |
Path |
Путь на сервере | /api |
Expires |
Дата истечения (абсолютная) | Thu, 01 Jan 2026... |
Max-Age |
Время жизни в секундах | 3600 |
Secure |
Только через HTTPS | Флаг |
HttpOnly |
Недоступен через JavaScript | Флаг |
SameSite |
Защита от CSRF | Strict, Lax, None |
SameSite Policy
| Значение | Cross-site GET | Cross-site POST | Описание |
|---|---|---|---|
Strict |
Нет | Нет | Максимальная защита |
Lax |
Да | Нет | Баланс (по умолчанию) |
None |
Да | Да | Без ограничений (требует Secure) |
Подвох экзамена:
SameSite=NoneОБЯЗАТЕЛЬНО требует атрибутSecure(только HTTPS). БезSecureбраузер отклонит cookie сSameSite=None. По умолчанию браузеры используютLax.
Cookies в Symfony
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
// Creating a cookie
$cookie = Cookie::create('preferences')
->withValue(json_encode(['theme' => 'dark']))
->withExpires(new \DateTimeImmutable('+30 days'))
->withPath('/')
->withDomain('.example.com')
->withSecure(true)
->withHttpOnly(true)
->withSameSite('lax');
$response = new Response('OK');
$response->headers->setCookie($cookie);
// Shorthand — set raw cookie string
$response->headers->setCookie(
Cookie::create('tracking', 'abc123', time() + 86400)
);
// Reading cookies from request
$preferences = $request->cookies->get('preferences');
// Deleting a cookie — set with past expiration
$response->headers->clearCookie('preferences', '/', '.example.com');
Подвох экзамена:
Cookie::create()в Symfony возвращает объект Cookie (immutable с PHP-совместимыми withers). МетодclearCookie()создаёт cookie сMax-Age=0. Path и Domain должны совпадать с оригинальными при удалении, иначе cookie не удалится.
Expires vs Max-Age
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Cookie;
// Expires — absolute datetime
$cookie = Cookie::create('session')
->withValue('abc')
->withExpires(new \DateTimeImmutable('2026-12-31'));
// Max-Age — relative seconds (preferred)
$cookie = Cookie::create('session')
->withValue('abc')
->withExpires(new \DateTimeImmutable('+1 hour'));
// Session cookie — no Expires/Max-Age (deleted when browser closes)
$cookie = Cookie::create('session')
->withValue('abc')
->withExpires(0);
HTTP-кэширование
Модели кэширования
HTTP определяет две модели кэширования:
- Expiration model — клиент знает, когда кэш устаревает
- Validation model — клиент проверяет актуальность у сервера
Cache-Control
Cache-Control: public, max-age=3600, s-maxage=86400
| Директива | Описание |
|---|---|
public |
Может кэшировать любой прокси |
private |
Только браузер пользователя |
no-cache |
Можно кэшировать, но нужно валидировать каждый раз |
no-store |
Вообще не кэшировать |
max-age=N |
Время жизни в секундах (для клиента) |
s-maxage=N |
Время жизни для shared кэша (прокси, CDN) |
must-revalidate |
После истечения max-age обязательно валидировать |
immutable |
Ресурс никогда не изменится |
stale-while-revalidate=N |
Отдать stale, пока идёт ревалидация |
Подвох экзамена:
no-cacheНЕ означает "не кэшировать"! Это значит "кэшируй, но каждый раз проверяй актуальность у сервера". Для полного запрета кэширования нуженno-store. Это один из самых частых вопросов.
Кэширование в Symfony
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
// Expiration model — public cache for 1 hour
public function list(): Response
{
$response = $this->render('product/list.html.twig', [
'products' => $products,
]);
$response->setPublic();
$response->setMaxAge(3600); // Browser: 1 hour
$response->setSharedMaxAge(86400); // CDN: 24 hours
return $response;
}
// Private cache — user-specific content
public function dashboard(): Response
{
$response = $this->render('dashboard.html.twig');
$response->setPrivate();
$response->setMaxAge(60); // Only browser, 1 minute
return $response;
}
// No cache at all
public function checkout(): Response
{
$response = $this->render('checkout.html.twig');
$response->headers->addCacheControlDirective('no-store');
return $response;
}
}
ETag (Entity Tag)
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ArticleController extends AbstractController
{
public function show(int $id, Request $request): Response
{
$article = $this->repository->find($id);
// Generate ETag from content hash
$etag = md5($article->getContent() . $article->getUpdatedAt()->getTimestamp());
$response = new Response();
$response->setEtag($etag);
$response->setPublic();
// Check If-None-Match header
if ($response->isNotModified($request)) {
return $response; // 304 Not Modified — no body sent
}
$response->setContent(
$this->renderView('article/show.html.twig', ['article' => $article])
);
return $response;
}
}
Last-Modified
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PageController extends AbstractController
{
public function show(string $slug, Request $request): Response
{
$page = $this->repository->findBySlug($slug);
$lastModified = $page->getUpdatedAt();
$response = new Response();
$response->setLastModified($lastModified);
$response->setPublic();
// Check If-Modified-Since header
if ($response->isNotModified($request)) {
return $response; // 304 — content not changed
}
$response->setContent(
$this->renderView('page/show.html.twig', ['page' => $page])
);
return $response;
}
}
ETag vs Last-Modified
| Критерий | ETag | Last-Modified |
|---|---|---|
| Точность | Byte-level (хэш контента) | Секундная (дата) |
| Заголовок запроса | If-None-Match |
If-Modified-Since |
| Заголовок ответа | ETag |
Last-Modified |
| Тип | Strong / Weak | Только дата |
| Приоритет | Выше (RFC 9110) | Ниже |
| Вычисления | Нужен хэш | Нужна дата |
Подвох экзамена: Если в запросе есть оба заголовка (
If-None-MatchиIf-Modified-Since), сервер ДОЛЖЕН проверить оба, ноETagимеет приоритет по RFC 9110. SymfonyisNotModified()правильно обрабатывает оба.
HTTP Cache в Symfony (Reverse Proxy)
<?php
declare(strict_types=1);
// config/packages/framework.yaml equivalent
// framework:
// http_cache:
// enabled: true
// Symfony includes built-in reverse proxy (HttpCache)
// In public/index.php:
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
$kernel = new HttpCache($kernel);
Symfony 8.0: В Symfony 8.0 HTTP Cache конфигурация упрощена. Встроенный reverse proxy по-прежнему доступен, но рекомендуется Varnish или CDN для production. Cache-тэги и purge API стали стандартной частью FrameworkBundle.
Vary Header
<?php
declare(strict_types=1);
// Vary tells caches that response differs based on certain headers
$response->setVary(['Accept-Encoding', 'Accept-Language']);
// Example: same URL, different content based on Accept header
// GET /api/products Accept: application/json → JSON response
// GET /api/products Accept: text/html → HTML response
// Cache stores SEPARATE versions for each Accept value
Подвох экзамена: Без заголовка
Varyкэш будет отдавать одну версию всем клиентам, даже если они запрашивают разные форматы. Если ответ зависит отAccept,Accept-LanguageилиCookie— обязательно укажитеVary.