Choosing Between Twig and Legacy PHP Templates in Contao 4.9–5.x
Decision guide for Contao 4.9–5.x: why new projects should use Twig, how to migrate legacy PHP templates incrementally, and a concrete validation checklist with commands and caveats.
02 Jul 2025, 23:33 UTC

The Decision: Start New Projects with Twig; Migrate Incrementally
Contao 4.9 made Twig the default template engine. Legacy .php5 templates still render but are deprecated — they receive no new features, block access to Twig's auto-escaping and inheritance, and trigger deprecation warnings in production logs. For teams maintaining Contao 4.9 through 5.x LTS, the practical rule is: new projects use Twig exclusively; existing projects migrate layout and page templates first, then content elements module by module.
Constraints That Shape the Choice
- Mixed rendering is supported: Contao reads both
.html.twigand.php5files in the same request. A page can use a Twig layout while rendering legacy content-element templates. - Custom PHP logic in templates must move out: Database queries, service calls, or business logic inside
.php5files cannot migrate directly. They belong in event listeners, models, or custom Twig extensions. - Third-party extension coverage varies: Core bundles (news, calendar, form, metamodels) ship Twig templates since 4.9. Some smaller extensions still ship only
.php5— verifycomposer.jsonforextra.contao.template-pathpointing to Twig directories before relying on them in 5.x. - Cache clearing is mandatory: After adding or renaming Twig files, run
contao:cache:clear. OPcache may serve stale compiled classes otherwise.
Supported Options Compared
| Aspect | Twig (.html.twig) | Legacy PHP (.php5) |
|---|---|---|
| Default in Contao | Yes, since 4.9 | Deprecated, retained for compatibility |
| Auto-escaping | Built-in (context-aware) | Manual htmlspecialchars required |
| Template inheritance | extends/block with namespace paths (e.g., @ContaoCore/layout.html.twig) | Include/override via filesystem paths only |
| Sandboxing | Enabled by default; restricts PHP function calls | Full PHP access — no guardrails |
| Debugging | Symfony Profiler: template tree, compilation time, variable dump | var_dump / logging only |
| Performance (warm) | Compiled to cached PHP classes; OPcache applies | Direct PHP execution; OPcache applies |
| First-render overhead | Negligible after warm-up | None |
| Custom filters/functions | Via Twig extensions (whitelisted in security policy) | Direct PHP calls |
| Migration path | Community script contao:migrate:templates converts simple templates | One-way — no automated conversion to Twig |
Trade-offs in Practice
Security and Maintainability
Twig's auto-escaping eliminates a whole class of XSS vulnerabilities that plague raw PHP templates when developers forget htmlspecialchars. The sandbox prevents templates from calling arbitrary PHP functions — a deliberate constraint that forces logic into testable PHP classes. Legacy templates offer no such protection; a single eval or database call in a template becomes a maintenance liability.
Developer Experience
Teams familiar with Symfony or Laravel adapt to Twig syntax immediately. The Symfony Profiler's Twig panel shows the exact template inheritance chain, compilation time per template, and every variable available at each block — impossible with legacy templates. However, existing Contao developers accustomed to inline PHP may face a short learning curve for filters, macros, and namespace-based inheritance (@ContaoCore/... instead of filesystem paths).
Performance Reality
Benchmarks show no meaningful difference after warm-up. Twig compiles templates to PHP classes cached in var/cache/prod/; OPcache serves them identically to hand-written PHP. First render after cache clear adds ~5–15 ms per template — negligible for typical page loads. The legacy path avoids compilation but loses opcode optimization benefits when templates contain complex logic.
Migration Cost
Converting a 200-line layout template with conditionals and loops takes 30–60 minutes manually. The community script php vendor/bin/contao-console contao:migrate:templates handles simple variable output and basic control structures but cannot translate custom PHP logic. Budget migration per template, not per project.
Concrete Migration Sequence
- Audit current templates: List every
.php5file intemplates/andsrc/Resources/contao/templates/. Flag files containing PHP logic beyond variable output (foreach,if, function calls, database queries). - Move logic out of templates: For each flagged template, extract business logic into:
- Event listeners (e.g.,
Contao\CoreBundle\EventListener\TemplateEvent) - Model methods or repository classes
- Custom Twig extensions (register in
services.yamlwith tagtwig.extension)
- Event listeners (e.g.,
- Migrate layout and page templates first: These control global structure and affect every page. Convert
fe_page.html5→fe_page.html.twig,layout_full.html5→layout_full.html.twig. Useextends '@ContaoCore/layout.html.twig'for inheritance. - Migrate content elements module by module: Start with high-use elements (text, image, headline). Run the migration script on each module's template directory, then manually fix any unconverted logic.
- Verify in dev mode: Enable Symfony Profiler (
APP_ENV=dev), render each migrated page, and check the Twig panel for:- Template tree matches expected inheritance
- No deprecation warnings in
var/log/dev.log - Variable dump shows expected data at each block
- Clear production cache:
php vendor/bin/contao-console contao:cache:clear --env=prodthen warm OPcache with a few requests.
Validation Checklist
- Fresh Contao 5.3 project via Contao Manager shows only
.html.twigfiles intemplates/. - Adding a test
.php5file intemplates/custom/renders but logs deprecation warning invar/log/prod.log. - Symfony Profiler Twig panel displays template lineage and compilation metrics.
- Custom Twig filters/functions work after whitelisting in
config/packages/twig.yamlundertwig.security_policy. - All third-party extensions in
composer.jsondeclareextra.contao.template-pathpointing to Twig directories.
Limitations and When to Pause
- If a third-party extension ships only
.php5templates and has no maintainer, you must either fork and port templates or keep that module on legacy rendering — which is supported but not future-proof. - Complex custom PHP logic (e.g., dynamic SQL built in templates) requires architectural refactoring before Twig migration; don't force it through Twig extensions.
- Twig sandbox may break templates that rely on PHP functions like
date()orarray_map()directly. Whitelist viatwig.security_policyor rewrite using Twig filters.
Bottom Line
Twig is the supported, maintained path. Legacy templates are a compatibility bridge, not a long-term option. Migrate layout and page templates first — they deliver the biggest security and maintainability gains — then work through content elements at a sustainable pace. The tooling and ecosystem are ready; the only blocker is unported third-party templates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.