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

Open in your IDE?
  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(), [200, 203, 300, 301, 302, 304, 404, 410])) {
  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. }