vendor/sensio/framework-extra-bundle/src/EventListener/HttpCacheListener.php line 42

  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 Sensio\Bundle\FrameworkExtraBundle\EventListener;
  11. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\HttpKernel\Event\KernelEvent;
  16. use Symfony\Component\HttpKernel\KernelEvents;
  17. /**
  18.  * HttpCacheListener handles HTTP cache headers.
  19.  *
  20.  * It can be configured via the Cache annotation.
  21.  *
  22.  * @author Fabien Potencier <fabien@symfony.com>
  23.  */
  24. class HttpCacheListener implements EventSubscriberInterface
  25. {
  26.     private $lastModifiedDates;
  27.     private $etags;
  28.     private $expressionLanguage;
  29.     public function __construct()
  30.     {
  31.         $this->lastModifiedDates = new \SplObjectStorage();
  32.         $this->etags = new \SplObjectStorage();
  33.     }
  34.     /**
  35.      * Handles HTTP validation headers.
  36.      */
  37.     public function onKernelController(KernelEvent $event)
  38.     {
  39.         $request $event->getRequest();
  40.         $configuration $request->attributes->get('_cache');
  41.         if (!$configuration instanceof Cache) {
  42.             return;
  43.         }
  44.         $response = new Response();
  45.         $lastModifiedDate '';
  46.         if ($configuration->getLastModified()) {
  47.             $lastModifiedDate $this->getExpressionLanguage()->evaluate($configuration->getLastModified(), $request->attributes->all());
  48.             $response->setLastModified($lastModifiedDate);
  49.         }
  50.         $etag '';
  51.         if ($configuration->getEtag()) {
  52.             $etag hash('sha256'$this->getExpressionLanguage()->evaluate($configuration->getEtag(), $request->attributes->all()));
  53.             $response->setEtag($etag);
  54.         }
  55.         if ($response->isNotModified($request)) {
  56.             $event->setController(function () use ($response) {
  57.                 return $response;
  58.             });
  59.             $event->stopPropagation();
  60.         } else {
  61.             if ($etag) {
  62.                 $this->etags[$request] = $etag;
  63.             }
  64.             if ($lastModifiedDate) {
  65.                 $this->lastModifiedDates[$request] = $lastModifiedDate;
  66.             }
  67.         }
  68.     }
  69.     /**
  70.      * Modifies the response to apply HTTP cache headers when needed.
  71.      */
  72.     public function onKernelResponse(KernelEvent $event)
  73.     {
  74.         $request $event->getRequest();
  75.         $configuration $request->attributes->get('_cache');
  76.         if (!$configuration instanceof Cache) {
  77.             return;
  78.         }
  79.         $response $event->getResponse();
  80.         // http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1
  81.         if (!\in_array($response->getStatusCode(), [200203300301302304404410])) {
  82.             return;
  83.         }
  84.         if (!$response->headers->hasCacheControlDirective('s-maxage') && null !== $age $configuration->getSMaxAge()) {
  85.             $age $this->convertToSecondsIfNeeded($age);
  86.             $response->setSharedMaxAge($age);
  87.         }
  88.         if ($configuration->mustRevalidate()) {
  89.             $response->headers->addCacheControlDirective('must-revalidate');
  90.         }
  91.         if (!$response->headers->hasCacheControlDirective('max-age') && null !== $age $configuration->getMaxAge()) {
  92.             $age $this->convertToSecondsIfNeeded($age);
  93.             $response->setMaxAge($age);
  94.         }
  95.         if (!$response->headers->hasCacheControlDirective('max-stale') && null !== $stale $configuration->getMaxStale()) {
  96.             $stale $this->convertToSecondsIfNeeded($stale);
  97.             $response->headers->addCacheControlDirective('max-stale'$stale);
  98.         }
  99.         if (!$response->headers->hasCacheControlDirective('stale-while-revalidate') && null !== $staleWhileRevalidate $configuration->getStaleWhileRevalidate()) {
  100.             $staleWhileRevalidate $this->convertToSecondsIfNeeded($staleWhileRevalidate);
  101.             $response->headers->addCacheControlDirective('stale-while-revalidate'$staleWhileRevalidate);
  102.         }
  103.         if (!$response->headers->hasCacheControlDirective('stale-if-error') && null !== $staleIfError $configuration->getStaleIfError()) {
  104.             $staleIfError $this->convertToSecondsIfNeeded($staleIfError);
  105.             $response->headers->addCacheControlDirective('stale-if-error'$staleIfError);
  106.         }
  107.         if (!$response->headers->has('Expires') && null !== $configuration->getExpires()) {
  108.             $date \DateTime::createFromFormat('U'strtotime($configuration->getExpires()), new \DateTimeZone('UTC'));
  109.             $response->setExpires($date);
  110.         }
  111.         if (!$response->headers->has('Vary') && null !== $configuration->getVary()) {
  112.             $response->setVary($configuration->getVary());
  113.         }
  114.         if ($configuration->isPublic()) {
  115.             $response->setPublic();
  116.         }
  117.         if ($configuration->isPrivate()) {
  118.             $response->setPrivate();
  119.         }
  120.         if (!$response->headers->has('Last-Modified') && isset($this->lastModifiedDates[$request])) {
  121.             $response->setLastModified($this->lastModifiedDates[$request]);
  122.             unset($this->lastModifiedDates[$request]);
  123.         }
  124.         if (!$response->headers->has('Etag') && isset($this->etags[$request])) {
  125.             $response->setEtag($this->etags[$request]);
  126.             unset($this->etags[$request]);
  127.         }
  128.     }
  129.     /**
  130.      * @return array
  131.      */
  132.     public static function getSubscribedEvents()
  133.     {
  134.         return [
  135.             KernelEvents::CONTROLLER => 'onKernelController',
  136.             KernelEvents::RESPONSE => 'onKernelResponse',
  137.         ];
  138.     }
  139.     private function getExpressionLanguage()
  140.     {
  141.         if (null === $this->expressionLanguage) {
  142.             if (!class_exists(ExpressionLanguage::class)) {
  143.                 throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
  144.             }
  145.             $this->expressionLanguage = new ExpressionLanguage();
  146.         }
  147.         return $this->expressionLanguage;
  148.     }
  149.     /**
  150.      * @param int|string $time Time that can be either expressed in seconds or with relative time format (1 day, 2 weeks, ...)
  151.      *
  152.      * @return int
  153.      */
  154.     private function convertToSecondsIfNeeded($time)
  155.     {
  156.         if (!is_numeric($time)) {
  157.             $now microtime(true);
  158.             $time ceil(strtotime($time, (int) $now) - $now);
  159.         }
  160.         return $time;
  161.     }
  162. }