The template cache system compiles PHP view classes into optimised versions stripped of whitespace, HTML comments, and unnecessary markup, then stores the result in Redis. On subsequent requests the compiled version is loaded directly from cache, skipping file I/O and parsing entirely.
When a view is rendered via $this->render( MyView::class ) or imported via $this->import( MyView::class ), the framework checks TEMPLATES_CACHE_ENABLED and DEV_MODE before deciding how to load it:
render( MyView::class )
└── TEMPLATES_CACHE_ENABLED = true AND DEV_MODE = false?
├── Yes → TemplatesCache::getCachedTemplateClass( MyView::class )
│ ├── Cache hit → load compiled class from Redis → instantiate
│ └── Cache miss → read source file → compile → store in Redis → instantiate
└── No → instantiate MyView::class directly from source
The compiled class is stored under a different namespace (PHP_SF\CachedTemplates) to avoid conflicts with the original source class. When a cache hit occurs, the compiled class definition is loaded via eval() and then instantiated normally.
This means in production, your template PHP files are read from disk exactly once — on the first request after a cache clear. Every subsequent request uses the Redis-cached compiled version.
Two conditions must both be true for template caching to activate:
// config/constants.php
const DEV_MODE = false;
const TEMPLATES_CACHE_ENABLED = true;
DEV_MODE = true always bypasses template caching regardless of TEMPLATES_CACHE_ENABLED. This is intentional — you should never be working against cached templates in development.
Recommended values by environment:
| Environment | DEV_MODE |
TEMPLATES_CACHE_ENABLED |
|---|---|---|
| Development | true |
false |
| Staging | false |
true |
| Production | false |
true |
For the cache system to locate source files, every template directory must be registered with both its path and its PSR-4 namespace during kernel bootstrap:
$kernel = ( new PHP_SF\Kernel )
->addTemplatesDirectory( 'templates', 'App\View' );
Multiple directories can be registered:
$kernel = ( new PHP_SF\Kernel )
->addTemplatesDirectory( 'templates', 'App\View' )
->addTemplatesDirectory( 'templates/Admin', 'App\View\Admin' );
The path is relative to the project root. The namespace must exactly match the namespace declaration in the template files. A mismatch means getCachedTemplateClass() won't find the source file and will return false, falling back to loading the original class directly.
The compiler applies the following transformations to each template file:
Whitespace reduction
<script> blocks (newlines inside scripts are preserved because they matter in JavaScript)Comment removal
<!-- ... -->)// ...) — a negative lookbehind prevents stripping // inside URLs (e.g. href="https://..." is left intact)Markup optimisation
</option>, </li>, </dt>, </dd>, </tr>, </th>, </td>class="foo" → class=foo) — a lookahead ensures quotes are only removed when the attribute is followed by whitespace or >, never before />, so SVG self-closing tags like <path fill="none"/> are left intactNamespace rewriting
App\View) with PHP_SF\CachedTemplates$this->import() calls to use fully qualified class names so imports resolve correctly under the new namespaceImport path resolution
$this->import( SomeView::class ) callsEach compiled template is stored in Redis under:
cached_template_class_{FullyQualifiedClassName}
For example, App\View\welcome_page is cached as:
{SERVER_PREFIX}:{APP_ENV}:cached_template_class_App\View\welcome_page
These keys have no TTL — they persist until explicitly cleared. This is intentional: templates don't change between requests in production, so there's no reason to expire them on a timer.
Template cache keys are cleared along with all other application cache when you run:
php bin/console app:cache:clear
Or via the API endpoint from an allowed host:
GET /api/cache_clear/all
Both clear the entire Redis database, which includes route cache, translation cache, and template cache. After clearing, the next request to each route will recompile that route's templates and re-cache them. On a large application with many templates, the first few requests after a cache clear will be noticeably slower.
In a deployment pipeline, the recommended sequence is:
# 1. Deploy new code
# 2. Clear Symfony cache
php bin/console symfony:cache:clear
# 3. Clear application cache (routes, templates, translations)
php bin/console app:cache:clear
# 4. Optionally warm up by hitting key routes
TemplatesCache is a singleton. Its instance is created on first use and holds the registered directory/namespace mappings in static arrays populated during kernel bootstrap.
The core method is getCachedTemplateClass( string $className ): array|false. It returns either:
[
'className' => 'PHP_SF\CachedTemplates\welcome_page',
'fileContent' => 'namespace PHP_SF\CachedTemplates; final class welcome_page ...',
]
or false if caching is disabled, the class is already in the cached namespace, or the source file can't be located in any registered directory.
The caller (either AbstractController::render() or AbstractView::import()) receives this result and handles it the same way:
if ( TEMPLATES_CACHE_ENABLED ) {
$result = TemplatesCache::getInstance()->getCachedTemplateClass( $view );
if ( $result !== false ) {
if ( class_exists( $result['className'] ) === false )
eval( $result['fileContent'] );
$view = $result['className'];
}
}
$view = new $view( $data );
The class_exists() guard ensures eval() is only called once per class per process. This matters when the same template class is used for multiple purposes within a single request — for example, when the blank middleware sets both the header and footer to the same view class. Without this guard, the second eval() would attempt to re-declare an already-defined class and cause a fatal error.
Before the whitespace and markup transformations run, PHP comments are stripped using token_get_all() and php_strip_whitespace() rather than regex. This is more reliable — it correctly handles comments inside strings, heredocs, and other edge cases that a regex approach would miss.
The result is that /** @noinspection ... */ blocks, // region markers, copyright headers, and all other PHP comments are removed from the compiled output, further reducing the payload stored in Redis.
eval() on every cache miss — the first request for each template after a cache clear uses eval() to define the compiled class. This is a one-time cost per template per deployment, not per request. In production with a warm cache, eval() is never called.
No partial cache invalidation — there is no mechanism to invalidate a single template's cache entry without clearing everything. If you change one view file and want to deploy without a full cache clear, you'd need to manually delete the specific Redis key.
Import path rewriting is best-effort — the compiler rewrites $this->import( SomeView::class ) calls but relies on string matching. Unusual import patterns (dynamically computed class names, variable class references) won't be rewritten correctly and will fail at runtime under the cached namespace. Keep imports as direct class references.
Compiled namespace is fixed — all cached templates live under PHP_SF\CachedTemplates. This namespace is checked in getCachedTemplateClass() — if a class is already in this namespace it is returned as-is without re-caching. Don't manually create classes in this namespace.
Script tag newline preservation — the compiler splits on script> to detect script blocks and preserve newlines inside them. This works for standard inline <script> usage but may behave unexpectedly with unusual script tag formatting. Always verify JavaScript-heavy templates in staging before production.
Forgetting to clear cache after deploying template changes — compiled templates persist in Redis with no TTL. Deploying a new version of a view without clearing cache means users will see the old compiled version until cache is cleared manually. Always run app:cache:clear as part of your deployment.
Enabling TEMPLATES_CACHE_ENABLED with DEV_MODE = true — has no effect. DEV_MODE = true always wins and template caching is bypassed. Don't waste time debugging why changes aren't appearing — check DEV_MODE first.
Namespace mismatch in addTemplatesDirectory() — if the registered namespace doesn't match the actual namespace in your template files, getCachedTemplateClass() silently returns false and templates load from source without caching. No error is thrown. If template caching appears to have no effect, verify the namespace registration in your kernel bootstrap.
Dynamic class names in $this->import() — the compiler cannot rewrite variable-based imports like $this->import( $viewClass ). These will resolve against the original namespace at runtime, which works fine from source but may fail under the cached namespace. Use direct class references in all import() calls.