symfony/ux-twig-component
Symfony UX Twig Components lets you bind PHP objects to Twig templates to build reusable UI pieces like alerts, modals, and sidebars. Create small, composable components with clean rendering and better template organization for Symfony apps.
Installation:
composer require symfony/ux-twig-component
Ensure your config/bundles.php includes Symfony\UX\TwigComponent\TwigComponentBundle::class.
First Component:
Create a PHP class with the [AsTwigComponent] attribute:
// src/Component/AlertComponent.php
namespace App\Component;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent('alert')]
class AlertComponent
{
public string $message = "Default message";
public string $type = "info";
}
Template: Create a corresponding Twig template:
{# templates/components/alert.html.twig #}
<div class="alert alert-{{ type }}">
{{ message }}
</div>
Usage in Twig:
{% component('alert', {
message: 'Hello, world!',
type: 'success'
}) %}
Or with HTML syntax:
<twig:alert message="Hello, world!" type="success" />
Autowiring: Symfony automatically registers the component. No manual service configuration is needed for Symfony 5.3+.
Replace hardcoded alerts in your templates with a reusable AlertComponent. Pass dynamic messages and types (e.g., success, error) as props to maintain consistency across your application.
[AsTwigComponent('name')] for globally reusable components (e.g., alert, card).src/Component/ and use its directory name as the component name (e.g., src/Component/MyComponent.php → <twig:MyComponent />).#[AsTwigComponent('alert', template: 'components/custom_alert.html.twig')]
public string $title;
public bool $isOpen = false;
Access in Twig: {{ title }}, {% if isOpen %}.#[ExposeInTemplate] attribute for getter methods:
#[ExposeInTemplate]
public function getFormattedDate(): string {
return $this->date->format('Y-m-d');
}
Access in Twig: {{ getFormattedDate() }}.use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent('modal')]
class ModalComponent {
public bool $isOpen = false;
#[PostMount]
public function postMount(): void {
$this->isOpen = true;
}
}
use Symfony\UX\TwigComponent\Attribute\PreRender;
#[PreRender]
public function preRender(): void {
$this->content = $this->sanitize($this->content);
}
<twig:alert
message="Hello"
class="custom-class"
data-custom="value"
/>
Access in Twig: {{ attributes.add('aria-label', 'Notification') }}.{{ attributes.defaults(stimulus_controller('alert-controller')) }}
{% component %} inside another component’s template.provide/inject:
{# Parent template #}
{% component('parent', {
provide: { theme: 'dark' }
}) %}
{# Child template #}
{{ inject('theme') }} {# Outputs: 'dark' #}
@symfony/stimulus-bridge for interactivity:
<twig:modal {{ attributes.defaults(stimulus_controller('modal-controller')) }}>
Content here
</twig:modal>
PostMount to subscribe to updates:
#[PostMount]
public function postMount(): void {
$this->hub->subscribe('/updates', $this->handleUpdate(...));
}
#[AsTwigComponent('user_form')]
class UserFormComponent {
public FormInterface $form;
}
{% component('user_form', { form: form }) %}
ComponentTestCase:
use Symfony\UX\TwigComponent\Test\ComponentTestCase;
class AlertComponentTest extends ComponentTestCase {
public function testRendering(): void {
$component = new AlertComponent();
$component->message = 'Test';
$this->renderComponent($component)
->assertSelectorTextContains('div.alert', 'Test');
}
}
debug:twig-component command:
php bin/console debug:twig-component
cache_dir is configured in framework.yaml.config/packages/dev/twig.yaml:
twig:
twig_component:
profiler:
collect_components: true
PHP Version Requirements:
Attribute Escaping:
ComponentAttributes now handles escaping by default. Passing null as an attribute value throws an exception. Use remove() to unset attributes:
{{ attributes.remove('data-old') }}
Template Resolution:
template path in [AsTwigComponent].templates/components/{name}.html.twig.templates/{name}.html.twig (for anonymous components).ComponentTemplateFinder for custom logic:
$finder = new ComponentTemplateFinder($loader, 'custom_namespace');
Prop Overrides:
component() override class defaults, even for readonly properties. Use PostMount to enforce immutability:
#[PostMount]
public function postMount(): void {
if ($this->isReadonly && isset($this->newProp)) {
throw new \RuntimeException('Cannot modify readonly prop');
}
}
Stimulus Controllers:
cva function is removed. Use html_cva from twig/html-extra instead:
{{ html_cva([
'class': 'btn btn-' ~ type,
'data-controller': 'alert',
]) }}
Debug Mode:
twig_component:
profiler:
collect_components: false
Command Line:
php bin/console debug:twig-component
php bin/console debug:twig-component search alert
Twig Errors:
try-catch to log errors gracefully:
{% try %}
{% component('alert', { message: riskyData }) %}
{% catch Exception $e %}
<div class="error">{{ e.message }}</div>
{% endtry %}
Profiler:
PreMount, PostMount, PreRender, PostRender.How can I help you explore Laravel packages today?