Концепция HTTP-кеширования
HTTP-кеширование позволяет повторно использовать ранее полученные ответы. Существует два основных механизма: expiration (истечение) и validation (проверка).
В экзамене Sf8 ESI (Edge Side Includes) НЕ входят в темы. Фокус на базовых механизмах кеширования.
Типы кешей
| Тип | Расположение | Описание |
|---|---|---|
| Browser cache | Клиент | Кеш браузера пользователя |
| Shared proxy cache | Сеть | CDN, Varnish, Symfony reverse proxy |
| Gateway cache | Сервер | Перед приложением |
Expiration Model
Expiration говорит кешу, как долго ответ считается свежим. До истечения срока кеш не обращается к серверу.
Cache-Control
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
final class ProductController extends AbstractController
{
public function list(): Response
{
$response = $this->render('product/list.html.twig', [
'products' => $this->getProducts(),
]);
// Public cache: can be cached by CDN/proxy
$response->setPublic();
$response->setMaxAge(3600); // 1 hour for proxies
$response->headers->set(
'Cache-Control',
'public, max-age=3600, s-maxage=7200'
);
return $response;
}
public function profile(): Response
{
$response = $this->render('user/profile.html.twig');
// Private cache: only browser cache, not CDN
$response->setPrivate();
$response->setMaxAge(600); // 10 minutes
return $response;
}
public function checkout(): Response
{
$response = $this->render('checkout/index.html.twig');
// No cache at all
$response->headers->set(
'Cache-Control',
'no-cache, no-store, must-revalidate'
);
return $response;
}
}
Cache-Control директивы
| Директива | Описание |
|---|---|
public |
Может кешироваться прокси и CDN |
private |
Только браузерный кеш (per-user) |
max-age=N |
Срок в секундах для browser cache |
s-maxage=N |
Срок в секундах для shared cache (proxy/CDN) |
no-cache |
Всегда проверять у сервера (валидация) |
no-store |
Не кешировать вообще |
must-revalidate |
Обязательно проверять после истечения |
stale-while-revalidate=N |
Отдавать устаревший ответ, пока ревалидация |
Expires Header
<?php
declare(strict_types=1);
$response->setExpires(new \DateTime('+1 hour'));
// Or specific date
$response->setExpires(new \DateTime('2025-12-31 23:59:59'));
Подвох экзамена:
Cache-Controlимеет приоритет надExpires. Если оба установлены,Cache-Controlпобеждает.Expires-- устаревший заголовок,Cache-Control-- предпочтительный.
Validation Model
Validation проверяет, изменился ли ресурс. Сервер возвращает 304 Not Modified, если ресурс не изменился -- без тела ответа.
ETag
ETag -- хеш содержимого. Клиент отправляет If-None-Match, сервер сравнивает.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class ArticleController extends AbstractController
{
public function show(Request $request, Article $article): Response
{
$response = new Response();
// Generate ETag from content hash
$etag = md5($article->getContent() . $article->getUpdatedAt()->getTimestamp());
$response->setEtag($etag);
$response->setPublic();
// Check if client has current version
if ($response->isNotModified($request)) {
// Return 304 Not Modified (no body)
return $response;
}
// Content changed -- render full response
$response->setContent(
$this->renderView('article/show.html.twig', [
'article' => $article,
])
);
return $response;
}
}
Last-Modified
Last-Modified -- дата последнего изменения. Клиент отправляет If-Modified-Since.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class BlogController extends AbstractController
{
public function index(Request $request): Response
{
// Get latest modification date
$latestPost = $this->postRepository->findLatest();
$lastModified = $latestPost->getUpdatedAt();
$response = new Response();
$response->setLastModified($lastModified);
$response->setPublic();
// Check if client already has current version
if ($response->isNotModified($request)) {
return $response; // 304 Not Modified
}
// Render full response
$response->setContent(
$this->renderView('blog/index.html.twig', [
'posts' => $this->postRepository->findAll(),
])
);
return $response;
}
}
Комбинирование ETag и Last-Modified
<?php
declare(strict_types=1);
namespace App\Controller;
final class CatalogController extends AbstractController
{
public function show(Request $request, int $id): Response
{
$catalog = $this->catalogRepository->find($id);
$response = new Response();
$response->setPublic();
$response->setMaxAge(3600);
// Both ETag and Last-Modified
$response->setEtag(md5($catalog->getVersion()));
$response->setLastModified($catalog->getUpdatedAt());
if ($response->isNotModified($request)) {
return $response;
}
$response->setContent(
$this->renderView('catalog/show.html.twig', [
'catalog' => $catalog,
])
);
return $response;
}
}
#[Cache] атрибут
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpKernel\Attribute\Cache;
final class StaticController extends AbstractController
{
#[Cache(maxage: 3600, public: true, smaxage: 7200)]
public function termsOfService(): Response
{
return $this->render('static/terms.html.twig');
}
#[Cache(maxage: 86400, public: true)]
public function about(): Response
{
return $this->render('static/about.html.twig');
}
#[Cache(maxage: 0, public: false)]
public function dashboard(): Response
{
return $this->render('dashboard/index.html.twig');
}
}
Reverse Proxy (HttpCache)
Symfony включает встроенный reverse proxy.
<?php
declare(strict_types=1);
// public/index.php
use App\Kernel;
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel(
$context['APP_ENV'],
(bool) $context['APP_DEBUG'],
);
// Wrap kernel with HttpCache reverse proxy
if ('prod' === $context['APP_ENV']) {
$kernel = new \Symfony\Component\HttpKernel\HttpCache\HttpCache(
$kernel,
new \Symfony\Component\HttpKernel\HttpCache\Store(
__DIR__ . '/../var/cache/http_cache'
),
);
}
return $kernel;
};
Vary Header
Vary указывает, какие заголовки запроса влияют на кешированный ответ.
<?php
declare(strict_types=1);
$response->setVary(['Accept-Encoding', 'Accept-Language']);
// Cache stores different versions for different Accept-Encoding/Language
Итоги
- Expiration:
Cache-Control(max-age, s-maxage, public/private) - Validation:
ETag(хеш) иLast-Modified(дата) isNotModified($request)возвращает 304 без тела ответаCache-Controlимеет приоритет надExpirespublic-- кешируется CDN/proxy,private-- только браузер#[Cache]атрибут для декларативного кеширования- Vary header определяет варианты кеша по заголовкам запроса