Passed
Push — 6.4.9.0 ( 572988...1aef59 )
by
unknown
28:27 queued 17:47
created

CacheResponseSubscriber::getSubscribedEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Storefront\Framework\Cache;
4
5
use Shopware\Core\Checkout\Cart\Cart;
6
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
7
use Shopware\Core\Framework\Adapter\Cache\CacheStateSubscriber;
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
class CacheResponseSubscriber implements EventSubscriberInterface
22
{
23
    public const STATE_LOGGED_IN = CacheStateSubscriber::STATE_LOGGED_IN;
24
    public const STATE_CART_FILLED = CacheStateSubscriber::STATE_CART_FILLED;
25
26
    public const CURRENCY_COOKIE = 'sw-currency';
27
    public const CONTEXT_CACHE_COOKIE = 'sw-cache-hash';
28
    public const SYSTEM_STATE_COOKIE = 'sw-states';
29
    public const INVALIDATION_STATES_HEADER = 'sw-invalidation-states';
30
31
    private const CORE_HTTP_CACHED_ROUTES = [
32
        'api.acl.privileges.get',
33
    ];
34
35
    private CartService $cartService;
36
37
    private int $defaultTtl;
38
39
    private bool $httpCacheEnabled;
40
41
    private MaintenanceModeResolver $maintenanceResolver;
42
43
    public function __construct(
44
        CartService $cartService,
45
        int $defaultTtl,
46
        bool $httpCacheEnabled,
47
        MaintenanceModeResolver $maintenanceModeResolver
48
    ) {
49
        $this->cartService = $cartService;
50
        $this->defaultTtl = $defaultTtl;
51
        $this->httpCacheEnabled = $httpCacheEnabled;
52
        $this->maintenanceResolver = $maintenanceModeResolver;
53
    }
54
55
    /**
56
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, string|arr...y{0: string, 1?: int}>> at position 17 could not be parsed: Expected '>' at position 17, but found 'list'.
Loading history...
57
     */
58
    public static function getSubscribedEvents()
59
    {
60
        return [
61
            KernelEvents::REQUEST => 'addHttpCacheToCoreRoutes',
62
            KernelEvents::RESPONSE => [
63
                ['setResponseCache', -1500],
64
            ],
65
        ];
66
    }
67
68
    public function addHttpCacheToCoreRoutes(RequestEvent $event): void
69
    {
70
        $request = $event->getRequest();
71
        $route = $request->attributes->get('_route');
72
73
        if (\in_array($route, self::CORE_HTTP_CACHED_ROUTES, true)) {
74
            $request->attributes->set('_' . HttpCache::ALIAS, [new HttpCache([])]);
75
        }
76
    }
77
78
    public function setResponseCache(ResponseEvent $event): void
79
    {
80
        if (!$this->httpCacheEnabled) {
81
            return;
82
        }
83
84
        $response = $event->getResponse();
85
86
        $request = $event->getRequest();
87
88
        if ($this->maintenanceResolver->isMaintenanceRequest($request)) {
89
            return;
90
        }
91
92
        $context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
93
94
        if (!$context instanceof SalesChannelContext) {
95
            return;
96
        }
97
98
        $route = $request->attributes->get('_route');
99
        if ($route === 'frontend.checkout.configure') {
100
            $this->setCurrencyCookie($request, $response);
101
        }
102
103
        $cart = $this->cartService->getCart($context->getToken(), $context);
104
105
        $states = $this->updateSystemState($cart, $context, $request, $response);
106
107
        if ($request->getMethod() !== Request::METHOD_GET) {
108
            return;
109
        }
110
111
        if ($context->getCustomer() || $cart->getLineItems()->count() > 0) {
112
            $cookie = Cookie::create(self::CONTEXT_CACHE_COOKIE, $this->buildCacheHash($context));
113
            $cookie->setSecureDefault($request->isSecure());
114
115
            $response->headers->setCookie($cookie);
116
        } else {
117
            $response->headers->removeCookie(self::CONTEXT_CACHE_COOKIE);
118
            $response->headers->clearCookie(self::CONTEXT_CACHE_COOKIE);
119
        }
120
121
        $config = $request->attributes->get('_' . HttpCache::ALIAS);
122
        if (empty($config)) {
123
            return;
124
        }
125
126
        /** @var HttpCache $cache */
127
        $cache = array_shift($config);
128
129
        if ($this->hasInvalidationState($cache, $states)) {
130
            return;
131
        }
132
133
        $maxAge = $cache->getMaxAge() ?? $this->defaultTtl;
134
135
        $response->setSharedMaxAge($maxAge);
136
        $response->headers->addCacheControlDirective('must-revalidate');
137
        $response->headers->set(
138
            self::INVALIDATION_STATES_HEADER,
139
            implode(',', $cache->getStates())
140
        );
141
    }
142
143
    private function hasInvalidationState(HttpCache $cache, array $states): bool
144
    {
145
        foreach ($states as $state) {
146
            if (\in_array($state, $cache->getStates(), true)) {
147
                return true;
148
            }
149
        }
150
151
        return false;
152
    }
153
154
    private function buildCacheHash(SalesChannelContext $context): string
155
    {
156
        return md5(json_encode([
157
            $context->getRuleIds(),
158
            $context->getContext()->getVersionId(),
159
            $context->getCurrency()->getId(),
160
        ]));
161
    }
162
163
    /**
164
     * System states can be used to stop caching routes at certain states. For example,
165
     * the checkout routes are no longer cached if the customer has products in the cart or is logged in.
166
     */
167
    private function updateSystemState(Cart $cart, SalesChannelContext $context, Request $request, Response $response): array
168
    {
169
        $states = $this->getSystemStates($request, $context, $cart);
170
171
        if (empty($states)) {
172
            $response->headers->removeCookie(self::SYSTEM_STATE_COOKIE);
173
            $response->headers->clearCookie(self::SYSTEM_STATE_COOKIE);
174
175
            return [];
176
        }
177
178
        $cookie = Cookie::create(self::SYSTEM_STATE_COOKIE, implode(',', $states));
179
        $cookie->setSecureDefault($request->isSecure());
180
181
        $response->headers->setCookie($cookie);
182
183
        return $states;
184
    }
185
186
    private function getSystemStates(Request $request, SalesChannelContext $context, Cart $cart): array
187
    {
188
        $states = [];
189
        $swStates = (string) $request->cookies->get(self::SYSTEM_STATE_COOKIE);
190
        if ($swStates !== null) {
0 ignored issues
show
introduced by
The condition $swStates !== null is always true.
Loading history...
191
            $states = explode(',', $swStates);
192
            $states = array_flip($states);
193
        }
194
195
        $states = $this->switchState($states, self::STATE_LOGGED_IN, $context->getCustomer() !== null);
196
197
        $states = $this->switchState($states, self::STATE_CART_FILLED, $cart->getLineItems()->count() > 0);
198
199
        return array_keys($states);
200
    }
201
202
    private function switchState(array $states, string $key, bool $match): array
203
    {
204
        if ($match) {
205
            $states[$key] = true;
206
207
            return $states;
208
        }
209
210
        unset($states[$key]);
211
212
        return $states;
213
    }
214
215
    private function setCurrencyCookie(Request $request, Response $response): void
216
    {
217
        $currencyId = $request->get(SalesChannelContextService::CURRENCY_ID);
218
219
        if (!$currencyId) {
220
            return;
221
        }
222
223
        $cookie = Cookie::create(self::CURRENCY_COOKIE, $currencyId);
224
        $cookie->setSecureDefault($request->isSecure());
225
226
        $response->headers->setCookie($cookie);
227
    }
228
}
229