symfony7响应缓存的使用: 目前使用symfony7做后台API的开发,部分接口会做响应缓存,基于 Caching Interface, 这个是PSR-6的标准: https://www.php-fig.org/psr/psr-6/
关于php的标准(PSR:PHP Standards Recommendations): https://www.php-fig.org/
//cache配置
config/packages/cache.yaml
framework:
cache:
app: cache.adapter.filesystem
system: cache.adapter.system
directory: '%kernel.cache_dir%/pools'
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
//定义cache的listener
congig/services.yaml
App\EventListener\RequestCacheListener:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
<?php
namespace App\EventListener;
use App\Attribute\RequestCache;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelInterface;
class RequestCacheListener
{
public function __construct(
private CacheItemPoolInterface $requestCachePool,
private readonly KernelInterface $kernel,
) {
}
//这个方法在 request时会被调用,不管有没有缓存
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest() || !in_array($this->kernel->getEnvironment(), ["prod", "staging"])) {
return;
}
$request = $event->getRequest();
$controller = $request->attributes->get('_controller');
$controllerParts = explode('::', $controller);
if (2 !== count($controllerParts)) {
return;
}
$controllerClass = $controllerParts[0];
$controllerMethod = $controllerParts[1];
try {
$reflectionController = new \ReflectionMethod($controllerClass, $controllerMethod);
$requestCache = $reflectionController->getAttributes(RequestCache::class, \ReflectionAttribute::IS_INSTANCEOF); // 通过定义的指定attribute来决定缓存类型
} catch (\ReflectionException $e) {
return;
}
if (empty($requestCache)) {
return;
}
$requestCache = $requestCache[0]->newInstance();
$cacheKey = md5($request->getUri());
$cachedItem = $this->requestCachePool->getItem($cacheKey);
if ($cachedItem->isHit()) {
$response = $cachedItem->get();
$event->setResponse($response);
return;
}
$event->getRequest()->attributes->set('_request_cache', $requestCache);
}
//这个方法在 response时会被调用,不管有没有缓存
public function onKernelResponse(ResponseEvent $event): void
{
$requestCache = $event->getRequest()->attributes->get('_request_cache');
if (null === $requestCache) {
$response = $event->getResponse();
$response->headers->set('X-Cache', 'Hit');
return;
}
$response = $event->getResponse();
$cacheKey = md5($event->getRequest()->getUri());
$cacheTime = $requestCache->expirationTime;
// cache for 5s only for errors
if (500 === $response->getStatusCode()) {
$cacheTime = 5;
}
$response->headers->add(['X-Cache' => 'Miss']);
$response->headers->addCacheControlDirective('public');
$response->setMaxAge($cacheTime);
$cachedItem = $this->requestCachePool->getItem($cacheKey);
$cachedItem->set($response); // 这里将整个响应结果缓存,比如有些接口会返回 国家 类型等列表,可以进行缓存
$cachedItem->expiresAfter($cacheTime);
$this->requestCachePool->save($cachedItem);
}
}
//调用cache
//先定义一个attribute
<?php
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_METHOD)]
class RequestCache
{
public function __construct(
public int $expirationTime = 3600 // Default expiration time in seconds
) {
}
}
//控制器下使用这个attribute
use App\Attribute\RequestCache;
#[RequestCache(expirationTime: 86400)]
public function listEventStatuses(
Service\EventService $eventService
): JsonResponse {
return new ApiSuccessResponse($eventService->getEventStatuses());
}
//清理cache, CacheService.php, 我认为这个也可以做一个listener来控制什么时候清理缓存
public function __construct(
private readonly CacheItemPoolInterface $requestCachePool,
) {
}
public function clear(){
$this->requestCachePool->deleteItem("f2e644c1ffe9be69e85325ed0e746c97"); // 清理指定见
$this->requestCachePool->clear(); // 清理所有
}
对如下缓存代码的分析:
if ($cachedItem->isHit()) {
$response = $cachedItem->get();
$event->setResponse($response);
return;
}
好,我们仔细分析一下这段关键代码在 Symfony 生命周期中的作用以及之后的流程:
if ($cachedItem->isHit()) {
$response = $cachedItem->get();
$event->setResponse($response);
return;
}
1️⃣ 当前代码做了什么
检查缓存命中 (
isHit())。如果命中:
- 从缓存池拿出之前存的 Response 对象 (
$cachedItem->get())。 - 调用
$event->setResponse($response),将这个 Response 设置给事件。
- 从缓存池拿出之前存的 Response 对象 (
return:当前监听器不再继续处理,跳过下面代码逻辑(例如_request_cache注入)。
2️⃣ Symfony 后续请求处理流程
在 Symfony 中,请求处理大致顺序:
- kernel.request → 所有 RequestListener 被调用
- kernel.controller → 调用 Controller
- Controller 返回 Response
- kernel.view → 如果 Controller 没返回 Response,可处理
- kernel.response → 所有 ResponseListener 被调用
- 发送 Response 给客户端
情况 A:缓存命中
- 已经
setResponse($response),Symfony 会 短路 Controller:- 不再执行 Controller 方法
- 不会触发 kernel.controller
- 不会生成新的 Response
- 但 仍会触发 kernel.response,因为 Symfony 需要把 Response 发出去。
- 在
onKernelResponse可以对这个缓存 Response 做一些操作,例如:- 添加
X-Cacheheader(可以改为Hit) - 设置 cache-control 等
- 添加
- 最终这个缓存 Response 被发送给客户端。
情况 B:缓存未命中
- 代码执行到
return之外,_request_cache被写入 request attributes。 - Controller 会正常执行生成 Response。
onKernelResponse拿到_request_cache:- 将 Response 写入缓存池
- 设置缓存时间、headers
- 下次同样请求就能命中缓存。
3️⃣ 总结:缓存命中后的“请求干什么”
| 步骤 | 动作 |
|---|---|
| kernel.request | 检查缓存,命中 → setResponse,Controller 不执行 |
| kernel.controller | 被跳过 |
| Controller | 不执行 |
| kernel.response | 仍然触发,可以对缓存 Response 做 header、Cache-Control 等处理 |
| 客户端接收 Response | 完整的缓存响应,包括 body、status、headers |
✅ 关键点:缓存命中后,整个请求不会再执行 Controller,但 Symfony 会正常触发 Response 事件并发送 Response。
这个缓存到底缓存的什么内容:
$cachedItem = $this->requestCachePool->getItem($cacheKey);
$cachedItem->set($response);
$cachedItem->expiresAfter($cacheTime);
$this->requestCachePool->save($cachedItem);
关键点 1:缓存的对象是 Symfony Response
set($response)这里直接把$response对象写入缓存。- 也就是说,缓存里存的是 整个
Symfony\Component\HttpFoundation\Response对象,包含:- HTTP 状态码(200、500 等)
- headers(包括你加的
X-Cache) - 内容 body(HTML、JSON 等)
- cookies 等信息
所以并不是只缓存一个简单的字符串或者数组,而是整个响应对象。
关键点 2:缓存读取时直接返回对象
在 onKernelRequest:
if ($cachedItem->isHit()) {
$response = $cachedItem->get();
$event->setResponse($response);
return;
}
get()拿出来的就是之前存的Response对象。- 直接
setResponse($response),请求就被短路,浏览器收到缓存内容,和正常生成响应完全一致。 - 所以你没看到“单独 set 值”,实际上
$response就是缓存的完整内容。
关键点 3:缓存策略
- 缓存按 URI:
$cacheKey = md5($request->getUri()),同一 URL 命中同一缓存。 - 缓存时间可控:
- 默认:注解里的
expirationTime - 错误(500):强制 5 秒
- 默认:注解里的
✅ 总结一句话:
缓存的不是单独的数据字段,而是整个
Response对象,包括状态码、headers 和 body。读取缓存时直接返回这个 Response,对客户端来说就像正常生成的一样。
小优化:
if ($cachedItem->isHit()) {
$response = $cachedItem->get();
$response->headers->set('X-Cache', 'Hit');
$event->setResponse($response);
return;
}
