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

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