Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Templates System Bundle Laravel Package

swp/templates-system-bundle

Symfony bundle for managing reusable templates and layouts. Provides a central template registry, theme and layout resolution, and rendering helpers so applications can compose views consistently across modules without duplicating markup.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is. swp/templates-system-bundle is the Twig-backed template layer extracted from Superdesk Web Publisher (SWP). Instead of reading Twig source only from the filesystem, it lets you store template source in the database, group page regions into named containers, fill those containers with widgets, and map templates to routes. If you have ever wanted "CMS-editable Twig" inside a Symfony app, this is the shape of that bundle.

Minimal install.

composer require swp/templates-system-bundle

Register the bundle (legacy Symfony in AppKernel.php; Symfony 4+ in config/bundles.php):

// app/AppKernel.php
$bundles = [
    // ...
    new SWP\Bundle\TemplatesSystemBundle\SWPTemplatesSystemBundle(),
];

The bundle ships Doctrine ORM mappings for the template/container/widget graph, so Doctrine must be configured. Create the tables:

bin/console doctrine:schema:update --force
# or generate a migration if you don't use schema:update

Where to look first — read these in this order:

  1. Model/Template.php — the DB template: name/code, body, optional container, active flag.
  2. Model/Container.php and Model/Widget.php — the page-region graph.
  3. Twig/ — the Twig extension (renderContainer, renderWidget, …) and the DB-backed loader.
  4. DependencyInjection/Configuration.php — the real config surface (usually small).
  5. Resources/config/services.yml — service IDs are your extension points, and IDs drift between versions, so trust the YAML over any blog post.

First use case — render a DB template with one container.

  1. Create a template row named homepage whose body is:
    <h1>{{ page_title }}</h1>
    {{ renderContainer('main') }}
    
  2. Create a container named main.
  3. Add a widget to it, type html, data {"html": "<p>Hello</p>"}.
  4. Render it:
    $html = $this->get('swp_template_system.twig')
        ->render('homepage', ['page_title' => 'Welcome']);
    

If that round-trips, you understand the package: Twig source from the DB, regions from containers, content from widgets.

Implementation Patterns

1. Chain the DB loader ahead of the filesystem loader. The bundle's loader is what makes render('homepage') hit Doctrine. Register it inside a Twig chain so filesystem templates (@App/layout.html.twig, bundle templates) still resolve as a fallback:

# config/packages/twig.yaml
twig:
    loader: twig.loader.chain
services:
    swp_template_system.twig.loader:
        # verify the exact class path in Resources/config/services.yml
        class: SWP\Bundle\TemplatesSystemBundle\Twig\Loader\TemplateLoader
        arguments: ['@doctrine.orm.entity_manager']
        tags: [{ name: twig.loader, priority: 10 }]

The durable pattern: keep layout chrome in filesystem templates (versioned, code-reviewed) and page bodies in DB (editable). The layout does {% extends '@App/layout.html.twig' %} and {% block body %}{{ renderContainer('main') }}{% endblock %}.

2. Build pages out of named regions. The canonical SWP layout:

{# layout.html.twig #}
<header>{{ renderContainer('header') }}</header>
<main>{{ renderContainer('main') }}</main>
<aside>{{ renderContainer('sidebar') }}</aside>
<footer>{{ renderContainer('footer') }}</footer>

Containers are rows; widgets belong to containers. This gives non-developers a "drag widgets into a region" workflow without touching Twig.

3. Widget handlers are where your code lives. A widget is (type, data). The renderer picks a handler for the type. Implement one handler per widget type, tag it, and return HTML — usually by delegating to a normal Twig file:

final class HtmlWidgetHandler implements WidgetHandlerInterface
{
    public function __construct(private Environment $twig) {}

    public function isSupported(Widget $widget): bool
    {
        return $widget->getType() === 'html';
    }

    public function render(Widget $widget): string
    {
        return $this->twig->render('@App/widgets/html.html.twig', [
            'data' => $widget->getData(),
        ]);
    }
}
services:
    App\Widget\HtmlWidgetHandler:
        tags: [{ name: swp.templates_system.widget_handler }]

(Check Widget/ for the exact interface name and tag ID in your version.)

4. Assign templates to routes, don't hardcode them. The bundle's assignment layer maps a template to a route/pattern. For anything nontrivial, decide the template in a listener and stash it as a request attribute:

public function onKernelRequest(RequestEvent $event): void
{
    $request = $event->getRequest();
    // pick 'article', 'category', 'homepage' based on route + tenant
    $request->attributes->set('_swp_template', 'article');
}

Keep assignment logic in one service. Scattering if ($request->get('_route') === ...) across controllers is the classic mess.

5. Enrich the render context with a template-event listener. Almost every real app needs "the current article," "menus," "site settings" available to every DB template. Listen to the bundle's template-render event and add them:

public function onTemplateRender(TemplateEvent $event): void
{
    $event->getContext()['menus'] = $this->menuRepository->findForCurrentSite();
}

This keeps template and widget authors from needing bespoke PHP.

6. Render through a thin service, not service IDs. Wrap the engine so controllers never touch container internals:

final class PageRenderer
{
    public function __construct(private Environment $twig) {}

    public function render(string $template, array $context = []): string
    {
        return $this->twig->render($template, $context);
    }
}

Then return new Response($this->pageRenderer->render('homepage', $ctx));.

7. Test the data layer, snapshot the Twig layer. Unit-test widget handlers with a real Twig env and a hand-built Widget; integration-test the loader and container renderer against a test DB. Snapshot rendered HTML per template so edits show up as diffs.

8. Cache rendered regions. Wrap renderContainer output in a cache keyed by (container name, tenant, template version). The DB round-trips per container/per widget add up quickly on listing pages.

Gotchas and Tips

Compatibility is the first gate. Last release was June 2018; it targets Symfony 2.8/3.x, Twig 1/2, and PHP 7.x. On Symfony 5+, twig.loader tag semantics, Twig_* class names, and $container->get() calls will break. Budget a fork-and-port or pin an old stack — do not assume composer require just works.

Almost nobody uses this outside SWP. Zero stars, zero dependents means no StackOverflow answers and no third-party docs. Resources/config/services.yml and the Twig/ directory are the documentation.

Template names are global. The DB loader keys by name/code. Duplicate names across sites/tenants collide; enforce uniqueness at the DB and application level. Template paths are names, not directories — foo/bar.twig is just an identifier.

Chain-loader order matters. DB loader last → filesystem templates shadow DB templates of the same name. DB loader first → a bad row can shadow a real bundle template, and you only discover it after deleting the row. Use DB first for editable page names, but namespace them (page/homepage) to avoid collisions.

Container names fail silently or loudly — know which. renderContainer('main') with no main row either throws or renders nothing, depending on version. Add a dev-mode preflight check so a typo doesn't ship a blank page.

Rendering is RCE by design. Template bodies are Twig. Anyone who can write a template row can execute arbitrary logic through Twig. Never expose template editing to untrusted users; consider a Twig sandbox and an audit log. This is the single biggest operational risk of DB-backed templates.

Cache invalidation on template edit. Twig compiles templates, so a DB row change needs the compiled/loader cache cleared. Wire a Doctrine postUpdate/postPersist listener on Template (and Widget) to clear the Twig cache — otherwise editors save, see no change, and file a bug.

N+1 on containers and widgets. renderContainer typically issues a query for the container plus one per widget, often plus one per handler for related content. Eager-load widgets and their relations in the repository, or cache the rendered region. Profile with the Doctrine logger before shipping a page with 20 widgets.

Widget data is untyped JSON. It comes back as an array; renaming a key silently breaks every existing widget. Version the data shape or validate it in the handler with a clear exception, and make handlers tolerant of missing keys.

Template assignment precedence. If multiple rules can match, precedence is easy to misread. Write down the order (route > pattern > default) and test it. Keep assignment in one listener/service.

Multi-tenancy is not included. SWP layers tenancy on top; the bundle alone has no tenant scoping. With multiple sites you must scope templates/containers/widgets per tenant yourself and make cache keys tenant-aware.

Bundle owns the schema. The entities/mappings ship with the bundle, so doctrine:schema:update creates the tables but you don't own migrations. Extending an entity means inheritance mapping or a fork — decide early.

Upgrading Twig. Twig 2/3 removed Twig_Environment, changed loader interface signatures, and added Source/TemplateWrapper in 3. If you port, replace Twig_* with Twig\*, update loader method signatures, and rerun the full test suite against real DB templates.

Debugging checklist.

  • bin/console debug:container | grep swp_template_system — find the real service IDs.
  • bin/console debug:twig — confirm the loader chain and that page names resolve.
  • Enable the Doctrine logger and count queries around renderContainer.
  • Blank page? Check the template isActive flag, that the container exists and has visible widgets, and that a handler is tagged with isSupported() returning true.

When to skip it. If you want CMS-editable Twig today on a modern Symfony, evaluate maintained alternatives (a custom Twig LoaderInterface backed by Doctrine plus a sandbox, or rendering stored Markdown/HTML inside normal Twig page templates). This bundle is worth reading as a reference for "DB-backed Twig + regions + widgets"; it is worth adopting only if you are already in the SWP ecosystem.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route