nyrodev/php-template-bundle
A Symfony bundle by nyrodev that adds PHP-based templating support to your application, with helpers and utilities for rendering views directly in PHP instead of relying on a separate template engine. Lightweight, simple to configure, and easy to drop into existing Symfony projects.
nyrodev/php-template-bundle re-introduces the symfony/templating PhpEngine (removed from Symfony core in 5.0) as a bundle-provided service. Use it when you are maintaining a legacy app on PHP templates, migrating off Symfony 4.x, or when a team is more productive in plain PHP than Twig. It is not a Twig replacement — it is a coexistence layer.
Minimal steps:
composer require nyrodev/php-template-bundle. Confirm the required symfony/templating / symfony/framework-bundle version constraints in its composer.json before committing; the package is small (2 stars, 0 dependents) and version ranges are the most likely source of friction.config/bundles.php:
NyroDev\PhpTemplateBundle\NyroDevPhpTemplateBundle::class => ['all' => true],
php bin/console debug:container --show-private | grep -i templating. You are looking for the PhpEngine service (likely templating or nyro_dev_php_template.engine) and its helper set.templates/ directory, e.g. templates/hello.html.php:
<?php $view->extend('layout.html.php') ?>
<?php $view['slots']->set('title', 'Hello') ?>
<h1>Hello <?= $view->escape($name) ?></h1>
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class HelloController extends AbstractController
{
public function hello(string $name, PhpEngine $engine): Response
{
return new Response($engine->render('hello.html.php', ['name' => $name]));
}
}
Where to look first: the bundle's DependencyInjection/ extension and its service definition file. Read those before the README — for a package this size the DI config is the documentation, and it tells you exactly which helper set is registered (slots, request, session, router, assets, translator, actions) and under which service id.
First use case: a single legacy "dashboard" or "print" view that a designer edits directly. Prove the round-trip (layout + slot + $view->escape) works before committing to a full migration strategy.
Layout inheritance via slots. PHP templates have no {% extends %}. The equivalent is $view->extend(), which must be the first statement in the file. Output the child body with $view['slots']->output('_content') in the layout:
{{# layout.html.php #}}
<!DOCTYPE html>
<html>
<head>
<title><?= $view->escape($view['slots']->get('title', 'App')) ?></title>
</head>
<body>
<?php $view['slots']->output('_content') ?>
</body>
</html>
Partials via the engine, not include. include will not inject $view. Use the engine so helpers are available:
<?= $view->render('partials/flash.html.php', ['messages' => $messages]) ?>
Controller convention. Wrap rendering in a small trait or abstract controller so injection does not leak into every action:
trait PhpTemplatingTrait
{
protected function renderPhp(string $template, array $vars = []): Response
{
return new Response($this->container->get('templating')->render($template, $vars));
}
}
Then actions stay readable and the swap point stays in one file.
Mixing with Twig. Run both engines side by side during migration. New features go into .twig, legacy forms and emails stay in .html.php. Route the choice by file extension in a DelegatingEngine-style wrapper, or simpler: keep the PHP render call explicit (renderPhp()) and never rely on AbstractController::render() for PHP templates (see Gotchas).
Forms and error rendering. $view['form'] is not part of the standard templating helper set, and Symfony's form theme renderer is Twig-first by default. Either register a small custom helper that proxies FormRenderer, or keep forms in Twig and embed them in a PHP layout via a sub-request. The latter is usually less work.
Emails. Transactional mail is the strongest use case for this bundle. If you wire the engine into MailerInterface via a TemplatedEmail-equivalent or a small wrapper, PHP templates let non-Symfony developers edit email bodies without learning Twig syntax.
Testing. Assert on the rendered string ($engine->render(...)) rather than on the service. Because PHP templates are compiled by OPcache rather than by a template cache layer, tests see changes immediately without a cache warmup — a real ergonomic advantage over Twig.
No autoescaping. This is the single biggest footgun. Every dynamic value from user input needs $view->escape($value) (or $view->escape($value, 'js'|'css'|'url')). Twig's safety-by-default does not exist here. Add a lint rule or code review checklist item; XSS regressions in PHP templates are common and silent.
AbstractController::render() is Twig-only. In Symfony 5+, the base controller hardcodes the twig service. Calling $this->render('foo.html.php') will fail or render through Twig. Always fetch the templating engine explicitly (injected or $this->container->get('templating')). Do not rely on suffix-based engine delegation — the DelegatingEngine that used to make that work is not present in modern Symfony.
Helper availability is narrower than you remember. The router, assets, actions, and form helpers all lived in FrameworkBundle/TwigBundle and were removed alongside the engine. Check the bundle's service definitions for what is actually registered; for anything missing, implement a HelperInterface and register it on the helper set (or via a compiler pass) rather than calling $this->urlGenerator from inside the template.
Template resolution. Legacy Bundle:Controller:template notation versus modern @Bundle/... and templates/ paths — confirm which the bundle's TemplateNameParser supports. If both Twig and PHP templates live in templates/, keep extensions distinct (.html.php vs .html.twig) so neither loader picks up the wrong file. IDE indexers and static analysis tools will flag .php files under templates/; add an exclusion.
extend() must be first. Anything before it (even a blank line with echo) will produce a template that renders without a layout and is painful to debug. PHP templates fail with real line numbers, though — use that. php bin/console cache:clear is not usually needed for template changes, but is for container/service changes.
Debugging blank output. If a route returns an empty body, check in this order: (1) $view is undefined (you used include instead of $view->render), (2) extend() called after output, (3) a helper you referenced is not registered — the engine throws InvalidArgumentException for unknown helpers, which is easy to swallow in a try/catch higher up.
Extension points to know. Custom helpers via a class implementing HelperInterface registered on the engine's helper set; custom globals via GlobalVariables; and a compiler pass if you need to swap the template loader (e.g. to load PHP templates from a DB or a plugin directory).
Small-package risk. With 2 stars and no dependents, expect no security advisories, no LTS policy, and possible breakage on Symfony majors. If this bundle becomes load-bearing, either vendor the specific engine classes into your app (src/Templating/) or fork and track upstream. It is a thin wrapper around a component Symfony published for years — copying it is cheap insurance, and removes a transitive dependency you don't control.
How can I help you explore Laravel packages today?