cakephp/twig-view
TwigView integrates the Twig templating engine into CakePHP, letting you build views with clean Twig syntax while using CakePHP helpers, layouts, and conventions. A flexible alternative to PHP templates with extension support.
cakephp/twig-view is the CakePHP view-layer plugin that swaps the default PHP templates for Twig. If you're arriving from Laravel, think of it as "Blade's job, done by Twig" — same role, different runtime; the integration surface is CakePHP's ViewBuilder, not a service provider, so none of your Laravel Twig-bridge knowledge transfers directly.
Minimal setup:
composer require cakephp/twig-view — match the major line to your framework: 1.x is the maintenance branch (CakePHP 4-era), 2.x is the current line for CakePHP 5. Confirm against Packagist before pinning.src/Application::bootstrap():
$this->addPlugin('TwigView');
ViewBuilder::setClassName('TwigView\View\TwigView') in AppView::initialize(), or per-controller with $this->viewBuilder()->setClassName('TwigView\View\TwigView') inside initialize().templates/Pages/home.php to home.twig, and the layout to templates/layout/default.twig. Twig templates resolve from the same templates/ tree using the .twig extension.Where to look first: the plugin's docs/ directory (the README is thin), then src/View/TwigView.php for how the Twig environment is constructed, and src/Extension/ for what CakePHP helpers/functions are pre-registered.
First real use case: migrate one read-only action — an index/list page — end to end, including its layout and one element. That exercises template resolution, helper access, and autoescaping in about an hour and surfaces the layout-mismatch problem (below) before you scale to the whole app.
Upgrade note for this release: 1.3.3 is a security backport to the 1.x branch. If you're on 1.3.2 or earlier, treat it as a priority patch (^1.3.3) and render-smoke-test your templates afterwards — security fixes in a templating layer can alter what's callable or how output is escaped.
Incremental, controller-by-controller migration. The cleanest adoption pattern is per-controller setClassName() rather than a global switch. Bake-generated PHP templates keep working for untouched controllers while you convert the ones you care about. Do not try a big-bang conversion of a mature app.
Layout and element co-resolution is the crux. A Twig view cannot extend a PHP layout. When you convert an action, convert its layout and every element it includes in the same commit. Elements are reachable either as Twig includes ({% include 'element/sidebar' %}) or through the view object, which is exposed as _view — {{ _view.element('sidebar') }}.
Helpers are top-level Twig variables. CakePHP helpers land in the context by name, so {{ Html.link('Edit', {'action': 'edit', id}, {'class': 'button'}) }} and {{ Form.create(post) }} read naturally. Anything the plugin doesn't expose, reach through _view: {{ _view.Url.build('/foo') }}. This is the seam to study first — when a helper call fails, it's almost always "wrong variable name" or "wrong argument shape," not a Twig problem.
Translation and formatting come through as Twig functions ({{ __('Hello') }}, {{ __d('domain', 'Message') }}), so i18n keeps working without extra wiring.
Register your own Twig extensions in bootstrap. Custom filters/functions go through the plugin's Configure keys in Application::bootstrap() — this is the extension point for app-specific filters like a money formatter or a markdown filter. Keep them in a small src/Twig/ namespace and register as a list; don't monkey-patch the environment from a view.
Escaping discipline. Twig autoescapes by default. Helper output that is already markup needs |raw, and that is precisely where XSS creeps in. Set a rule for the team: |raw only on values produced by a CakePHP helper, never on request data.
Layouts with blocks. {% extends 'layout/default' %}, {% block content %}, and {{ _view.fetch('content') }} inside the layout mirror the PHP-template equivalent directly, so a layout port is mostly mechanical transcription.
This is not a Laravel package. No @section/@yield directives, no ServiceProvider, no Blade components. If your team is Laravel-first, budget for the mental shift on helper access (Html.link(...) vs $this->Html->link(...)) and on error output — Twig's exception pages point at template paths relative to templates/, which is fine but unfamiliar.
Compiled-template cache is the #1 time sink. Twig caches compiled templates under tmp/cache/. After changing configuration, adding extensions, or pulling a version bump, clear it (bin/cake cache clear_all, or delete the twig cache directory) before concluding that "nothing changed." In dev, enable Twig's debug option so you get dump() and useful compile errors instead of a blank 500.
Mixed extensions fail loudly and late. A .twig view extending a .php layout throws at render time, not at boot. Grep for layout and element( before declaring a controller converted.
Version-line mismatch. Because 1.x is now in maintenance mode with security-only backports, don't plan new features around it. If you're starting fresh on CakePHP 5, target 2.x; if you're pinned to 1.x, pin ^1.3.3 and expect no new capabilities — only fixes.
Post-upgrade verification for 1.3.3. The changelog is a single line ("backport security fixes"), with no CVE detail in the notes. Pull the linked PR or the security advisory to know what class of issue it is, then smoke-test the templates most likely affected: anything using |raw, anything rendering user-supplied content, and anything calling into helpers with dynamic arguments. Budget a quick manual pass over the layouts of your highest-traffic pages.
Debugging strategy that actually works. When a template silently renders wrong, first check whether the variable is in the context at all ({{ dump(_context) }} with debug on), then whether escaping is mangling it, then whether a stale compiled template is serving old code. That order resolves roughly everything.
Extend rather than fork. The plugin is small; app-specific behavior belongs in a registered Twig extension or a subclass of TwigView set as the view class name. Forking the plugin to patch escaping or helper dispatch will make the next security backport painful — exactly the situation 1.3.3 creates.
How can I help you explore Laravel packages today?