runroom/samples-bundle
Runroom Samples Bundle provides a simple Symfony integration for managing sample data, helping teams define reusable examples and load them consistently for development, testing, and demos.
This is a Symfony bundle, not a Laravel package, so the mental model for a Laravel dev is: it is View::composer('*', ...) plus a composer-style hook that fires on every Twig render instead of only on view() calls. One event, dispatched right before the template is handed to Twig, carrying the template name and the variable bag so listeners can mutate it.
Minimal setup:
composer require runroom/samples-bundle
Then register it in config/bundles.php (Symfony Flex usually does this for you):
Runroom\SamplesBundle\RunroomSamplesBundle::class => ['all' => true],
Look at these first, in this order — the repo is small, so you can read the whole src/ in ten minutes:
getTemplate() / getParameters() or similar accessors).DependencyInjection/ extension and any services.xml/services.yaml, to confirm what is auto-registered and what you must wire yourself.First use case, the one that actually justifies the dependency: stop repeating the same "chrome" variables in every controller. Instead of injecting SEO metadata, breadcrumbs, a cart count, or a feature-flag object into dozens of actions, register one subscriber and decorate the render:
final class GlobalViewDataSubscriber implements EventSubscriberInterface
{
public function __construct(private SeoBuilder $seo, private Cart $cart) {}
public static function getSubscribedEvents(): array
{
return [RenderEvent::class => 'onRender'];
}
public function onRender(RenderEvent $event): void
{
if (str_starts_with($event->getTemplate(), 'admin/')) {
return;
}
$event->setParameter('seo', $this->seo->forCurrentRequest());
$event->setParameter('cartCount', $this->cart->count());
}
}
That's the whole value proposition: one central place where view data is composed, decoupled from controllers, testable in isolation.
Guard by template prefix, not by controller. Because the event fires at the rendering layer, it has no idea which route or controller you came from. Filter on getTemplate() ('layout.html.twig', 'emails/', 'partials/') or on a request attribute pulled from the request stack via RequestStack. This is the single most important pattern; unguarded subscribers are how this bundle turns into a footgun.
Layered subscribers over one fat one. Register several small subscribers — SeoSubscriber, NavigationSubscriber, FeatureFlagSubscriber — each with its own template guard. Symfony runs them by priority (default 0, higher first), which lets you guarantee ordering, e.g. the navigation subscriber that populates breadcrumbs runs before the SEO subscriber that consumes them by declaring priority: 10.
Use it over Twig globals when the value is request-dependent. Twig globals: are resolved once at container/extension setup and re-resolved per request but not per template; a global is fine for a static config object, wrong for anything that varies by template ("no cart widget on checkout") or is expensive. The render event is per-render, so it lets you skip work for templates that will never use the variable.
Use it instead of Twig\Extension\AbstractExtension::getGlobals() when you need the template name. This is the genuinely differentiating capability: getGlobals() cannot see which template is rendering. Anything you'd naturally reach for {% include %} context hacks or a {{ render(controller(...)) }} sub-request to solve can often be replaced by a guarded listener here — cheaper, since no sub-request or container round-trip is involved.
Testing. The event object is a plain value object, so unit-test listeners by constructing it with a template name and parameters, calling onRender(), and asserting on the mutated bag. No kernel, no Twig environment, no fixtures. If the bundle's event class is final with no named constructor, use reflection sparingly or test through a functional WebTestCase rendering an actual template.
Integration tip for Laravel-brained code: do not try to reach for request()-style helpers or service location. Wire dependencies in the constructor and keep the listener stateless per request; the event carries only what Twig will see.
render(), the event fires for every {% include %}, {% embed %} and macro-bearing partial too, not just top-level controller renders. That means a per-render guard, and it means your subscriber can run dozens of times per page. Measure with a counter in dev before you assume once-per-request semantics.setParameter() (merge) or setParameters() (replace). Calling the replace-style setter from two listeners is a classic "why did my SEO object vanish" bug. Prefer the merge-style API, and if only the replace-style exists, always array_merge($event->getParameters(), [...]).Markup/Twig\Markup unless you deliberately want to defeat escaping.ServiceLocator or defer heavy DB calls behind a flag that the template guard has already narrowed.composer.json and read the diff before upgrading major Symfony versions — the render lifecycle is an area Symfony has historically churned (kernel.view, Twig\Environment decoration). If the package is a thin decorator, keep your subscribers in your own namespace so you can swap in a hand-rolled 20-line Twig\Extension decorator later with minimal diff.RequestStack::getMainRequest() and getCurrentRequest() differ; any subscriber that pulls routing or locale data will silently use the fragment's request. Decide explicitly which you want.Twig\Environment::render() yourself. If the bundle already does that, don't decorate again — double dispatch is the fastest way to get duplicated variables and doubled listener cost.dump($event->getTemplate()) at the top of each subscriber in dev tells you immediately whether it fires once or thirty times and for which templates. Do this on day one; it answers the question that the README almost certainly doesn't.How can I help you explore Laravel packages today?