moselwal/frankenphp — worker mode plus Souin cache for TYPO3.
moselwal/frankenphp wires TYPO3 into the FrankenPHP worker mode, emits RFC 8297 conformant 103 Early Hints from the AssetCollector and invalidates the upstream Caddy/Souin HTTP cache automatically via PSR-14 events and a DataHandler hook. A backend module exposes cache flushes and key inspection right inside TYPO3.
Five building blocks
- FrankenPHP frontend worker — request classification separates frontend requests (worker mode) from backend and install tool. The worker bootstraps TYPO3 fresh per request, no state sharing between requests.
- 103 Early Hints — actual RFC 8297 responses via
headers_send(103)from theAssetCollector. CSS and JS preloads start in parallel with server rendering. - Caddy/Souin cache invalidation — REST-API based via PURGE and DELETE. Three strategies: URL-based, tag-based via Surrogate-Key, wildcard.
- PSR-14 event architecture plus DataHandler hook — auto-invalidation on page, content and file changes.
- Backend module — Admin Tools → manual flushes and cache-key inspection.
Requirements
- TYPO3 13.4 or 14.x
- PHP 8.3+
- FrankenPHP 1.x
- Caddy 2.11+ with
cache-handler/ Souin - Extension key
frankenphp, namespaceMoselwal\FrankenPHP, GPL-2.0-or-later
Frontend worker and request classification
| Class | Purpose |
|---|---|
Classes/Runtime/FrontendWorker | Worker loop with frankenphp_handle_request(), fresh TYPO3 bootstrap per request with REQUESTTYPE_FE |
Classes/Runtime/RequestClassifier | Pure PHP (no TYPO3 dependencies), classifies requests from $_SERVER into frontend, backend or install. Backend detection by path or host |
Resources/Private/Worker/frontend.php | Worker entry point (register as worker in the Caddyfile) |
Classes/Runtime/ is excluded from DI auto-registration because this code runs before the TYPO3 container.
Environment variables
| Variable | Description | Default |
|---|---|---|
TYPO3_BACKEND_ENTRYPOINT | Backend entry point (path or URL) | /typo3 |
MAX_REQUESTS | Requests per worker lifecycle | 1 |
TYPO3_AUTOLOAD_PATH | Path to vendor/autoload.php | auto-detect |
Cache invalidation against Caddy/Souin
CaddyCacheService speaks the Souin REST API:
- DELETE
{proto}://{host}:{port}{apiEndpoint}with JSON body{"url": "https://example.com/path"}— URL based - PURGE
{proto}://{host}:{port}{apiEndpoint}withSurrogate-Keyheader — tag based - Wildcard URLs — site wide
Auto-invalidation
- PSR-14
CacheFlushEvent— general cache flush - DataHandler hook
processDatamap_afterDatabaseOperations—pages,tt_content,sys_file,sys_file_reference,fe_users,fe_groups - PSR-14
SystemInformationToolbarCollectorEvent— backend toolbar integration
Optional API key via X-API-Key header.
Custom cache tags
$this->caddyCacheService->invalidateByTags(['custom:tag', 'product:123']);
Custom event listener
# Configuration/Services.yaml
services:
Your\Extension\EventListener\CustomCacheListener:
tags:
- name: event.listener
identifier: 'custom-cache-invalidation'
event: Your\Extension\Event\CustomEventEarlyHintMiddleware (103 Early Hints)
The middleware collects the CSS and JS sources registered via the AssetCollector in the PSR-15 hook after handler->handle(), resolves EXT: paths with PathUtility::getSystemResourceUri() (TYPO3 v14+ conformant) and sends two parallel hints:
- 103 Early Hints via
headers_send(103)— as soon as FrankenPHP exposes the function (function_exists()guard for CLI and test contexts). Link:header on the 200 response — fallback and for HTTP/2-push-capable reverse proxies.
Both mechanisms produce browser-deduplicable preload hints. CSS and JS heavy pages measurably improve their LCP values.
Middleware position: before typo3/cms-frontend/timetracker (see Configuration/RequestMiddlewares.php).
Installation and Caddyfile
composer require moselwal/frankenphp
Caddyfile
your-domain.com {
root * /app/public
php_server {
worker /app/vendor/moselwal/frankenphp/Resources/Private/Worker/frontend.php
env TYPO3_BACKEND_ENTRYPOINT /typo3
env MAX_REQUESTS 1000
}
route {
cache {
api {
souin
}
ttl 1h
default_cache_control public
}
}
}
Extension configuration
In the TYPO3 backend under “Admin Tools → Extension Configuration → frankenphp”:
| Key | Description | Default |
|---|---|---|
caddyHost | Caddy server host for the cache API | localhost |
caddyPort | Caddy server port | 80 |
caddyProtocol | http or https | http |
caddyApiEndpoint | Souin API path | /souin/api/souin |
caddyApiKey | Optional, sends an X-API-Key header | — |
enableDebugLogging | Detail logs for cache calls | 0 |
httpTimeout | HTTP timeout (seconds) | 5 |
invalidateAllSites | Cache flush invalidates all sites | 1 |
cacheTagHeader | Header name for tag invalidation | Surrogate-Key |
Worker reload and troubleshooting
Worker reload without a container restart
OPcache caches middleware code per worker thread. After code changes, typo3 cache:flush --group all alone is not enough — recycle the workers:
# SIGUSR1 to FrankenPHP PID 1
docker exec <httpd-container> kill -USR1 1
frankenphp reload --config /etc/caddy/Caddyfile only reloads the Caddy config, not necessarily the PHP worker threads.
Logging
PSR-3 logger via TYPO3:
- Info — normal cache invalidation operations
- Warning — failed API calls
- Error — exceptions during invalidation
- Debug — request and response details (only with
enableDebugLogging)
Backend module
“Admin Tools → Caddy Cache Management”:
- Flush All Cache — invalidates all configured sites
- Flush Site Cache — per site
- Cache Status — overview of the auto-invalidation
Troubleshooting
| Symptom | Likely cause |
|---|---|
103 is not visible in curl -v | Souin swallows the 103 on cache hits. Use Cache-Control: no-cache plus a cache buster in the URL. Also, curl < 8.10 silently hides 103. |
Browser console Refused to apply style from 'https://host/EXT:...' | Pre v4.0.3 — EXT: resolution was not implemented. Update to v4.0.6+. |
| Cache is not invalidated | Check Caddy host/port/protocol, enable enableDebugLogging, check logs in /var/log/typo3 |
| API calls fail with 401 | Check API key, allow the X-API-Key header in the Caddy config |
Moving TYPO3 to worker mode?
If you want to take TYPO3 into FrankenPHP worker mode, cleanly invalidate the Caddy/Souin cache layer in front of TYPO3, or shave measurable LCP off CSS/JS-heavy pages via 103 Early Hints, get in touch. We coordinate migration, Caddyfile, worker reload pipeline and the backend setup.
Or email us directly: kontakt@moselwal.de
Where we use this …
This package carries the PHP runtime in TYPO3 Kubernetes — worker mode, lower startup cost per pod, better utilisation. Managed variant: AI-Ready CMS as a Service.