指尖上的记忆指尖上的记忆
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub

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️⃣ 当前代码做了什么
  1. 检查缓存命中 (isHit())。

  2. 如果命中:

    • 从缓存池拿出之前存的 Response 对象 ($cachedItem->get())。
    • 调用 $event->setResponse($response),将这个 Response 设置给事件。
  3. return:当前监听器不再继续处理,跳过下面代码逻辑(例如 _request_cache 注入)。

2️⃣ Symfony 后续请求处理流程

在 Symfony 中,请求处理大致顺序:

  1. kernel.request → 所有 RequestListener 被调用
  2. kernel.controller → 调用 Controller
  3. Controller 返回 Response
  4. kernel.view → 如果 Controller 没返回 Response,可处理
  5. kernel.response → 所有 ResponseListener 被调用
  6. 发送 Response 给客户端
情况 A:缓存命中
  • 已经 setResponse($response),Symfony 会 短路 Controller:
    • 不再执行 Controller 方法
    • 不会触发 kernel.controller
    • 不会生成新的 Response
  • 但 仍会触发 kernel.response,因为 Symfony 需要把 Response 发出去。
  • 在 onKernelResponse 可以对这个缓存 Response 做一些操作,例如:
    • 添加 X-Cache header(可以改为 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;
}