runroom-packages/render-event-bundle
A Symfony bundle that dispatches events around response rendering, letting you hook into the render lifecycle to inspect or modify output before it reaches the client. Works with standard event subscribers and integrates with other Runroom packages.
Install the bundle via Composer:
composer require runroom-packages/render-event-bundle
Register it in config/bundles.php (Symfony's flex recipe may not exist for this package, so verify manually):
return [
// ...
Runroom\RenderEventBundle\RunroomRenderEventBundle::class => ['all' => true],
];
Where to look first: Open vendor/runroom-packages/render-event-bundle/src/ and read the bundle class plus the event class (typically Runroom\RenderEventBundle\Event\RenderEvent or TemplateEvent). The bundle is small — you can read the whole thing in under 10 minutes, and since documentation is thin, source is the documentation.
First use case: Intercept every Twig template render and inspect or mutate the template name and parameters. A minimal listener that tags every render with a request ID:
namespace App\EventListener;
use Runroom\RenderEventBundle\Event\RenderEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: RenderEvent::class)]
final class RenderAuditListener
{
public function __invoke(RenderEvent $event): void
{
// e.g. log what's about to render, or inject parameters
$event->setParameters($event->getParameters() + [
'_render_started_at' => microtime(true),
]);
}
}
That listener demonstrates the whole value proposition: a hook to observe and shape templates before Twig compiles output.
Parameter injection for shared context. The most reliable pattern is mutating parameters in the listener. Use it for global vars (locale, feature flags, a request-scoped nonce for CSP) without writing a Twig extension:
public function __invoke(RenderEvent $event): void
{
$params = $event->getParameters();
$params['_csp_nonce'] = $this->nonceProvider->get();
$event->setParameters($params);
}
Route/method-scoped behavior. Because the event carries the template name, you can scope mutations:
if (!str_starts_with($event->getTemplate(), 'admin/')) {
return;
}
Prefer str_starts_with over regex for readability and performance.
Profiling / debugging integration. Wire the listener into the Symfony profiler or a Stopwatch collector to see per-template timings. This is the primary reason people reach for such a package on legacy apps with heavy template hierarchies.
Subscriber pattern over multiple listeners. If you have many hooks (feature flags, nonces, audit), consolidate into a single EventSubscriberInterface::getSubscribedEvents() returning RenderEvent::class => ['onRender', 10]. This keeps ordering explicit — use priorities to control whether your parameters are available to later listeners.
Testing. Because the event is a plain object, unit-test listeners in isolation:
$event = new RenderEvent('foo.twig', ['a' => 1]);
(new MyListener())($event);
self::assertArrayHasKey('_render_started_at', $event->getParameters());
For end-to-end coverage, use Symfony's KernelBrowser and assert on Response content — the event itself fires synchronously during render.
Integration tip — combine with kernel.response listeners carefully. The render event fires before Twig output is generated, so anything you inject must be a valid Twig value (not a Response or stream). Don't try to short-circuit rendering from this listener; it's an observer/mutator, not a replacement for kernel.controller or a Twig\RuntimeLoader.
Very low adoption signals risk. 2 stars and 0 dependents mean: no community examples, no Stack Overflow answers, and you are the primary maintainer of any bug you hit. Before adopting into production, copy the bundle's source into your app (composer repositories path override or a src/Bundle/ folder) so it's under your control. It's small enough that this is cheap.
Twig caching masks changes. Twig caches compiled templates under var/cache/{env}/twig/. If you edit parameters logic and don't see effects, bin/console cache:clear — not the browser refresh — is your debugging step. In dev with auto_reload: true this usually resolves itself, but the cache directory can go stale after composer install with a running PHP process (FPM). Restart workers/consumers after installing.
Parameter mutation is order-dependent. If two listeners both call setParameters() with a full replacement array (not + merge), the last one wins and silently drops earlier changes. Always prefer:
$event->setParameters($event->getParameters() + $new; // union preserves existing keys
or read-modify-write patterns. Never pass a bare $event->getParameters() without merging if you intend to add.
Performance on hot paths. This event fires per-render, which on fragment-heavy pages or sub-requests can be hundreds of calls. Keep listener bodies allocation-free where possible, avoid hydrating services that perform I/O in the constructor, and make services lazy:
services:
App\EventListener\RenderAuditListener:
lazy: true
Benchmark with Blackfire before shipping — the cost is usually the listener, not the dispatch.
Sub-requests double-fire. HttpKernel sub-requests (e.g. render(controller(...)) in Twig) render templates too, so the event will fire multiple times per response. If your listener assumes one call per request, guard on $request->isMainRequest() — you'll need to inject RequestStack into the listener to check.
Event object is mutable and not final-guarded. Treat the event as owned by Twig rendering; don't store it in a service and mutate it later. Copy the values you need in the listener if you must retain them.
Extension points are limited by design. The package doesn't ship a formatter, DI tag autoconfiguration, or config tree typically — if you need behavior beyond "listen and mutate," you're writing it yourself. The natural extension points are:
kernel.event_listener/subscriber as above.Twig\Extension\AbstractExtension if you need functions/filters instead.RequestStack check for request-scoped logic.Version pinning. Given the low activity, pin to a specific release in composer.json ("runroom-packages/render-event-bundle": "1.x" or a dev- commit hash) rather than *. A surprise BC break from a solo-maintained bundle will be discovered the hard way otherwise.
Debugging dispatch. If a listener isn't firing, the check order is: (1) bundle actually registered in config/bundles.php for the env, (2) service autoconfigured/tagged (verify with bin/console debug:event-dispatcher Runroom\\RenderEventBundle\\Event\\RenderEvent), (3) event class name matches exactly — a typo in the FQCN inside #[AsEventListener] fails silently in production because the attribute is lazy. debug:event-dispatcher is the fastest ground truth.
PHP version and Symfony compatibility. Check the bundle's composer.json require block against your Symfony/PHP version before adding it, not after composer require resolves to something incompatible. For an orphaned bundle, the supported version matrix is often narrower than what you run.
How can I help you explore Laravel packages today?