therealworld / staticcache-module
Certain OXID7 controllers create static caches and may display them.
Package info
bitbucket.org/therealworld/staticcache-module
Type:oxideshop-module
pkg:composer/therealworld/staticcache-module
Requires
- php: ^8.3
- ext-json: *
- oxid-esales/oxideshop-ce: >=v7.4
- therealworld/scheduler-module: >=v2.2
- therealworld/tools-plugin: >=v3.6.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- v3.0.10
- v3.0.9
- v3.0.8
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.x-dev
- v2.1.16
- v2.1.15
- v2.1.14
- v2.1.13
- v2.1.12
- v2.1.11
- v2.1.10
- v2.1.9
- v2.1.8
- v2.1.7
- v2.1.6
- v2.1.5
- v2.1.4
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.9
- v2.0.8
- v2.0.7
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.2.12
- v1.2.11
- v1.2.10
- v1.2.9
- v1.2.8
- v1.2.7
- v1.2.6
- v1.2.5
- v1.2.4
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- dev-master
This package is auto-updated.
Last update: 2026-10-28 17:10:46 UTC
README
Deutsche Fassung: README.de.md

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.2therealworld/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:
| header | meaning |
|---|---|
miss, stored | rendered and written to the cache |
cache, age=42s | served from the cache |
cache+hydration, age=42s | served from the cache, dynamic widgets reloaded over ajax |
cache+token, cache+hydration+token | the visitor's own csrf token was injected |
bypass, reason=admin | an admin is looking, always live |
miss, reason=controller | this controller is never cached |
miss, reason=user / basket | not an anonymous, empty-basket visitor: never stored, and with hydration off not served from the cache either |
miss, reason=function | the request carries fnc |
miss, reason=status / redirect | not a plain 200 |
miss, reason=nohtml | a fragment, not a whole page |
miss, reason=diskspace | below the configured free space minimum |
miss, reason=savefailed | the 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.
| Group | Setting (admin label) | Default | Effect |
|---|---|---|---|
| Cache Time | iTRWCacheLifetime (Cache Lifetime in seconds) | 3600 | how long an entry stays valid |
| Caching Rules | aTRWCacheKeyIgnoredParams (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 Rules | aTRWCacheKeyAllowedParams (Query parameters allowed in the cache key (whitelist)) | routing, list, search, language/currency (cur), variant and widget parameters | the whitelist above. Empty = old behaviour |
| Cache bypass for admins | bTRWBypassCacheForAdminUser (No cache for admins logged into the frontend) | on | a 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 admins | bTRWBypassCacheForAdminCookie (No cache when a backend cookie is present) | on | same 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 limits | iTRWCacheMaxSizeMb (Maximum cache size in MB) | 0 | upper bound per shop, 0 = none. Enforced by the prune run only |
| Size and disk limits | sTRWCacheMinFreeDisk (Minimum free disk space) | 2G | below 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 Minification | bTRWMinifyHtml (Minify HTML output) | on | minify the whole html output, cached or not. <script>, <style>, <textarea> and <pre> are protected |
| Dynamic Hydration (AJAX) | bTRWDynamicHydration (Enable dynamic hydration) | on | for 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, oxwMiniBasket | which 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 (
extendinmetadata.php):Core\ShopControl(serves from the cache beforeprocess()),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 ownservices.yaml. - Chains this module joins (
services.yaml):StaticCachePageProbedecoratesPageCacheProbeInterface(a warmup can ask whether a url is warm),StaticCachePagePurgerdecoratesPageCachePurgerInterface(ToolsCache::cleanCache()/cleanPageCache()). - Services:
StaticCachePruner(used by the prune command and the scheduler task), the commandstrw.staticcache.pruneandtrw.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.