Skip to content
3 changes: 3 additions & 0 deletions .wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ LongText
LongTextField
Lychee
MACOSX
MDN
MJML
MRs
MVC
Expand Down Expand Up @@ -922,6 +923,7 @@ Synopsys
TCP
TLS
TTL
TTLs
TaxFreeConfigField
TaxProvider
TaxProviderStruct
Expand Down Expand Up @@ -1133,6 +1135,7 @@ bugfixes
bundler
buyable
cacheKey
cacheability
cacheable
cacheinvalidatorfacade
cachix
Expand Down
30 changes: 24 additions & 6 deletions concepts/framework/http_cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,22 @@ So whenever a user requests a page, Shopware will create a result page individua

The reverse proxy is located between the user and the web application and takes care of any requests to the web application. If a user requests a page that has been requested before, chances are that the reverse proxy can just hand out the same result as before, so the web application will not even be asked.

So a reverse proxy is basically a thin layer between the user and the web application that will try to avoid load on the web application by caching the results. Whenever the web application generates a response for a request, the reverse proxy will save the request and the response to cache storage. Next time the same request comes in, the response will most probably be the same.
So a reverse proxy is basically a thin layer between the user and the web application that will try to avoid load on the web application by caching the results. Whenever the web application generates a response for a request, the reverse proxy will save the request and the response to cache storage. The next time the same request comes in, the response will most probably be the same.

## How does it work?

Caching is always about questions like:

* Did I return the same page before?
* Did the content of the page change meanwhile?
* Is this page the same for all customers or will the current customer get another result \(e.g. price\)?
* Is this page the same for all customers, or will the current customer get another result \(e.g. price\)?

The Shopware HTTP cache has a variety of mechanisms to answer these questions.

## When will the page be cached?

Set the defaults value of the `_httpCache` key to `true`. Examples for this can be found in the [ProductController](https://github.com/shopware/shopware/blob/trunk/src/Storefront/Controller/ProductController.php#L62).
Only `GET` requests are considered cacheable.

```php
#[Route(path: '/detail/{productId}', name: 'frontend.detail.page', methods: ['GET'], defaults: ['_httpCache' => true])]
Expand All @@ -54,17 +55,17 @@ For a dynamic system like Shopware, the cache key needs to take the application
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`.

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.
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.
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 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.
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).

Expand All @@ -81,10 +82,27 @@ This cookie describes the current session in simple tags like `cart-filled` and

An example of usage for this feature is to save the cache for logged-in customers only.

### Determining TTL and other cache parameters

The TTL and other cache parameters are determined via [caching policies](../../guides/hosting/performance/caches.md#http-caching-policies). The feature is experimental and will become the default behavior in Shopware v6.8.0.0.

## Cache invalidation

As soon as a response has been defined as cacheable and the response is written to the cache, it is tagged accordingly. For this purpose, the core uses all cache tags generated during the request or loaded from existing cache entries. The cache invalidation of a Storefront controller route is controlled by the cache invalidation of the Store API routes.

For more information about Store API cache invalidation, you can refer to the [Caching Guide](../../guides/plugins/plugins/framework/caching/index.md).

This is because all data loaded in a Storefront controller, is loaded in the core via the corresponding Store API routes and provided with corresponding cache tags. So the tags of the HTTP cache entries we have in the core consist of the sum of all Store API tags generated or loaded during the request. Therefore, the invalidation of a controller route is controlled over the Store API cache invalidation.
This is because all data loaded in a Storefront controller is loaded in the core via the corresponding Store API routes and provided with corresponding cache tags. So the tags of the HTTP cache entries we have in the core consist of the sum of all Store API tags generated or loaded during the request. Therefore, the invalidation of a controller route is controlled over the Store API cache invalidation.

## HTTP Cache workflow

**Note:** Workflow described here applies since v6.8.0.0 or since 6.7.6.0 when the `CACHE_REWORK` feature flag is enabled.

When a response is generated and about to be sent to the client, the `CacheResponseSubscriber` executes the following logic to determine caching behavior:

* Header application: The system applies `sw-language-id` and `sw-currency-id` headers. The `Vary` header is expanded to include these IDs and the `sw-context-hash`, ensuring proxies store separate cache entries for different contexts.
* Early exits: Basic checks are performed (e.g., is HTTP cache enabled?) to potentially skip further processing.
* Context hash calculation: The `sw-context-hash` is calculated based on the current state (cart, customer, rules, etc.). Extensions can hook into this process to add their own parameters (see [Plugin Caching Guide](../../guides/plugins/plugins/framework/caching/index.md#http-cache)).
* Cacheability assessment: The request is evaluated if it can be cached. It must be a `GET` request and the route must be marked with the `_httpCache` attribute.
* Validation: The system compares the client's `sw-context-hash` with the server-calculated hash. If they mismatch, a no-cache policy is applied to prevent cache poisoning. The `sw-dynamic-cache-bypass` header is added to hint proxies to apply shorter TTLs for ["hit-for-pass"](https://info.varnish-software.com/blog/hit-for-pass-varnish-cache) objects (custom configuration needed).
* Policy application: The appropriate [caching policy](../../guides/hosting/performance/caches.md#http-caching-policies) is resolved for the route and applied to the response, setting the correct `Cache-Control` headers.
82 changes: 80 additions & 2 deletions guides/hosting/performance/caches.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,92 @@ The HTTP Cache is a *must-have* for every production system. With an enabled cac

### How to configure the HTTP cache

The HTTP cache configuration takes place completely in the `.env.local` file. The following configurations are available here:
Basic HTTP cache configuration takes place in the `.env.local` file.

| Name | Description |
|:------------------------------|:-------------------------------|
| `SHOPWARE_HTTP_CACHE_ENABLED` | Enables the HTTP cache |
| `SHOPWARE_HTTP_DEFAULT_TTL` | Defines the default cache time |

The storage used for HTTP Cache is always the [App Cache](#app-cache), see below how to configure it. If you want to move this out of the application cache, you should use an external reverse proxy cache like [Varnish](https://varnish-cache.org/) or [Fastly](https://www.fastly.com/). For more [see here](../infrastructure//reverse-http-cache.md).
`SHOPWARE_HTTP_DEFAULT_TTL` is deprecated and will be removed in Shopware v6.8.0.0. Use [HTTP Caching Policies](#http-caching-policies) instead to define default cache times.

To provide more detailed control over the HTTP cache behavior, use the [HTTP Caching Policies](#http-caching-policies) feature.

The storage used for HTTP Cache is always the [App Cache](#app-cache), see below how to configure it. If you want to move this out of the application cache, you should use an external reverse proxy cache like [Varnish](https://varnish-cache.org/) or [Fastly](https://www.fastly.com/). For more [see here](../infrastructure/reverse-http-cache.md).

### HTTP Caching Policies

> **Note:** This feature is experimental and subject to change. It will be the default behavior in Shopware v6.8.0.0.
> To use it now, enable the `CACHE_REWORK` feature flag.

Caching policies allow you to define HTTP cache behavior per area (storefront, store_api) and per route via configuration. Shopware comes with reasonable defaults, but you can customize them.

#### Configuration

##### Defining a policy

By default, Shopware ships with `storefront.cacheable`, `store_api.cacheable` and `no_cache_private` policies.
You can define your own policies:

```yaml
# config/packages/shopware.yaml
shopware:
http_cache:
policies:
custom_policy:
headers:
cache_control:
public: true
max_age: 600 # browser ttl
s_maxage: 3600 # reverse proxy ttl
```

Supported `cache_control` directives: `public`, `private`, `no_cache`, `no_store`, `no_transform`, `must_revalidate`, `proxy_revalidate`, `immutable`, `max_age`, `s_maxage`, `stale_while_revalidate`, `stale_if_error`. For more information on these directives, see the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).

You can redefine existing policies. Note that policy definitions are not merged; redefining an existing policy overrides it completely.

Currently, you can only configure the `cache_control` header.

##### Setting default policies

You can change default `cacheable` and `uncacheable` policies per area (`storefront`, `store_api`):

```yaml
shopware:
http_cache:
default_policies:
store_api: # the area name
cacheable: custom_policy # policy to use for cacheable responses
```

##### Fine-tuning per route or app hook

You can override default policies per route:

```yaml
shopware:
http_cache:
route_policies:
store-api.product.search: custom_policy
```

App developers can override TTLs from the default policies via script configuration. See [custom endpoints](../../plugins/apps/app-scripts/custom-endpoints.md#set-the-max-age-of-the-cache-item) for details.
You can override this by configuring hook-specific policies using the `route#hook` pattern:

```yaml
shopware:
http_cache:
route_policies:
frontend.script_endpoint#storefront-acme-feature: storefront.my_custom_policy # storefront-acme-feature is the normalized hook name
```

##### Policy precedence

Shopware resolves policies in the following order (highest to lowest priority):

1. `route_policies[route#hook]` - most specific, for script endpoints with hooks (e.g., `frontend.script_endpoint#acme-app-hook`).
2. `route_policies[route]` - route-level override.
3. `default_policies[area].{cacheable|uncacheable}` - area defaults; TTLs (`max-age`, `s-maxage`) can be overridden by values from the request attribute or script configuration.

## How to change the cache storage

Expand Down
13 changes: 9 additions & 4 deletions guides/plugins/apps/app-scripts/custom-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@

Scripts available for the Storefront should be stored in a folder prefixed with `storefront-`, so the folder name would be `storefront-{hook-name}`.
The execution of those scripts is possible over the `/storefront/script/{hook-name}` endpoint.
Custom Storefront endpoints can be called by a normal browser request or from javascript via ajax.
Custom Storefront endpoints can be called by a normal browser request or from JavaScript via ajax.

This endpoint allows `POST` and `GET` requests.

Expand Down Expand Up @@ -165,7 +165,7 @@

## Caching

To improve the end-user experience and provide a scalable system, the customer-facing APIs (i.e., `store-api` and `storefront`) offer caching mechanism to cache the response to specific requests and return the response from the cache on further requests instead of computing it again and again on each request.
To improve the end-user experience and provide a scalable system, the customer-facing APIs (i.e., `store-api` and `storefront`) offer a caching mechanism to cache the response to specific requests and return the response from the cache on further requests instead of computing it again and again on each request.

By default, caching is enabled for custom endpoints, but for `store-api` endpoints you have to generate the cache key in the script.
For `storefront` requests, however, shopware takes care of it so that responses get automatically cached (if the [HTTP-Cache](../../../../concepts/framework/http_cache) is enabled).
Expand Down Expand Up @@ -200,15 +200,20 @@

You can specify for how long a response should be cached by calling the `cache.maxAge()` method and passing the number of seconds after which the cache item should expire.

> **Note:** `cache.maxAge()` is deprecated and will be removed in v6.8.0.0. Starting with v6.7.6.0, you can use `sharedMaxAge()` (corresponds to `s-maxage` in the `Cache-Control` header).
> When the `CACHE_REWORK` feature flag is enabled, you can also use `clientMaxAge()` (corresponds to `max-age` in the `Cache-Control` header).

```twig
{% set response = services.response.json({ 'foo': 'bar' }) %}
{% do response.cache.maxAge(120) %}
{% do response.cache.sharedMaxAge(120) %}

{% do hook.setResponse(response) %}
```

#### Invalidate cache items for specific states

> **Note:** The cache states feature is deprecated and will be removed in v6.8.0.0. It also does not work when the `CACHE_REWORK` feature flag is enabled.

You can specify that the cached response is not valid if one of the given states is present.
For more detailed information on the invalidation states, refer to the [HTTP-cache docs](../../../../concepts/framework/http_cache#sw-states).

Expand Down Expand Up @@ -259,7 +264,7 @@
{% endif %}
```

You can then use the filtered down list of ids to invalidate entity specific tags:
You can then use the filtered-down list of ids to invalidate entity-specific tags:

Check warning on line 267 in guides/plugins/apps/app-scripts/custom-endpoints.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/app-scripts/custom-endpoints.md#L267

This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2]) Suggestions: `IDs` Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2 Category: CASING
Raw output
guides/plugins/apps/app-scripts/custom-endpoints.md:267:43: This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2])
 Suggestions: `IDs`
 Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2
 Category: CASING

```twig
{% set tags = [] %}
Expand Down