The template cache system compiles PHP view classes into optimised versions stripped of whitespace, HTML comments, and unnecessary markup, then writes the result to disk as a plain PHP file. On subsequent requests the compiled file is loaded via require, letting OPcache serve the bytecode directly from shared memory — no file parsing, no network round-trip.
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 )
│ ├── File exists → require compiled file (OPcache hit) → instantiate
│ └── File missing → read source → compile → write file → require → instantiate
└── No → instantiate MyView::class directly from source
The compiled class lives under a different namespace (PHP_SF\CachedTemplates) to avoid conflicts with the original source class. Once the file is loaded, OPcache keeps its bytecode in shared memory for the lifetime of the worker process — subsequent requests pay nothing beyond a file_exists() check and a require that OPcache intercepts.
This means in production, your template PHP files are read from disk exactly once per deployment — on the first request after a cache clear. Every subsequent request uses the OPcache-resident 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 written to:
var/cache/templates/{ClassName}.php
Backslashes in the class name are replaced with underscores:
| Source class | Compiled file |
|---|---|
App\View\welcome_page |
var/cache/templates/PHP_SF_CachedTemplates_welcome_page.php |
App\View\Admin\Dashboard |
var/cache/templates/PHP_SF_CachedTemplates_Admin_Dashboard.php |
These files contain a valid PHP class definition and can be opened directly for inspection or debugging. Stack traces from errors inside a cached template point to the compiled file and line number, not to an anonymous eval() string.
The var/cache/ directory is excluded from version control.
Template compiled files 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
The command deletes all *.php files under var/cache/templates/ and calls opcache_invalidate() on each so the bytecode is evicted from shared memory immediately. After clearing, the next request to each route will recompile that route's templates and write new files. 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 constructor also ensures var/cache/templates/ exists, creating it if necessary.
The core method is getCachedTemplateClass( string $className ): string|false. It returns the cached class name (e.g. PHP_SF\CachedTemplates\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(), AbstractView::import(), or Response::send()) uses the result like this:
if ( TEMPLATES_CACHE_ENABLED )
$view = TemplatesCache::getInstance()->getCachedTemplateClass( $view ) ?: $view;
$view = new $view( $data );
getCachedTemplateClass() handles loading the class itself — when it returns a class name, that class is already defined in the current process. The caller just instantiates it.
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 size of the file written to disk.
First request after cache clear is slower — the compile pipeline (comment stripping, 29 regex transformations, namespace rewriting, atomic file write) runs once per template on cache miss. In production with a warm cache this never happens.
No partial cache invalidation — there is no mechanism to invalidate a single template's compiled file 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 file from var/cache/templates/ and call opcache_invalidate() on it.
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.
OPcache required for maximum performance — compiled files are plain PHP and work without OPcache, but each new worker process will then parse the file on first load. With OPcache enabled, the parsed bytecode is shared across all workers and persists for the lifetime of the pool. OPcache is strongly recommended in production.
Forgetting to clear cache after deploying template changes — compiled files persist on disk with no expiry. 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.
Stale OPcache after manual file deletion — if you delete a compiled file manually without calling opcache_invalidate(), OPcache may continue serving the old bytecode from shared memory. Use app:cache:clear which handles both steps, or call opcache_invalidate( $filePath, true ) explicitly after manual deletion.