Search by

therealworld / staticcache-module

therealworld

Certain OXID7 controllers create static caches and may display them.

Package info

bitbucket.org/therealworld/staticcache-module

Homepage

Issues

Type:oxideshop-module

pkg:composer/therealworld/staticcache-module

Statistics

Installs: 684

Dependents: 0

Suggesters: 0

v3.0.10 2026-10-28 17:10 UTC

README

Deutsche Fassung: README.de.md

vendor-logo the-real-world.de

description

Stores the rendered html of a page and serves the next visitor from that copy, so the shop answers without running the controller, the templates or their database queries. With dynamic hydration (on by default) logged-in visitors and visitors with a basket get the cached page too — the parts that must not be shared are replaced with placeholders and reloaded over ajax. With hydration switched off they get the live page.

  • Composer package: therealworld/staticcache-module
  • OXID module id: trwstaticcache

New oe-console commands

  • trw:staticcache:prune [--shop-id=N] [--dry-run]
  • trw:staticcache:status [--url=...] [--lang=N] [--explain]

The commands live in this module, not in the clirun-plugin: only the module knows how its cache is stored, and this way the plugin does not need the module as a dependency. They are registered through the module's services.yaml and appear in oe-console as soon as the module is installed and active.

Requirements and installation

  • OXID eShop CE >=v7.4, PHP ^8.3, ext-json
  • therealworld/tools-plugin >=v3.6.2
  • therealworld/scheduler-module >=v2.2 (for the nightly prune task)
composer require therealworld/staticcache-module
vendor/bin/oe-console oe:module:activate trwstaticcache

What is cached, and what is not

A page is served from the cache unless the controller is excluded or the request carries an fnc parameter. A page is stored only when it additionally comes from an anonymous visitor with an empty basket, answers with http 200, is not a redirect, contains a closing document tag, and the volume still has room.

Never cached: everything under account*, the basket, the user, payment, order and thank-you controllers, plus the controllers active modules exclude themselves: through the CacheExcludedControllerContributorInterface decoration chain of the tools-plugin (base class AbstractCacheExcludedControllerContributor, examples: the captcha endpoints, the newsletter web view, the withdrawal form). An exclusion lives in the module's services.yaml and stops applying on its own when the module is deactivated. There is no setting for it: a shop that has to exclude a core or third party controller adds a small contributor of its own.

Every response says what happened

The X-TRW-StaticCache header states the outcome of each request, which is what makes the cache diagnosable at all — and what lets a warmup verify in a single request instead of two:

headermeaning
miss, storedrendered and written to the cache
cache, age=42sserved from the cache
cache+hydration, age=42sserved from the cache, dynamic widgets reloaded over ajax
cache+token, cache+hydration+tokenthe visitor's own csrf token was injected
bypass, reason=adminan admin is looking, always live
miss, reason=controllerthis controller is never cached
miss, reason=user / basketnot an anonymous, empty-basket visitor: never stored, and with hydration off not served from the cache either
miss, reason=functionthe request carries fnc
miss, reason=status / redirectnot a plain 200
miss, reason=nohtmla fragment, not a whole page
miss, reason=diskspacebelow the configured free space minimum
miss, reason=savefailedthe entry could not be written
curl -s -D - -o /dev/null 'https://example.org/a-page/' | grep -i x-trw

Note that OXID counts curl as a search engine (aRobots in config.inc.php), so a plain curl call sees the bot variant of the page. Pass -A 'Mozilla/5.0 ...' for the visitor view.

The cache key

Built from the shop id, the protocol, the language, the request path and those query parameters that are allowed to matter. Two optional parts join it: the attribute filter the visitor has set on the current category, and a cookie fingerprint that active modules contribute through the tools-plugin's CacheKeyCookieContributorInterface chain (both are empty for a visitor without filter and cookies, which is what a warmup sees). It deliberately does not contain the session, so one entry serves everybody.

Query parameters: a whitelist, not a blacklist

aTRWCacheKeyAllowedParams lists the parameters that may take part in the key. Anything not listed is stripped from the key — the request is still cached and still served, it just no longer creates an entry of its own.

This is the important setting of the module. With an ignore list alone, every parameter nobody thought of — a new tracking network, an affiliate id, a typo, a bot experimenting — silently multiplies the cache: one crawl once produced 901,349 keys for 23,811 pages and filled the volume.

The price is that the direction of failure flips. A parameter that really does change the page and is missing from the list will make the shop serve the wrong content, where before it only wasted disk. A module that introduces such a parameter therefore declares it itself: through the CacheKeyParamContributorInterface decoration chain of the tools-plugin (base class AbstractCacheKeyParamContributor; e.g. a module that shows catalogue items not imported into the shop on cl=details&extid=… keeps extid in the key with a subclass whose getOwnParams() returns ['extid']). Those parameters do not appear in the setting, but apply on top of it while the module is active — and leave the key again on their own when it is deactivated. Leaving the list empty restores the old behaviour (ignore list only) — that is the way back if a shop turns out to depend on something nobody listed.

aTRWCacheKeyIgnoredParams still applies afterwards and keeps session and campaign parameters out of the key even if someone puts a * in the whitelist.

Use trw:staticcache:status --url=... --explain to see what the key building does to a concrete url:

Url          : https://example.org/a-category/?pgNr=2&utm_source=news&whatever=1
State        : warm (expires in 00:59:42)
  normalized : /a-category/?pgNr=2
  params kept: pgNr
  dropped    : utm_source, whatever
Cache key    : 923a5d6205ebeb2b08c5460c390a181c

Housekeeping

Symfony's filesystem cache only removes an expired entry when the same key is read again. A url a bot asked for once and nobody ever asks for again therefore stays on disk forever, which is exactly the shape of the traffic a page cache sees. Without a prune the directory only grows.

trw:staticcache:prune

Drops expired entries and enforces the size limit, evicting the entries with the shortest remaining lifetime until the cache is back to 90% of the limit. Deletes without an extra flag — an expired entry is expired, and a command meant for cron must not need one; use --dry-run to look first.

vendor/bin/oe-console trw:staticcache:prune --dry-run
vendor/bin/oe-console trw:staticcache:prune --shop-id=1

The scheduler task Prune the page cache (TaskStaticCachePrune) does the same thing nightly (0 4 * * *) for every shop. Like every task, the scheduler only picks it up with look for new tasks and installs it inactive — switch it on in the backend under Service → Task-Scheduler.

trw:staticcache:status

Reports the directory, the number of entries, their size, the expiry range and the free disk space; with --url it says whether one page is warm and, with --explain, how its key is built. This is also the only honest test that a warmup works: cold before the request, warm after it.

Options

Module settings in the backend, in the order of the admin groups.

GroupSetting (admin label)DefaultEffect
Cache TimeiTRWCacheLifetime (Cache Lifetime in seconds)3600how long an entry stays valid
Caching RulesaTRWCacheKeyIgnoredParams (Query parameters ignored in the cache key)session, tracking and campaign parameters (force_sid, sid, stoken, utm_*, gclid, fbclid, ...)removed from the key after the whitelist; a trailing * matches by prefix
Caching RulesaTRWCacheKeyAllowedParams (Query parameters allowed in the cache key (whitelist))routing, list, search, language/currency (cur), variant and widget parametersthe whitelist above. Empty = old behaviour
Cache bypass for adminsbTRWBypassCacheForAdminUser (No cache for admins logged into the frontend)ona frontend user with admin rights always sees the live page and never writes to the cache. Costs one database query per logged-in request
Cache bypass for adminsbTRWBypassCacheForAdminCookie (No cache when a backend cookie is present)onsame for any browser carrying a backend session cookie (admin_sid). Free (no database access) and covers the usual workflow. The cookie is not validated — it only bypasses the cache and grants no rights
Size and disk limitsiTRWCacheMaxSizeMb (Maximum cache size in MB)0upper bound per shop, 0 = none. Enforced by the prune run only
Size and disk limitssTRWCacheMinFreeDisk (Minimum free disk space)2Gbelow this much free space the cache stops writing but keeps serving; accepts 2G, 500M, 5% or plain bytes, empty switches it off. If free space cannot be measured, writing continues — a broken measurement must not switch the cache off
HTML MinificationbTRWMinifyHtml (Minify HTML output)onminify the whole html output, cached or not. <script>, <style>, <textarea> and <pre> are protected
Dynamic Hydration (AJAX)bTRWDynamicHydration (Enable dynamic hydration)onfor logged-in visitors and visitors with a basket, serve the cached page and replace its personalised widgets with placeholders that are reloaded over ajax. Off = those visitors get the live page
Dynamic Hydration (AJAX)aTRWDynamicWidgets (Dynamic widget markers)oxwServiceMenu, oxwMiniBasketwhich data-trw-dynamic markers to hydrate

Note on bTRWDynamicHydration: the switch decides whether logged-in visitors and visitors with a basket are served from the cache at all. Switched on, they get the cached page and the personalised widgets are reloaded (cache+hydration). Switched off, they bypass the cache and the page is rendered live (miss, reason=user / basket), so they never see the widgets in their anonymous state. Anonymous visitors with an empty basket are served from the cache either way.

See docs/DYNAMIC_HYDRATION.md for what a theme has to provide, and which widgets cannot be hydrated.

Where the cache lives

<sCompileDir>/static_cache/<shopId>/ — its own directory, next to the compiled templates rather than inside them, so a template deployment and a page cache purge no longer take each other down and the page cache can be cleared, pruned and measured on its own.

trw:clear:cache (clirun-plugin), the cacheclean module and the backend's clear cache still remove it together with everything else — that is what "clear the cache" is expected to mean.

They do so without knowing this module: they call ToolsCache::cleanCache() from the tools plugin, which empties the page cache through PageCachePurgerInterface. This module replaces the plugin's no-op default with StaticCachePagePurger. An admin action that only changes what cached pages show (e.g. the start page) calls ToolsCache::cleanPageCache() and leaves the compile caches alone.

For developers

  • Class extensions (extend in metadata.php): Core\ShopControl (serves from the cache before process()), Core\Output (minification, writes the cache entry, csrf token injection on a fresh render), Core\Session (renders a fixed placeholder instead of the csrf token, so a stored page stays neutral and every visitor gets their own token on delivery).
  • Chains of the tools-plugin this module consumes: CacheKeyParamContributorInterface (extra key parameters), CacheExcludedControllerContributorInterface (excluded controllers), CacheKeyCookieContributorInterface (cookie part of the key). Other modules join them in their own services.yaml.
  • Chains this module joins (services.yaml): StaticCachePageProbe decorates PageCacheProbeInterface (a warmup can ask whether a url is warm), StaticCachePagePurger decorates PageCachePurgerInterface (ToolsCache::cleanCache()/cleanPageCache()).
  • Services: StaticCachePruner (used by the prune command and the scheduler task), the commands trw.staticcache.prune and trw.staticcache.status.
  • Scheduler task: src/SchedulerTasks/TaskStaticCachePrune.php.
  • Events: activation removes the pool the module used to keep inside template_cache/; deactivation clears the cache of the current shop.
  • Theme requirements for hydration: docs/DYNAMIC_HYDRATION.md.
  • No database tables, no migrations.