vendor/shopware/storefront/Framework/Cache/CacheResponseSubscriber.php line 100

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Storefront\Framework\Cache;
  3. use Shopware\Core\Checkout\Cart\Cart;
  4. use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
  5. use Shopware\Core\Framework\Adapter\Cache\CacheStateSubscriber;
  6. use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
  7. use Shopware\Core\Framework\Log\Package;
  8. use Shopware\Core\PlatformRequest;
  9. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
  10. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  11. use Shopware\Storefront\Framework\Cache\Annotation\HttpCache;
  12. use Shopware\Storefront\Framework\Routing\MaintenanceModeResolver;
  13. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  14. use Symfony\Component\HttpFoundation\Cookie;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\HttpKernel\Event\RequestEvent;
  18. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  19. use Symfony\Component\HttpKernel\KernelEvents;
  20. /**
  21.  * @deprecated tag:v6.5.0 - reason:becomes-internal - EventSubscribers will become internal in v6.5.0
  22.  */
  23. #[Package('storefront')]
  24. class CacheResponseSubscriber implements EventSubscriberInterface
  25. {
  26.     public const STATE_LOGGED_IN CacheStateSubscriber::STATE_LOGGED_IN;
  27.     public const STATE_CART_FILLED CacheStateSubscriber::STATE_CART_FILLED;
  28.     public const CURRENCY_COOKIE 'sw-currency';
  29.     public const CONTEXT_CACHE_COOKIE 'sw-cache-hash';
  30.     public const SYSTEM_STATE_COOKIE 'sw-states';
  31.     public const INVALIDATION_STATES_HEADER 'sw-invalidation-states';
  32.     private const CORE_HTTP_CACHED_ROUTES = [
  33.         'api.acl.privileges.get',
  34.     ];
  35.     private bool $reverseProxyEnabled;
  36.     private CartService $cartService;
  37.     private int $defaultTtl;
  38.     private bool $httpCacheEnabled;
  39.     private MaintenanceModeResolver $maintenanceResolver;
  40.     private ?string $staleWhileRevalidate;
  41.     private ?string $staleIfError;
  42.     /**
  43.      * @internal
  44.      */
  45.     public function __construct(
  46.         CartService $cartService,
  47.         int $defaultTtl,
  48.         bool $httpCacheEnabled,
  49.         MaintenanceModeResolver $maintenanceModeResolver,
  50.         bool $reverseProxyEnabled,
  51.         ?string $staleWhileRevalidate,
  52.         ?string $staleIfError
  53.     ) {
  54.         $this->cartService $cartService;
  55.         $this->defaultTtl $defaultTtl;
  56.         $this->httpCacheEnabled $httpCacheEnabled;
  57.         $this->maintenanceResolver $maintenanceModeResolver;
  58.         $this->reverseProxyEnabled $reverseProxyEnabled;
  59.         $this->staleWhileRevalidate $staleWhileRevalidate;
  60.         $this->staleIfError $staleIfError;
  61.     }
  62.     /**
  63.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  64.      */
  65.     public static function getSubscribedEvents()
  66.     {
  67.         return [
  68.             KernelEvents::REQUEST => 'addHttpCacheToCoreRoutes',
  69.             KernelEvents::RESPONSE => [
  70.                 ['setResponseCache', -1500],
  71.             ],
  72.             BeforeSendResponseEvent::class => 'updateCacheControlForBrowser',
  73.         ];
  74.     }
  75.     public function addHttpCacheToCoreRoutes(RequestEvent $event): void
  76.     {
  77.         $request $event->getRequest();
  78.         $route $request->attributes->get('_route');
  79.         if (\in_array($routeself::CORE_HTTP_CACHED_ROUTEStrue)) {
  80.             $request->attributes->set('_' HttpCache::ALIAS, [new HttpCache([])]);
  81.         }
  82.     }
  83.     public function setResponseCache(ResponseEvent $event): void
  84.     {
  85.         if (!$this->httpCacheEnabled) {
  86.             return;
  87.         }
  88.         $response $event->getResponse();
  89.         $request $event->getRequest();
  90.         if (!$this->maintenanceResolver->shouldBeCached($request)) {
  91.             return;
  92.         }
  93.         $context $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  94.         if (!$context instanceof SalesChannelContext) {
  95.             return;
  96.         }
  97.         $route $request->attributes->get('_route');
  98.         if ($route === 'frontend.checkout.configure') {
  99.             $this->setCurrencyCookie($request$response);
  100.         }
  101.         $cart $this->cartService->getCart($context->getToken(), $context);
  102.         $states $this->updateSystemState($cart$context$request$response);
  103.         // We need to allow it on login, otherwise the state is wrong
  104.         if (!($route === 'frontend.account.login' || $request->getMethod() === Request::METHOD_GET)) {
  105.             return;
  106.         }
  107.         if ($context->getCustomer() || $cart->getLineItems()->count() > 0) {
  108.             $newValue $this->buildCacheHash($context);
  109.             if ($request->cookies->get(self::CONTEXT_CACHE_COOKIE'') !== $newValue) {
  110.                 $cookie Cookie::create(self::CONTEXT_CACHE_COOKIE$newValue);
  111.                 $cookie->setSecureDefault($request->isSecure());
  112.                 $response->headers->setCookie($cookie);
  113.             }
  114.         } elseif ($request->cookies->has(self::CONTEXT_CACHE_COOKIE)) {
  115.             $response->headers->removeCookie(self::CONTEXT_CACHE_COOKIE);
  116.             $response->headers->clearCookie(self::CONTEXT_CACHE_COOKIE);
  117.         }
  118.         $config $request->attributes->get('_' HttpCache::ALIAS);
  119.         if (empty($config)) {
  120.             return;
  121.         }
  122.         /** @var HttpCache $cache */
  123.         $cache array_shift($config);
  124.         if ($this->hasInvalidationState($cache$states)) {
  125.             return;
  126.         }
  127.         $maxAge $cache->getMaxAge() ?? $this->defaultTtl;
  128.         $response->setSharedMaxAge($maxAge);
  129.         $response->headers->addCacheControlDirective('must-revalidate');
  130.         $response->headers->set(
  131.             self::INVALIDATION_STATES_HEADER,
  132.             implode(','$cache->getStates())
  133.         );
  134.         if ($this->staleIfError !== null) {
  135.             $response->headers->addCacheControlDirective('stale-if-error'$this->staleIfError);
  136.         }
  137.         if ($this->staleWhileRevalidate !== null) {
  138.             $response->headers->addCacheControlDirective('stale-while-revalidate'$this->staleWhileRevalidate);
  139.         }
  140.     }
  141.     /**
  142.      * In the default HttpCache implementation the reverse proxy cache is implemented too in PHP and triggered before the response is send to the client. We don't need to send the "real" cache-control headers to the end client (browser/cloudflare).
  143.      * If a external reverse proxy cache is used we still need to provide the actual cache-control, so the external system can cache the system correctly and set the cache-control again to
  144.      */
  145.     public function updateCacheControlForBrowser(BeforeSendResponseEvent $event): void
  146.     {
  147.         if ($this->reverseProxyEnabled) {
  148.             return;
  149.         }
  150.         $response $event->getResponse();
  151.         $noStore $response->headers->getCacheControlDirective('no-store');
  152.         // We don't want that the client will cache the website, if no reverse proxy is configured
  153.         $response->headers->remove('cache-control');
  154.         $response->setPrivate();
  155.         if ($noStore) {
  156.             $response->headers->addCacheControlDirective('no-store');
  157.         } else {
  158.             $response->headers->addCacheControlDirective('no-cache');
  159.         }
  160.     }
  161.     /**
  162.      * @param list<string> $states
  163.      */
  164.     private function hasInvalidationState(HttpCache $cache, array $states): bool
  165.     {
  166.         foreach ($states as $state) {
  167.             if (\in_array($state$cache->getStates(), true)) {
  168.                 return true;
  169.             }
  170.         }
  171.         return false;
  172.     }
  173.     private function buildCacheHash(SalesChannelContext $context): string
  174.     {
  175.         return md5(json_encode([
  176.             $context->getRuleIds(),
  177.             $context->getContext()->getVersionId(),
  178.             $context->getCurrency()->getId(),
  179.             $context->getCustomer() ? 'logged-in' 'not-logged-in',
  180.         ], \JSON_THROW_ON_ERROR));
  181.     }
  182.     /**
  183.      * System states can be used to stop caching routes at certain states. For example,
  184.      * the checkout routes are no longer cached if the customer has products in the cart or is logged in.
  185.      *
  186.      * @return list<string>
  187.      */
  188.     private function updateSystemState(Cart $cartSalesChannelContext $contextRequest $requestResponse $response): array
  189.     {
  190.         $states $this->getSystemStates($request$context$cart);
  191.         if (empty($states)) {
  192.             if ($request->cookies->has(self::SYSTEM_STATE_COOKIE)) {
  193.                 $response->headers->removeCookie(self::SYSTEM_STATE_COOKIE);
  194.                 $response->headers->clearCookie(self::SYSTEM_STATE_COOKIE);
  195.             }
  196.             return [];
  197.         }
  198.         $newStates implode(','$states);
  199.         if ($request->cookies->get(self::SYSTEM_STATE_COOKIE) !== $newStates) {
  200.             $cookie Cookie::create(self::SYSTEM_STATE_COOKIE$newStates);
  201.             $cookie->setSecureDefault($request->isSecure());
  202.             $response->headers->setCookie($cookie);
  203.         }
  204.         return $states;
  205.     }
  206.     /**
  207.      * @return list<string>
  208.      */
  209.     private function getSystemStates(Request $requestSalesChannelContext $contextCart $cart): array
  210.     {
  211.         $states = [];
  212.         $swStates = (string) $request->cookies->get(self::SYSTEM_STATE_COOKIE);
  213.         if ($swStates !== '') {
  214.             $states array_flip(explode(','$swStates));
  215.         }
  216.         $states $this->switchState($statesself::STATE_LOGGED_IN$context->getCustomer() !== null);
  217.         $states $this->switchState($statesself::STATE_CART_FILLED$cart->getLineItems()->count() > 0);
  218.         return array_keys($states);
  219.     }
  220.     /**
  221.      * @param array<string, int|bool> $states
  222.      *
  223.      * @return array<string, int|bool>
  224.      */
  225.     private function switchState(array $statesstring $keybool $match): array
  226.     {
  227.         if ($match) {
  228.             $states[$key] = true;
  229.             return $states;
  230.         }
  231.         unset($states[$key]);
  232.         return $states;
  233.     }
  234.     private function setCurrencyCookie(Request $requestResponse $response): void
  235.     {
  236.         $currencyId $request->get(SalesChannelContextService::CURRENCY_ID);
  237.         if (!$currencyId) {
  238.             return;
  239.         }
  240.         $cookie Cookie::create(self::CURRENCY_COOKIE$currencyId);
  241.         $cookie->setSecureDefault($request->isSecure());
  242.         $response->headers->setCookie($cookie);
  243.     }
  244. }