Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Symfony UX Twig Component
https://github.com/symfony/ux-twig-component
Admin
Twig Components provide a way to bind objects to templates, enabling easier rendering and reuse of
...
Tokens:
20,545
Snippets:
154
Trust Score:
9.3
Update:
1 month ago
Context
Skills
Chat
Benchmark
81.9
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Symfony UX Twig Component Symfony UX Twig Component is a Symfony bundle that enables the creation of reusable, object-bound template components for Twig. It brings the familiar component architecture from client-side frameworks into Symfony, allowing developers to build encapsulated UI elements like alerts, modals, buttons, and sidebars that can be rendered with props and attributes. Each component consists of a PHP class that holds the component's data and logic, paired with a Twig template that defines its markup. The bundle supports both class-based components with full PHP power (dependency injection, computed properties, lifecycle hooks) and anonymous components defined purely in Twig templates. Components can be rendered using either a Twig function syntax or an HTML-like `<twig:ComponentName>` syntax, making templates more readable and maintainable. The system handles property mounting, attribute management, nested components, and provides a robust testing API. ## AsTwigComponent Attribute The `#[AsTwigComponent]` attribute registers a PHP class as a Twig component. It supports optional name customization, custom template paths, and configuration for property exposure and attribute variable naming. ```php <?php // src/Twig/Components/Alert.php namespace App\Twig\Components; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] class Alert { public string $type = 'success'; public string $message; } // With custom name and template #[AsTwigComponent('custom-alert', template: 'components/alerts/main.html.twig')] class CustomAlert { public string $message; } // Disable automatic public property exposure #[AsTwigComponent(exposePublicProps: false)] class StrictAlert { public string $type = 'info'; // Must use this.type in template instead of just type } // Custom attributes variable name #[AsTwigComponent(attributesVar: '_attrs')] class CustomAttrsComponent { // Use {{ _attrs }} in template instead of {{ attributes }} } ``` ## Rendering Components in Twig Components can be rendered using the `component()` function, the `{% component %}` tag, or the HTML-like `<twig:>` syntax. Props are passed as the second argument or as attributes. ```twig {# Function syntax #} {{ component('Alert', {message: 'Success!', type: 'success'}) }} {# Tag syntax with content blocks #} {% component Alert with {type: 'danger'} %} {% block content %} <strong>Error:</strong> Something went wrong! {% endblock %} {% endcomponent %} {# HTML syntax (recommended) #} <twig:Alert message="Hello World!" type="info" /> {# HTML syntax with dynamic props (prefix with :) #} <twig:Alert :message="errorMessage" :type="alertType" /> {# HTML syntax with content #} <twig:Alert type="warning"> <p>This is the alert content with <strong>HTML</strong>!</p> </twig:Alert> {# Spread attributes #} <twig:Alert {{ ...alertProps }} /> {# Boolean props #} <twig:Alert message="Notice" dismissable /> {# dismissable = true #} <twig:Alert message="Notice" :dismissable="false" /> {# dismissable = false #} ``` ## Component Template Structure Component templates have access to public properties directly, the component instance via `this`, extra attributes via `attributes`, and computed properties via `computed`. ```twig {# templates/components/Alert.html.twig #} <div {{ attributes.defaults({class: 'alert alert-' ~ type}) }}> {# Public properties available directly #} <strong>{{ type|upper }}:</strong> {{ message }} {# Access via this for methods #} {% if this.hasIcon %} <span class="icon">{{ this.getIcon }}</span> {% endif %} {# Default content block #} {% block content %}{% endblock %} {# Named blocks with defaults #} {% block footer %} <small>Auto-dismiss in 5 seconds</small> {% endblock %} </div> {# Using computed properties (cached method results) #} <div class="products"> {% for product in computed.products %} <div>{{ product.name }}</div> {% endfor %} {# Second call uses cached result #} <p>Total: {{ computed.products|length }}</p> </div> ``` ## Anonymous Components Anonymous components are template-only components without a PHP class. Props are declared using the `{% props %}` tag. ```twig {# templates/components/Button.html.twig #} {% props label, type = 'primary', size = 'md', disabled = false %} <button {{ attributes.defaults({ class: 'btn btn-' ~ type ~ ' btn-' ~ size, type: 'button' }) }} {% if disabled %}disabled{% endif %} > {% block content %}{{ label }}{% endblock %} </button> {# Usage #} <twig:Button label="Click me" type="danger" size="lg" /> <twig:Button type="secondary" disabled> <span class="icon">+</span> Add Item </twig:Button> ``` ## Mount Method and Lifecycle Hooks The `mount()` method allows custom initialization logic. Use `#[PreMount]` to modify data before mounting and `#[PostMount]` to process data after mounting. ```php <?php namespace App\Twig\Components; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use Symfony\UX\TwigComponent\Attribute\PreMount; use Symfony\UX\TwigComponent\Attribute\PostMount; use Symfony\Component\OptionsResolver\OptionsResolver; #[AsTwigComponent] class UserCard { public string $name; public string $role = 'user'; public array $permissions = []; // mount() receives props that match parameter names public function mount(bool $isAdmin = false): void { if ($isAdmin) { $this->role = 'admin'; $this->permissions = ['read', 'write', 'delete']; } } #[PreMount(priority: 10)] // Higher priority runs first public function validateData(array $data): array { $resolver = new OptionsResolver(); $resolver->setIgnoreUndefined(true); $resolver->setRequired('name'); $resolver->setAllowedTypes('name', 'string'); $resolver->setDefaults(['role' => 'user']); $resolver->setAllowedValues('role', ['user', 'admin', 'guest']); return $resolver->resolve($data) + $data; } #[PostMount] public function processExtraData(array $data): array { // Handle custom props not mapped to properties if (isset($data['uppercase']) && $data['uppercase']) { $this->name = strtoupper($this->name); } unset($data['uppercase']); return $data; // Remaining data becomes attributes } } // Usage // <twig:UserCard name="John" isAdmin uppercase /> ``` ## ExposeInTemplate Attribute Use `#[ExposeInTemplate]` to expose private/protected properties or methods directly as template variables. ```php <?php namespace App\Twig\Components; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate; #[AsTwigComponent] class Dashboard { #[ExposeInTemplate] private string $title = 'My Dashboard'; // Available as {{ title }} #[ExposeInTemplate('user_name')] // Custom variable name private string $userName; // Available as {{ user_name }} #[ExposeInTemplate(name: 'avatar', getter: 'getAvatarUrl')] private string $avatarPath; // Available as {{ avatar }}, uses getAvatarUrl() public function getTitle(): string { return $this->title; } public function getUserName(): string { return $this->userName; } public function getAvatarUrl(): string { return '/uploads/avatars/' . $this->avatarPath; } #[ExposeInTemplate] public function getFormattedDate(): string // Available as {{ formattedDate }} { return (new \DateTime())->format('F j, Y'); } #[ExposeInTemplate('stats')] public function calculateStats(): array // Available as {{ stats }} { return ['users' => 150, 'posts' => 1200]; } } ``` ## ComponentAttributes API The `ComponentAttributes` class manages HTML attributes passed to components. It provides methods for merging defaults, filtering, and rendering attributes. ```twig {# templates/components/Card.html.twig #} {# Basic usage - renders all attributes #} <div {{ attributes }}>Content</div> {# Set defaults (class and data-controller are merged, others overwritten) #} <div {{ attributes.defaults({class: 'card', role: 'article'}) }}> Content </div> {# Only specific attributes #} <div {{ attributes.only('id', 'class') }}>Content</div> {# Exclude specific attributes #} <div {{ attributes.without('style', 'onclick') }}>Content</div> {# Render specific attribute value (marks as rendered) #} <div style="{{ attributes.render('style') }} display:block;" {{ attributes }}> Content </div> {# Check if attribute exists #} {% if attributes.has('data-loading') %} <span class="spinner"></span> {% endif %} {# Nested attributes for child elements #} {# Usage: <twig:Dialog class="modal" title:class="header" body:class="content"> #} <div {{ attributes }}> <div {{ attributes.nested('title') }}>Title</div> <div {{ attributes.nested('body') }}>{% block content %}{% endblock %}</div> </div> {# With Stimulus controllers #} <div {{ attributes.defaults(stimulus_controller('modal', {backdrop: true})) }}> Content </div> ``` ## Dependency Injection in Components Components are registered as services, allowing full dependency injection for database access, API calls, and other services. ```php <?php namespace App\Twig\Components; use App\Repository\ProductRepository; use App\Service\PricingService; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] class FeaturedProducts { public int $limit = 5; public string $category = 'all'; public function __construct( private readonly ProductRepository $productRepository, private readonly PricingService $pricingService, ) { } // Called lazily when accessed in template via this.products or computed.products public function getProducts(): array { $products = $this->productRepository->findFeatured( $this->category, $this->limit ); return array_map(fn($p) => [ 'name' => $p->getName(), 'price' => $this->pricingService->format($p->getPrice()), 'image' => $p->getImageUrl(), ], $products); } public function getTotalValue(): string { $total = array_sum(array_column($this->getProducts(), 'price')); return $this->pricingService->format($total); } } // Template: templates/components/FeaturedProducts.html.twig // {% for product in computed.products %} // <div class="product">{{ product.name }} - {{ product.price }}</div> // {% endfor %} // Usage: <twig:FeaturedProducts limit="10" category="electronics" /> ``` ## Nested Components and Block Forwarding Components can be nested, with access to parent scope via `outerScope` and block forwarding via `outerBlocks`. ```twig {# templates/components/Modal.html.twig #} <div {{ attributes.defaults({class: 'modal'}) }}> <div class="modal-header">{% block header %}{% endblock %}</div> <div class="modal-body">{% block content %}{% endblock %}</div> <div class="modal-footer">{% block footer %}{% endblock %}</div> </div> {# templates/components/ConfirmModal.html.twig - Wrapping Modal #} {% props confirmText = 'Confirm', cancelText = 'Cancel' %} <twig:Modal {{ ...attributes.defaults({class: 'confirm-modal'}) }}> <twig:block name="header"> {{ block(outerBlocks.header) }} </twig:block> <twig:block name="content"> {{ block(outerBlocks.content) }} </twig:block> <twig:block name="footer"> <button class="btn-secondary">{{ cancelText }}</button> <button class="btn-primary">{{ confirmText }}</button> </twig:block> </twig:Modal> {# Usage with outer scope access #} {# templates/page.html.twig #} {% set pageTitle = 'Delete Item' %} <twig:ConfirmModal confirmText="Yes, Delete" data-controller="modal"> <twig:block name="header">{{ pageTitle }}</twig:block> <twig:block name="content"> {# Access parent component's scope #} Are you sure? This affects {{ outerScope.this.itemCount }} items. </twig:block> </twig:ConfirmModal> ``` ## Event Subscribers for Component Lifecycle Subscribe to component events to modify templates, variables, or add cross-cutting concerns. ```php <?php namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\UX\TwigComponent\Event\PreRenderEvent; use Symfony\UX\TwigComponent\Event\PostRenderEvent; use Symfony\UX\TwigComponent\Event\PreMountEvent; use Symfony\UX\TwigComponent\Event\PostMountEvent; class TwigComponentSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ PreRenderEvent::class => 'onPreRender', PostRenderEvent::class => 'onPostRender', PreMountEvent::class => 'onPreMount', PostMountEvent::class => 'onPostMount', ]; } public function onPreRender(PreRenderEvent $event): void { $component = $event->getComponent(); $metadata = $event->getMetadata(); // Change template conditionally if ($metadata->getName() === 'Alert' && $event->getVariables()['type'] === 'critical') { $event->setTemplate('components/CriticalAlert.html.twig'); } // Add custom variables $variables = $event->getVariables(); $variables['renderTime'] = microtime(true); $event->setVariables($variables); } public function onPostRender(PostRenderEvent $event): void { $mounted = $event->getMountedComponent(); // Log component renders, track metrics, etc. } public function onPreMount(PreMountEvent $event): void { $data = $event->getData(); // Sanitize or transform input data if (isset($data['html'])) { $data['html'] = strip_tags($data['html'], '<b><i><strong><em>'); } $event->setData($data); } public function onPostMount(PostMountEvent $event): void { // Add metadata for later use $event->addExtraMetadata('mounted_at', time()); } } ``` ## Testing Components Use the `InteractsWithTwigComponents` trait to test component mounting and rendering in PHPUnit tests. ```php <?php namespace App\Tests\Component; use App\Twig\Components\Alert; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents; class AlertComponentTest extends KernelTestCase { use InteractsWithTwigComponents; public function testComponentMounting(): void { $component = $this->mountTwigComponent( name: 'Alert', // or Alert::class data: ['message' => 'Test message', 'type' => 'danger'] ); $this->assertInstanceOf(Alert::class, $component); $this->assertSame('Test message', $component->message); $this->assertSame('danger', $component->type); } public function testComponentRendering(): void { $rendered = $this->renderTwigComponent( name: 'Alert', data: ['message' => 'Hello World', 'type' => 'success'] ); // String assertion $this->assertStringContainsString('Hello World', (string) $rendered); $this->assertStringContainsString('alert-success', (string) $rendered); // DOM crawler assertions $this->assertCount(1, $rendered->crawler()->filter('div.alert')); $this->assertSame( 'Hello World', $rendered->crawler()->filter('div.alert')->text() ); } public function testEmbeddedComponentWithBlocks(): void { $rendered = $this->renderTwigComponent( name: 'Modal', data: ['title' => 'Confirm'], content: '<p>Are you sure?</p>', // Default content block blocks: [ 'header' => '<h2>Confirmation Required</h2>', 'footer' => $this->renderTwigComponent('Button', ['label' => 'OK']), ] ); $this->assertStringContainsString('Confirmation Required', (string) $rendered); $this->assertStringContainsString('Are you sure?', (string) $rendered); } } ``` ## Bundle Configuration Configure component namespaces, template directories, and profiler settings in your Symfony configuration. ```yaml # config/packages/twig_component.yaml twig_component: # Directory for anonymous components anonymous_template_directory: 'components/' # Profiler data collection (enabled in debug by default) profiler: collect_components: true # Component namespace configuration defaults: # Short form: namespace prefix maps to template directory App\Twig\Components\: 'components/' # Long form with name prefix App\Admin\Components\: template_directory: 'admin/components/' name_prefix: 'Admin' # <twig:Admin:Dashboard /> # Third-party bundle components Acme\Bundle\Components\: template_directory: '@AcmeBundle/components/' name_prefix: 'Acme' ``` ```php // Debug command to list all components // php bin/console debug:twig-component // Output: // +------------------+---------------------------+-------------------------------------+------+ // | Component | Class | Template | Type | // +------------------+---------------------------+-------------------------------------+------+ // | Alert | App\Twig\Components\Alert | components/Alert.html.twig | | // | Button | | components/Button.html.twig | Anon | // | Admin:Dashboard | App\Admin\...\Dashboard | admin/components/Dashboard.html.twig| | // +------------------+---------------------------+-------------------------------------+------+ // Get details for a specific component // php bin/console debug:twig-component Alert ``` ## Summary Symfony UX Twig Component is ideal for building reusable UI elements in Symfony applications. Common use cases include: creating consistent design system components (buttons, cards, alerts, modals), building form field wrappers with validation styling, developing data display components that fetch their own data via dependency injection, and creating complex nested component hierarchies for layouts and page sections. The HTML-like `<twig:>` syntax makes templates more readable and component-oriented. Integration follows standard Symfony patterns: components are services with full DI support, lifecycle hooks allow data transformation and validation, events enable cross-cutting concerns like logging or caching, and the testing trait provides first-class PHPUnit support. The system works seamlessly with Symfony UX Live Components for reactive, Ajax-powered interfaces, Stimulus for JavaScript behavior, and third-party bundles through Twig namespaces. Whether building a simple alert box or a complex dashboard with nested interactive components, Twig Components provides the structure and flexibility needed for maintainable, testable UI code.