Quick quiz. Your plugin adds a storefront form — a product inquiry, say. Three requirements land with it: spam protection, the page must never end up in any cache (it prefills customer data), and the thank you page should be cached like a normal content page.
How much code is that? If your answer involves a captcha service, a response subscriber and cache configuration: it’s actually three words in your route attributes. Shopware’s kernel listeners run a small framework driven by route defaults, the core uses it on every relevant route — and apart from _httpCache appearing in passing in some examples, none of it is documented.
_captcha — spam protection as a route flag
#[Route(
path: '/acme/inquiry',
name: 'frontend.acme.inquiry.send',
defaults: ['_captcha' => true],
methods: ['POST']
)]
public function sendInquiry(Request $request, SalesChannelContext $context): Response
CaptchaRouteListener (storefront/Framework/Captcha/CaptchaRouteListener.php:46-80) intercepts the request before your controller runs, reads which captchas the merchant activated in the admin (honeypot, Google reCAPTCHA v2/v3, basic captcha — per sales channel), and validates them. Depending on the captcha type it either throws or swaps in an error controller response.
Read that again from the plugin developer’s perspective: you don’t choose the captcha. The merchant’s admin configuration decides, your route just declares „this endpoint deserves protection“. Honeypot today, reCAPTCHA tomorrow — no code change in your plugin. The core puts this flag on registration, contact form and review submission; your custom forms deserve the same one-liner.
(Template side: include @Storefront/storefront/component/captcha/base.html.twig in your form so the active captcha’s markup renders.)
Browser cache, reverse proxy, CDN — all told to forget the page existed. The core flags login, guest login and the entire checkout this way (AuthController.php:61, CheckoutController.php:78,121,166). The bug this prevents is the nasty one: a shared-cache layer serving customer A’s prefilled account page to customer B. If your route renders anything customer-specific and cacheable infrastructure sits in front of the shop, _noStore is the cheapest insurance you’ll ever buy.
_httpCache — opt into the cache, with options
The inverse flag: routes with defaults: ['_httpCache' => true] become HTTP-cacheable — that’s how the homepage, navigation and product detail pages get cached (ProductController.php:50, NavigationController.php:38).
The undocumented part is in CacheResponseSubscriber (Framework/Adapter/Cache/Http/CacheResponseSubscriber.php:124-152): the attribute doesn’t have to be true. It can be an array:
maxAge overrides the shop’s default TTL for this route — your fast-moving stock widget can cache for two minutes while the rest of the shop caches for hours.
states lists context states for which the cache must be bypassed: the subscriber compares them against the visitor’s state cookie (logged-in, cart-filled) and skips caching when they match. That’s the mechanism behind „cache the page for anonymous visitors, render fresh for logged-in customers“ — available per route, no Varnish VCL required.
The same code path also attaches the stale-while-revalidate/stale-if-error directives from shopware.http_cache.* config, ties into the sw-states cookie, and sets the invalidation-states header the cache layer evaluates. Route defaults are the tip; the iceberg underneath is Shopware’s whole HTTP cache keying.
Why this pattern is worth stealing
Beyond the three flags themselves, notice the architecture: cross-cutting behavior (spam protection, cache policy) declared at the route, enforced by kernel listeners, configured by the merchant. Your controller stays free of it. When you build plugin functionality that every route „of a certain kind“ needs — this is the house style for how Shopware wants it done. (XmlHttpRequest => true, which you’ve seen in core routes, is part of the same family: it marks routes as AJAX-only.)
TL;DR
Three route defaults replace three subscribers: _captcha => true validates whatever captcha the merchant configured, before your controller runs. _noStore => true nukes every cache layer’s memory of the response — mandatory for customer-specific pages. _httpCache => true opts into HTTP caching, and the undocumented array form ['maxAge' => 120, 'states' => ['logged-in']] sets per-route TTL and cache-bypass conditions. Copy the flags from core controllers; they’re the documentation.
Next up: database:clean-personal-data — GDPR housekeeping with a built-in command.
Which flag did your storefront plugin turn out to be missing? Drop me an email!
Found in Shopware 6.7.0.0: attribute constants in PlatformRequest.php:46-50, captcha validation in storefront/Framework/Captcha/CaptchaRouteListener.php:46-80, no-store headers in storefront/Framework/Routing/ResponseHeaderListener.php:64-76, cache attribute (incl. array form) in Framework/Adapter/Cache/Http/CacheResponseSubscriber.php:124-152, core route examples in AuthController.php:61, CheckoutController.php:78, ProductController.php:50.