lliure / twig-container
Twig extension for container-based template composition with additive content injection
Requires
- php: >=8.1
- twig/twig: ^3.0
README
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
| Feature | Description |
|---|---|
| Containers | Named areas that accumulate content |
| Inject | Add content from anywhere (before or after the container) |
| Unique | Prevent duplicate injections (CSS/JS) |
| Package | Reusable components with isolated context |
| content() | Mix original content with injections |
Comparison with Alternatives
| Feature | twig-stack | lliure/twig-container |
|---|---|---|
| Push/Inject | ✓ | ✓ |
| Prevent duplicates | pushonce | unique 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.