vendor/symfony/http-kernel/EventListener/CacheAttributeListener.php line 94

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\HttpKernel\EventListener;
  11. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  12. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\HttpKernel\Attribute\Cache;
  16. use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
  17. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  18. use Symfony\Component\HttpKernel\KernelEvents;
  19. /**
  20.  * Handles HTTP cache headers configured via the Cache attribute.
  21.  *
  22.  * @author Fabien Potencier <fabien@symfony.com>
  23.  */
  24. class CacheAttributeListener implements EventSubscriberInterface
  25. {
  26.     /**
  27.      * @var \SplObjectStorage<Request, \DateTimeInterface>
  28.      */
  29.     private \SplObjectStorage $lastModified;
  30.     /**
  31.      * @var \SplObjectStorage<Request, string>
  32.      */
  33.     private \SplObjectStorage $etags;
  34.     public function __construct(
  35.         private ?ExpressionLanguage $expressionLanguage null,
  36.     ) {
  37.         $this->lastModified = new \SplObjectStorage();
  38.         $this->etags = new \SplObjectStorage();
  39.     }
  40.     /**
  41.      * Handles HTTP validation headers.
  42.      */
  43.     public function onKernelControllerArguments(ControllerArgumentsEvent $event)
  44.     {
  45.         $request $event->getRequest();
  46.         if (!\is_array($attributes $request->attributes->get('_cache') ?? $event->getAttributes()[Cache::class] ?? null)) {
  47.             return;
  48.         }
  49.         $request->attributes->set('_cache'$attributes);
  50.         $response null;
  51.         $lastModified null;
  52.         $etag null;
  53.         /** @var Cache[] $attributes */
  54.         foreach ($attributes as $cache) {
  55.             if (null !== $cache->lastModified) {
  56.                 $lastModified $this->getExpressionLanguage()->evaluate($cache->lastModifiedarray_merge($request->attributes->all(), $event->getNamedArguments()));
  57.                 ($response ??= new Response())->setLastModified($lastModified);
  58.             }
  59.             if (null !== $cache->etag) {
  60.                 $etag hash('sha256'$this->getExpressionLanguage()->evaluate($cache->etagarray_merge($request->attributes->all(), $event->getNamedArguments())));
  61.                 ($response ??= new Response())->setEtag($etag);
  62.             }
  63.         }
  64.         if ($response?->isNotModified($request)) {
  65.             $event->setController(static fn () => $response);
  66.             $event->stopPropagation();
  67.             return;
  68.         }
  69.         if (null !== $etag) {
  70.             $this->etags[$request] = $etag;
  71.         }
  72.         if (null !== $lastModified) {
  73.             $this->lastModified[$request] = $lastModified;
  74.         }
  75.     }
  76.     /**
  77.      * Modifies the response to apply HTTP cache headers when needed.
  78.      */
  79.     public function onKernelResponse(ResponseEvent $event)
  80.     {
  81.         $request $event->getRequest();
  82.         /** @var Cache[] $attributes */
  83.         if (!\is_array($attributes $request->attributes->get('_cache'))) {
  84.             return;
  85.         }
  86.         $response $event->getResponse();
  87.         // http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1
  88.         if (!\in_array($response->getStatusCode(), [200203300301302304404410])) {
  89.             unset($this->lastModified[$request]);
  90.             unset($this->etags[$request]);
  91.             return;
  92.         }
  93.         if (isset($this->lastModified[$request]) && !$response->headers->has('Last-Modified')) {
  94.             $response->setLastModified($this->lastModified[$request]);
  95.         }
  96.         if (isset($this->etags[$request]) && !$response->headers->has('Etag')) {
  97.             $response->setEtag($this->etags[$request]);
  98.         }
  99.         unset($this->lastModified[$request]);
  100.         unset($this->etags[$request]);
  101.         $hasVary $response->headers->has('Vary');
  102.         foreach (array_reverse($attributes) as $cache) {
  103.             if (null !== $cache->smaxage && !$response->headers->hasCacheControlDirective('s-maxage')) {
  104.                 $response->setSharedMaxAge($this->toSeconds($cache->smaxage));
  105.             }
  106.             if ($cache->mustRevalidate) {
  107.                 $response->headers->addCacheControlDirective('must-revalidate');
  108.             }
  109.             if (null !== $cache->maxage && !$response->headers->hasCacheControlDirective('max-age')) {
  110.                 $response->setMaxAge($this->toSeconds($cache->maxage));
  111.             }
  112.             if (null !== $cache->maxStale && !$response->headers->hasCacheControlDirective('max-stale')) {
  113.                 $response->headers->addCacheControlDirective('max-stale'$this->toSeconds($cache->maxStale));
  114.             }
  115.             if (null !== $cache->staleWhileRevalidate && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) {
  116.                 $response->headers->addCacheControlDirective('stale-while-revalidate'$this->toSeconds($cache->staleWhileRevalidate));
  117.             }
  118.             if (null !== $cache->staleIfError && !$response->headers->hasCacheControlDirective('stale-if-error')) {
  119.                 $response->headers->addCacheControlDirective('stale-if-error'$this->toSeconds($cache->staleIfError));
  120.             }
  121.             if (null !== $cache->expires && !$response->headers->has('Expires')) {
  122.                 $response->setExpires(new \DateTimeImmutable('@'.strtotime($cache->expirestime())));
  123.             }
  124.             if (!$hasVary && $cache->vary) {
  125.                 $response->setVary($cache->varyfalse);
  126.             }
  127.         }
  128.         foreach ($attributes as $cache) {
  129.             if (true === $cache->public) {
  130.                 $response->setPublic();
  131.             }
  132.             if (false === $cache->public) {
  133.                 $response->setPrivate();
  134.             }
  135.         }
  136.     }
  137.     public static function getSubscribedEvents(): array
  138.     {
  139.         return [
  140.             KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments'10],
  141.             KernelEvents::RESPONSE => ['onKernelResponse', -10],
  142.         ];
  143.     }
  144.     private function getExpressionLanguage(): ExpressionLanguage
  145.     {
  146.         return $this->expressionLanguage ??= class_exists(ExpressionLanguage::class)
  147.             ? new ExpressionLanguage()
  148.             : throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
  149.     }
  150.     private function toSeconds(int|string $time): int
  151.     {
  152.         if (!is_numeric($time)) {
  153.             $now time();
  154.             $time strtotime($time$now) - $now;
  155.         }
  156.         return $time;
  157.     }
  158. }