Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions concepts/framework/http_cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,36 @@ Set the defaults value of the `_httpCache` key to `true`. Examples for this can
public function index(SalesChannelContext $context, Request $request): Response
```

### Cache Cookies
### Determining the cache key

Shopware uses several client cookies to differentiate the requests. This allows us to save the cache differentiated between clients, e.g., different customer groups or different active rules.
Determining the cache key is one of the most important tasks of the HTTP cache. The cache key is used to identify a request and its corresponding response. If the same request comes in again, the cache key will be used to look up the corresponding response in the cache storage.
For a dynamic system like Shopware, the cache key needs to take the application state into account, as the response to the same request might differ e.g., based on the tax state of the currently logged-in customer.
At the same time, it needs to be possible to generate the cache key directly from the request to support reverse proxy caches, where the caching is handled by a standalone application that has no access to Shopware's internal application state.
Shopware generates a `cache-hash` that encodes the application state and this hash is passed alongside every request and response, the caching component will then generate the exact cache key based on the `cache-hash`.

#### sw-currency

This cookie will be set when the non-logged-in customer with an empty cart changes the current currency. Why does Shopware need a separate cookie for currency? It allows us to maximize the cache hits for non-logged-in customers as we separate the cache as less as possible.
Concretely Shopware uses Cookies to store the `cache-hash` as part of the request/response structure. The `cache-hash` describes the current state of the customer "session", every parameter that leads to different responses being generated (e.g. tax-states, matched rules) should be taken into account for the `cache-hash` to ensure that every user sees the correct page.
However, it is equally important to keep the number of different cache entries/permutations as low as possible to maximize the cache hits.
The reason the `cache-hash` is stored as a cookie is that it needs to be sent with every request and can change on any response sent from shopware.
The client needs to send the latest value back to shopware on every request to ensure the correct cache entry is used. This is needed as the cache is resolved before the request is handled by shopware itself.
To allow reverse proxies to cache based on the application state, the information needs to be present on every request. The reverse proxies (e.g. Fastly or Varnish) or the symfony cache component use the provided `cache-hash` as part of the cache key they generate for every request, thus they can differentiate the cache entries for the same request based on the application state.

#### sw-cache-hash

This cookie replaces the `sw-currency` cookie and contains the active rules and active currency. This cookie will be set when the active rules do not match the default anymore \(e.g., customer login/items in cart\).
This cookie contains the hash of all cache-relevant information (e.g. is the user logged-in, what tax state and what currency do they use, which cache-relevant rules have matched).
This is the cookie that stores the `cache-hash` mentioned above.
This cookie will be set as soon as the application state differs from the default, which is: no logged in customer, the default currency and an empty cart.

If you want to know how to manipulate and control the `cache-hash`, you can refer to the [Plugin caching guide](../../guides/plugins/plugins/framework/caching/index.md#http-cache).

#### sw-currency

**Note:** The currency cookie is deprecated and will be removed in v6.8.0.0, as the currency information is already part of the `sw-cache-hash` cookie.
This cookie will be set when the non-logged-in customer with an empty cart changes the current currency. Why does Shopware need a separate cookie for currency? It allows us to maximize the cache hits for non-logged-in customers as we separate the cache as less as possible.

#### sw-states

**Note:** The states cookie is deprecated and will be removed in v6.8.0.0, as the state information is already part of the `sw-cache-hash` cookie and different caches are used for the different states.
If you want to disable the cache in certain circumstances, you can do so via the `sw-cache-hash` cookie as well.
This cookie describes the current session in simple tags like `cart-filled` and `logged-in`. When the client tags fit the response `sw-invalidation-states` header, the cache will be skipped.

An example of usage for this feature is to save the cache for logged-in customers only.
Expand Down
53 changes: 28 additions & 25 deletions devenv.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"lastModified": 1761588595,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"type": "github"
},
"original": {
Expand All @@ -32,10 +32,31 @@
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1760663237,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"git-hooks",
"nixpkgs"
]
},
Expand Down Expand Up @@ -68,32 +89,14 @@
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1747372754,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
"pre-commit-hooks": [
"git-hooks"
]
}
}
},
Expand Down
171 changes: 160 additions & 11 deletions guides/plugins/plugins/framework/caching/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,161 @@

### HTTP-Cache

#### Modifying the cache keys
Before jumping in and adjusting the HTTP-Caching, please familiarize yourself with the general [HTTP-Cache concept](../../../../../concepts/framework/http_cache.md) first.

#### Manipulating the cache key

There are several entry points to manipulate the cache key.

* `Shopware\Core\Framework\Adapter\Cache\Http\Extension\CacheHashRequiredExtension`: used to determine whether the cache hash should be calculated or if the request in running in the default state, and therefore no cache-hash is needed.
* `Shopware\Core\Framework\Adapter\Cache\Event\HttpCacheCookieEvent`: used to calculate the cache hash based on the application state, supports both reverse proxy caches and the default symfony HTTP-cache component.
* `Shopware\Core\Framework\Adapter\Cache\Http\Extension\ResolveCacheRelevantRuleIdsExtension`: used to determine which rule IDs are relevant for the cache hash.
* `Shopware\Core\Framework\Adapter\Cache\Event\HttpCacheKeyEvent`: used to calculate the exact cache key based on the response, only for symfony's default HTTP-cache component.

#### Modifying when the cache hash is calculated

By default, the cache hash is only calculated when the request is not in the default state, which is: no logged in customer, default currency, and an empty cart.
The reason is that the very first request to the application from a client should always be cached in the best case, the state that the application is in then is the "default state", which does not require a cache hash.
You can overwrite the default behaviour and add more conditions where the cache hash needs to be applied, e.g., when you shop needs to be more dynamic e.g. based on campaign query parameters:

```php

Check warning on line 42 in guides/plugins/plugins/framework/caching/index.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/framework/caching/index.md#L42

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `PHP` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/plugins/framework/caching/index.md:42:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `PHP`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
class RequireCacheHash implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
CacheHashRequiredExtension::NAME . '.post' => 'onRequireCacheHash',,

Check warning on line 48 in guides/plugins/plugins/framework/caching/index.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/framework/caching/index.md#L48

Two consecutive commas (DOUBLE_PUNCTUATION) Suggestions: `,` URL: https://languagetool.org/insights/post/punctuation-guide/#what-are-periods Rule: https://community.languagetool.org/rule/show/DOUBLE_PUNCTUATION?lang=en-US Category: PUNCTUATION
Raw output
guides/plugins/plugins/framework/caching/index.md:48:78: Two consecutive commas (DOUBLE_PUNCTUATION)
 Suggestions: `,`
 URL: https://languagetool.org/insights/post/punctuation-guide/#what-are-periods 
 Rule: https://community.languagetool.org/rule/show/DOUBLE_PUNCTUATION?lang=en-US
 Category: PUNCTUATION
];
}

public function onRequireCacheHash(CacheHashRequiredExtension $extension): void
{
if ($extension->request->query->has('campaignId')) {
$extension->result = true;
}
}
}
```

#### Modifying the cache hash

The cache hash is used as the basis for the cache key.
It is calculated based on the application state, which includes the current user, the current language, and so on.
As the cache hash is calculated based on the application state, you have access to the resolved `SalesChannelContext` to determine the cache hash.
It is stored alongside the response as a cookie and thus also provided with all following requests, to allow differentiating the cache based on the application state.
As the cache hash will be carried over to the next request, the computed cache hash can be used inside reverse proxy caches as well as the default symfony HTTP-cache component.

:::info
The cache hash is only computed on every response as soon as the application state differs from the default state, which is: no logged in customer, default currency, and an empty cart.
:::

By default, the cache hash will consist of the following parts:

* `rule-ids`: The matched rule IDs, to reduce possible cache permutations starting with v6.8.0.0, this will only include the rule IDs in `rule areas` that are cache relevant. See the next chapter how to extend this.
* `version-id`: The context version used to load versioned DAL entities.
* `currency-id`: The currency ID of the context.
* `tax-state`: The tax state of the context (gross/net).
* `logged-in`: Whether a customer is logged in in the current state or not.

To modify the cache hash, you can subscribe to the `HttpCacheCookieEvent` event and add your own parts to the cache hash.
This allows you to add more parts to the cache hash, e.g., the current customer's group.
You can also disable the cache for certain conditions, because if that condition is met, the content is so dynamic that caching is not efficiently possible e.g., if the cart is filled.

```php

Check warning on line 85 in guides/plugins/plugins/framework/caching/index.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/framework/caching/index.md#L85

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `PHP` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/plugins/framework/caching/index.md:85:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `PHP`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
class HttpCacheCookieListener implements EventSubscriberInterface
{
public function __construct(
private readonly CartService $cartService
) {
}

public static function getSubscribedEvents(): array
{
return [
HttpCacheCookieEvent::class => 'onCacheCookie',
];
}

public function onCacheCookie(HttpCacheCookieEvent $event): void
{
// you can add custom parts to the cache hash
// keep in mind that every possible value will increase the number of possible cache permutations
// and therefore directly impact cache hit rates, which in turn decreases performance
$event->add('customer-group', $event->context->getCustomerId());

// disable cache for filled carts
$cart = $this->cartService->getCart($event->context->getToken(), $event->context);
if ($cart->getLineItems()->count() > 0) {
// you can also explicitly disable caching based on specific conditions
$event->isCacheable = false;
}
}
}
```

#### Marking rule areas as cache relevant

Starting with v6.8.0.0, the cache hash will only include the rule IDs in `rule areas` that are cache relevant.
The reason is that a lot of rules are not relevant for the cache, e.g., rules that only affect pricing or shipping methods.
This greatly reduces the number of possible cache permutations, which in turn improves the cache hit rate.

By default, only the following rule areas are cache relevant:

* `RuleAreas::PRODUCT_AREA`

If you use the rule system in a way that is relevant for the cache (because the response differs based on the rules), you should add your rule area to the list of cache relevant rule areas.
To do so, you need to subscribe to the `ResolveCacheRelevantRuleIdsExtension` event.

```php

Check warning on line 130 in guides/plugins/plugins/framework/caching/index.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/framework/caching/index.md#L130

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `PHP` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/plugins/framework/caching/index.md:130:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `PHP`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
class ResolveRuleIds implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ResolveCacheRelevantRuleIdsExtension::NAME . '.pre' => 'onResolveRuleAreas',
];
}

public function onResolveRuleAreas(ResolveCacheRelevantRuleIdsExtension $extension): void
{
$extension->ruleAreas[] = RuleExtension::MY_CUSTOM_RULE_AREA;
}
}
```

This implies that you defined the rule area in your custom entities that have an associated rule entity, by using the DAL flag `Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RuleAreas` on the rule association in the entity extension.

```php

Check warning on line 149 in guides/plugins/plugins/framework/caching/index.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/framework/caching/index.md#L149

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `PHP` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/plugins/framework/caching/index.md:149:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `PHP`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
class RuleExtension extends EntityExtension
{
public const MY_CUSTOM_RULE_AREA = 'custom';

public function getEntityName(): string
{
return RuleDefinition::ENTITY_NAME;
}

Every cached item has a unique key used to retrieve the cached data later on. For that, it is important that all the relevant information that affects the data that is being cached is part of the key.
For example, if the same data is cached in multiple languages or for multiple sales channels, the key must contain the language and sales channel information, so that the correct data can be retrieved later on.
Please note that for every potential value your key part can take, a new cache entry will be created. So if you have a key part that can take 10 different values, you will have 10 times the number of cache entries for the same data.
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new ManyToManyAssociationField(
'myPropertyName',
MyCustomDefinition::class,
MyMappingDefinition::class,
RuleDefinition::ENTITY_NAME . '_id',
MyCustomDefinition::ENTITY_NAME . '_id',
))->addFlags(new CascadeDelete(), new RuleAreas(self::MY_CUSTOM_RULE_AREA)),
);
}
}
```

If you add customization to your projects that lead to different versions of the page being rendered, you need to make sure that the cache key is unique for each version of the page. This can be done by adding a specific part to the cache key that shopware is generating.
For details on how to extend core definitions refer to the [DAL Guide](../../framework/data-handling/add-complex-data-to-existing-entities.md).

#### Modifying the cache keys

You can also modify the exact cache key used to store the response in the [symfony HTTP-Cache](https://symfony.com/doc/current/http_cache.html).
If possible, you should manipulate the cache hash (as already explained above) instead, as that is also used in reverse proxy caches.
You can do so by subscribing to the `HttpCacheKeyEvent` event and add your specific part to the key.

```php
Expand All @@ -47,6 +195,9 @@
// Perform checks to determine the key
$key = $this->determineKey($request);
$event->add('myCustomKey', $key);

// You can also disable caching for certain conditions
$event->isCacheable = false;
}
}
```
Expand All @@ -62,23 +213,21 @@
One problem with caching is that you not only need to retrieve the correct data, but also need to have a performant way to invalidate the cache when the data changes.
Only invalidating the caches based on the unique cache key is often not that helpful, because you don't know which cache keys are affected by the change of a specific data set.
Therefore, a tagging system is used alongside the cache keys to make cache invalidations easier and more performant. Every cache entry can be tagged with multiple tags, thus we can invalidate the cache based on the tags.
For example, all pages that contain product data are tagged with product ids of all products they contain. So if a product is changed, we can invalidate all cache entries that are tagged with the product id of the changed product.
For example, all pages that contain product data are tagged with product IDs of all products they contain. So if a product is changed, we can invalidate all cache entries that are tagged with the product ID of the changed product.

To add your own cache tags to the HTTP-Cache, you need to dispatch the `AddCacheTagEvent` with the tag you want to add to the cache entry for the current request.
To add your own cache tags to the HTTP-Cache, you can use the `CacheTagCollector` service.

```php
class MyCustomEntityExtension
{
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly CacheTagCollector $cacheTagCollector,
) {}

public function loadAdditionalData(): void
{
// Load the additional data you need, add it to the response, then add the correct tag to the cache entry
$this->eventDispatcher->dispatch(
new AddCacheTagEvent('my-custom-entity-' . $idOfTheLoadedData)
);
$this->cacheTagCollector->addTag('my-custom-entity-' . $idOfTheLoadedData);
}
}
```
Expand Down