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

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\PlatformRequest;
  6. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
  7. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  8. use Shopware\Storefront\Framework\Cache\Annotation\HttpCache;
  9. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  10. use Symfony\Component\HttpFoundation\Cookie;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Symfony\Component\HttpFoundation\Response;
  13. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  14. use Symfony\Component\HttpKernel\KernelEvents;
  15. class CacheResponseSubscriber implements EventSubscriberInterface
  16. {
  17.     public const STATE_LOGGED_IN = 'logged-in';
  18.     public const STATE_CART_FILLED = 'cart-filled';
  19.     public const CURRENCY_COOKIE = 'sw-currency';
  20.     public const CONTEXT_CACHE_COOKIE = 'sw-cache-hash';
  21.     public const SYSTEM_STATE_COOKIE = 'sw-states';
  22.     public const INVALIDATION_STATES_HEADER = 'sw-invalidation-states';
  23.     /**
  24.      * @var CartService
  25.      */
  26.     private $cartService;
  27.     /**
  28.      * @var int
  29.      */
  30.     private $defaultTtl;
  31.     /**
  32.      * @var bool
  33.      */
  34.     private $httpCacheEnabled;
  35.     public function __construct(CartService $cartService, int $defaultTtl, bool $httpCacheEnabled)
  36.     {
  37.         $this->cartService = $cartService;
  38.         $this->defaultTtl = $defaultTtl;
  39.         $this->httpCacheEnabled = $httpCacheEnabled;
  40.     }
  41.     public static function getSubscribedEvents()
  42.     {
  43.         return [
  44.             KernelEvents::RESPONSE => [
  45.                 ['setResponseCache', -1500],
  46.             ],
  47.         ];
  48.     }
  49.     public function setResponseCache(ResponseEvent $event): void
  50.     {
  51.         if (!$this->httpCacheEnabled) {
  52.             return;
  53.         }
  54.         $response = $event->getResponse();
  55.         $request = $event->getRequest();
  56.         $context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  57.         if (!$context instanceof SalesChannelContext) {
  58.             return;
  59.         }
  60.         $route = $request->attributes->get('_route');
  61.         if ($route === 'frontend.checkout.configure') {
  62.             $this->setCurrencyCookie($request, $response);
  63.         }
  64.         $cart = $this->cartService->getCart($context->getToken(), $context);
  65.         /* @var SalesChannelContext $context */
  66.         $states = $this->updateSystemState($cart, $context, $request, $response);
  67.         if ($request->getMethod() !== Request::METHOD_GET) {
  68.             return;
  69.         }
  70.         if ($context->getCustomer() || $cart->getLineItems()->count() > 0) {
  71.             $cookie = Cookie::create(self::CONTEXT_CACHE_COOKIE, $this->buildCacheHash($context));
  72.             $cookie->setSecureDefault($request->isSecure());
  73.             $response->headers->setCookie($cookie);
  74.         } else {
  75.             $response->headers->removeCookie(self::CONTEXT_CACHE_COOKIE);
  76.             $response->headers->clearCookie(self::CONTEXT_CACHE_COOKIE);
  77.         }
  78.         $config = $request->attributes->get('_' . HttpCache::ALIAS);
  79.         if (empty($config)) {
  80.             return;
  81.         }
  82.         /** @var HttpCache $cache */
  83.         $cache = array_shift($config);
  84.         if ($this->hasInvalidationState($cache, $states)) {
  85.             return;
  86.         }
  87.         $maxAge = $cache->getMaxAge() ?? $this->defaultTtl;
  88.         $maxAge = $maxAge ?? 3600;
  89.         $response->setSharedMaxAge($maxAge);
  90.         $response->headers->addCacheControlDirective('must-revalidate');
  91.         $response->headers->set(
  92.             self::INVALIDATION_STATES_HEADER,
  93.             implode(',', $cache->getStates())
  94.         );
  95.     }
  96.     private function hasInvalidationState(HttpCache $cache, array $states): bool
  97.     {
  98.         foreach ($states as $state) {
  99.             if (\in_array($state, $cache->getStates(), true)) {
  100.                 return true;
  101.             }
  102.         }
  103.         return false;
  104.     }
  105.     private function buildCacheHash(SalesChannelContext $context): string
  106.     {
  107.         return md5(json_encode([
  108.             $context->getRuleIds(),
  109.             $context->getContext()->getVersionId(),
  110.             $context->getCurrency()->getId(),
  111.         ]));
  112.     }
  113.     /**
  114.      * System states can be used to stop caching routes at certain states. For example,
  115.      * the checkout routes are no longer cached if the customer has products in the cart or is logged in.
  116.      */
  117.     private function updateSystemState(Cart $cart, SalesChannelContext $context, Request $request, Response $response): array
  118.     {
  119.         $states = $this->getSystemStates($request, $context, $cart);
  120.         if (empty($states)) {
  121.             $response->headers->removeCookie(self::SYSTEM_STATE_COOKIE);
  122.             $response->headers->clearCookie(self::SYSTEM_STATE_COOKIE);
  123.             return [];
  124.         }
  125.         $cookie = Cookie::create(self::SYSTEM_STATE_COOKIE, implode(',', $states));
  126.         $cookie->setSecureDefault($request->isSecure());
  127.         $response->headers->setCookie($cookie);
  128.         return $states;
  129.     }
  130.     private function getSystemStates(Request $request, SalesChannelContext $context, Cart $cart): array
  131.     {
  132.         $states = [];
  133.         if ($request->cookies->has(self::SYSTEM_STATE_COOKIE)) {
  134.             $states = explode(',', $request->cookies->get(self::SYSTEM_STATE_COOKIE));
  135.             $states = array_flip($states);
  136.         }
  137.         $states = $this->switchState($states, self::STATE_LOGGED_IN, $context->getCustomer() !== null);
  138.         $states = $this->switchState($states, self::STATE_CART_FILLED, $cart->getLineItems()->count() > 0);
  139.         return array_keys($states);
  140.     }
  141.     private function switchState(array $states, string $key, bool $match): array
  142.     {
  143.         if ($match) {
  144.             $states[$key] = true;
  145.             return $states;
  146.         }
  147.         unset($states[$key]);
  148.         return $states;
  149.     }
  150.     private function setCurrencyCookie(Request $request, Response $response): void
  151.     {
  152.         $currencyId = $request->get(SalesChannelContextService::CURRENCY_ID);
  153.         if (!$currencyId) {
  154.             return;
  155.         }
  156.         $cookie = Cookie::create(self::CURRENCY_COOKIE, $currencyId);
  157.         $cookie->setSecureDefault($request->isSecure());
  158.         $response->headers->setCookie($cookie);
  159.     }
  160. }