# 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 `` 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 ` 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 %} Error: Something went wrong! {% endblock %} {% endcomponent %} {# HTML syntax (recommended) #} {# HTML syntax with dynamic props (prefix with :) #} {# HTML syntax with content #}

This is the alert content with HTML!

{# Spread attributes #} {# Boolean props #} {# dismissable = true #} {# 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 #}
{# Public properties available directly #} {{ type|upper }}: {{ message }} {# Access via this for methods #} {% if this.hasIcon %} {{ this.getIcon }} {% endif %} {# Default content block #} {% block content %}{% endblock %} {# Named blocks with defaults #} {% block footer %} Auto-dismiss in 5 seconds {% endblock %}
{# Using computed properties (cached method results) #}
{% for product in computed.products %}
{{ product.name }}
{% endfor %} {# Second call uses cached result #}

Total: {{ computed.products|length }}

``` ## 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 %} {# Usage #} + Add Item ``` ## 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 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 // ``` ## ExposeInTemplate Attribute Use `#[ExposeInTemplate]` to expose private/protected properties or methods directly as template variables. ```php 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 #}
Content
{# Set defaults (class and data-controller are merged, others overwritten) #}
Content
{# Only specific attributes #}
Content
{# Exclude specific attributes #}
Content
{# Render specific attribute value (marks as rendered) #}
Content
{# Check if attribute exists #} {% if attributes.has('data-loading') %} {% endif %} {# Nested attributes for child elements #} {# Usage: #}
Title
{% block content %}{% endblock %}
{# With Stimulus controllers #}
Content
``` ## Dependency Injection in Components Components are registered as services, allowing full dependency injection for database access, API calls, and other services. ```php 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 %} //
{{ product.name }} - {{ product.price }}
// {% endfor %} // Usage: ``` ## 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 #}
{# templates/components/ConfirmModal.html.twig - Wrapping Modal #} {% props confirmText = 'Confirm', cancelText = 'Cancel' %} {{ block(outerBlocks.header) }} {{ block(outerBlocks.content) }} {# Usage with outer scope access #} {# templates/page.html.twig #} {% set pageTitle = 'Delete Item' %} {{ pageTitle }} {# Access parent component's scope #} Are you sure? This affects {{ outerScope.this.itemCount }} items. ``` ## Event Subscribers for Component Lifecycle Subscribe to component events to modify templates, variables, or add cross-cutting concerns. ```php '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'], ''); } $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 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: '

Are you sure?

', // Default content block blocks: [ 'header' => '

Confirmation Required

', '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' # # 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 `` 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.