lliure/twig-container

Twig extension for container-based template composition with additive content injection

Maintainers

Package info

bitbucket.org/vinteenove/twig-container

pkg:composer/lliure/twig-container

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

v1.0.1 2026-03-23 21:04 UTC

This package is auto-updated.

Last update: 2026-03-23 21:04:37 UTC


README

Latest Version PHP Version License

Vue-like slots for Twig. Container-based template composition with additive content injection.

Perfect for injecting scripts, styles, and components from child templates into parent layouts.

Why This Library?

Twig's native {% block %} is substitutive - child blocks replace parent blocks. But sometimes you need additive composition:

{# ❌ Problem: Each component overwrites the scripts block #}
{% block scripts %}
    <script src="datepicker.js"></script>
{% endblock %}

{# ✅ Solution: Components ADD to the container #}
{% inject 'scripts' %}
    <script src="datepicker.js"></script>
{% endinject %}

Features

FeatureDescription
ContainersNamed areas that accumulate content
InjectAdd content from anywhere (before or after the container)
UniquePrevent duplicate injections (CSS/JS)
PackageReusable components with isolated context
content()Mix original content with injections

Comparison with Alternatives

Featuretwig-stacklliure/twig-container
Push/Inject
Prevent duplicatespushonceunique with auto or manual ID
Mix original + injected{{ content() }}
Reusable components{% package %}
Dynamic paths{% package path ~ '.twig' %}

Installation

composer require lliure/twig-container

Quick Start

use TwigContainer\Container;
use TwigContainer\ContainerRuntime;

// Add extension
$twig->addExtension(new Container());

// Add runtime loader
$twig->addRuntimeLoader(new class implements \Twig\RuntimeLoader\RuntimeLoaderInterface {
    private ?ContainerRuntime $runtime = null;
    public function load(string $class): ?object {
        if ($class === ContainerRuntime::class) {
            return $this->runtime ??= new ContainerRuntime();
        }
        return null;
    }
});

// Render with post-processing
$output = $twig->render('template.twig');
$runtime = $twig->getRuntime(ContainerRuntime::class);
echo $runtime->processPlaceholders($output);

Usage

Container

Define a named area:

{% container 'scripts' %}
    <script src="base.js"></script>
    {{ content() }}
{% endcontainer %}
  • {{ content() }} marks where injections appear
  • Without content(): injections replace original content
  • Without injections: shows original content

Inject

Add content to a container:

{% inject 'scripts' %}
    <script src="page.js"></script>
{% endinject %}

Works before or after the container (placeholder magic).

Unique Inject

Prevent duplicates (great for CSS/JS libraries):

{# Auto-unique based on file:line #}
{% inject 'styles' unique %}
    <link rel="stylesheet" href="datepicker.css">
{% endinject %}

{# Manual ID for cross-file deduplication #}
{% inject 'styles' unique 'datepicker-css' %}
    <link rel="stylesheet" href="datepicker.css">
{% endinject %}

Package

Include reusable components with isolated context:

{% package 'components/datepicker.twig' with {name: 'birthday', format: 'd/m/Y'} %}

Supports dynamic paths:

{% for field in fields %}
    {% package 'components/' ~ field.type ~ '.twig' with field.config %}
{% endfor %}

Real-World Example

The Problem

You have a datepicker component used multiple times. You need:

  • CSS loaded once
  • JS initialization for each instance
  • Everything in the right place (head/footer)

The Solution

components/datepicker.twig:

<input type="text" name="{{ name }}" class="datepicker" data-format="{{ format }}">

{# CSS - loads once per page #}
{% inject 'styles' unique 'datepicker' %}
    <link rel="stylesheet" href="datepicker.css">
{% endinject %}

{# JS - one per instance #}
{% inject 'scripts' %}
    <script>initDatepicker('{{ name }}', '{{ format }}');</script>
{% endinject %}

layout.twig:

<!DOCTYPE html>
<html>
<head>
    {% container 'styles' %}{{ content() }}{% endcontainer %}
</head>
<body>
    {% block content %}{% endblock %}

    {% container 'scripts' %}
        <script src="app.js"></script>
        {{ content() }}
    {% endcontainer %}
</body>
</html>

form.twig:

{% extends 'layout.twig' %}

{% block content %}
    <form>
        {% package 'components/datepicker.twig' with {name: 'start', format: 'd/m/Y'} %}
        {% package 'components/datepicker.twig' with {name: 'end', format: 'd/m/Y'} %}
    </form>
{% endblock %}

Output:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="datepicker.css">
</head>
<body>
    <form>
        <input type="text" name="start" class="datepicker" data-format="d/m/Y">
        <input type="text" name="end" class="datepicker" data-format="d/m/Y">
    </form>

    <script src="app.js"></script>
    <script>initDatepicker('start', 'd/m/Y');</script>
    <script>initDatepicker('end', 'd/m/Y');</script>
</body>
</html>

CSS loads once, scripts load per instance.

Running Tests

php tests/run.php              # Visual output
php tests/run.php --validate   # Exit code for CI

License

MIT - see LICENSE

Contributing

Issues and PRs welcome at Bitbucket.