Class components and slots for Twig: {% component %} and {% slot %} tags, auto-discovery registry with a manifest cache. Framework-free core + optional Laravel integration.
ttbooking/twig-component is a Laravel package for class components and slots for twig: {% component %} and {% slot %} tags, auto-discovery registry with a manifest cache. framework-free core + optional laravel integration..
It currently has 0 GitHub stars and 52 downloads on Packagist (latest version 0.5.0).
Install it with composer require ttbooking/twig-component.
Discover more Laravel packages by ttbooking
or browse all Laravel packages to compare alternatives.
Last updated
Class components and slots for Twig: the {% component %} tag, the component() function,
Vue 3 style {% slot %} slots and an auto-discovery component registry with a manifest cache.
The core is framework-neutral — it runs on bare Twig in any PHP application.
The Laravel integration (DI through the container, rendering via view(), artisan commands)
is an optional layer; integrating with another framework means implementing two small interfaces.
spatie/laravel-data
(props only).{% component 'name' with {...} %}…{% endcomponent %} and inline
component('name', {...}).{% slot 'name' %} with
a <slot> fallback in the component template. The slot body executes in the caller's scope.App\View\TwigComponents\UI\Box → ui:box). The manifest is cached
for production.ComponentFactory (how to instantiate a component) and
TemplateRenderer (how to render a template) — nothing else touches the framework.Optional (Laravel integration): Laravel ^13,
rcrowe/twigbridge ^0.14,
spatie/laravel-data ^4 for Data components.
composer require ttbooking/twig-component
use TTBooking\TwigComponent\{ComponentExtension, ComponentRegistry, NativeComponentFactory, TwigTemplateRenderer};
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
$registry = new ComponentRegistry(
namespace: 'App\\View\\TwigComponents\\',
componentsPath: __DIR__.'/src/View/TwigComponents',
manifestPath: __DIR__.'/var/cache/twig-component.php',
);
$twig = new Environment(new FilesystemLoader(__DIR__.'/templates'));
$twig->addExtension(new ComponentExtension(
$registry,
new NativeComponentFactory($psr11Container), // container is optional: without it — props-only constructors
new TwigTemplateRenderer($twig), // also accepts a closure fn (): Environment
));
NativeComponentFactory instantiates widgets via new with named arguments;
if any PSR-11 container is provided, constructor parameters without a default are autowired
from it by type. Warm the manifest during deployment: $registry->cache().
The ServiceProvider is registered via package discovery. Publish the config and enable
the Twig extension in config/twigbridge.php:
php artisan vendor:publish --tag=twig-component-config
// config/twigbridge.php
'extensions' => [
'enabled' => [
// …
TTBooking\TwigComponent\ComponentExtension::class,
],
],
The provider binds the Laravel implementations itself: widgets are built by the container
(app($class, $props)), templates render through view(), Data components are available.
A component can also be rendered directly from PHP — handy in tests of application components; no Twig environment is needed for that:
$html = app(ComponentExtension::class)->renderComponent('ui:box', ['title' => 'Orders']);
Implement ComponentFactory (instantiating a component from props using your DI)
and, if needed, TemplateRenderer (when rendering must not go directly through Twig) —
then assemble ComponentExtension as in the example above.
config/twig-component.php — where to look for components and where to write the manifest:
return [
'namespace' => 'App\\View\\TwigComponents\\',
'path' => app_path('View/TwigComponents'),
'manifest' => base_path('bootstrap/cache/twig-component.php'),
];
A widget component:
namespace App\View\TwigComponents\UI;
use TTBooking\TwigComponent\TwigComponent;
class Box implements TwigComponent
{
public function __construct(
public string $title = '',
public bool $collapsible = false,
) {}
public function template(): string
{
// a name the active renderer understands:
// standalone — a Twig template path: return 'components/ui/box.html.twig';
// Laravel — a view name (view()->name() enables IDE navigation):
return view('components/ui/box')->name();
}
public function context(): array
{
return [];
}
}
The components/ui/box.html.twig template (this is the component instance, slots are the passed slots):
<div class="box">
<div class="box-header">
{% slot 'header' %}<h3>{{ this.title }}</h3>{% endslot %}
</div>
<div class="box-body">{% slot %}{% endslot %}</div>
</div>
Usage at the call site:
{% component 'ui:box' with { title: 'Orders' } %}
<p>The body content is the default slot.</p>
{% slot 'header' %}<h3>A custom heading instead of the fallback</h3>{% endslot %}
{% endcomponent %}
A presentational component (data only, Laravel integration) extends
Spatie\LaravelData\Data; props are available in the template directly. Outside Laravel this
kind is unavailable — a limitation of laravel-data itself; use widgets instead.
The component registry is cached into a flat manifest. In Laravel (wired into optimize):
php artisan twig-component:cache # build (part of php artisan optimize)
php artisan twig-component:clear # remove (part of optimize:clear)
Without a framework — $registry->cache() / $registry->clearCache() in your deploy script.
A component renders in a separate Twig pass, so the slot body is captured into a string
and injected into the component template rather than being bound with native {% embed %}.
Consequences: slots render eagerly, there are no scoped slots.
Rules (modeled on Vue 3):
{% slot 'name' %} inside {% component %} passes a named slot
(like <template #name>); all remaining body content is the default slot content.{% slot %}…{% endslot %} without a name
(like <template #default>). Combining an explicit default slot with loose content is
a template compilation error; passing the same slot twice is one too.{% slot %}) does not count
as the default slot — the {% slot %} fallback in the component template is preserved.{% slot %} deeper than the top level (e.g. inside {% if %}) is not slot passing but
a "hole with a fallback" in the current template's scope. In a component template this
lets you forward your slots into a nested component; on a regular page there is no slots
and such a tag simply renders its fallback in place.this and slots keys of the render context are reserved: a context() key
(or a Data component prop — Data props become the context) with such a name raises
an error instead of a silent override.composer install
vendor/bin/phpunit # both suites
vendor/bin/phpunit --testsuite Core # the core on bare Twig, no framework
vendor/bin/phpunit --testsuite Laravel # the integration layer on Orchestra Testbench
No database required.
Detailed guides live in the docs/ folder (Russian translation: docs/ru/):
this, context(), DI, the component() function, the naming convention.context().MIT.