platform/src/Storefront/Framework/Cache/CacheResponseSubscriber.php line 75

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