twig-component

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TwigComponent

TwigComponent

Reusable UI components with PHP classes + Twig templates. Think React/Vue components, but server-rendered with zero JavaScript.
Two flavors exist: class components (PHP class + Twig template) for components that need logic, services, or computed properties, and anonymous components (Twig-only, no PHP class) for simple presentational elements.
通过PHP类+Twig模板实现的可复用UI组件。类似React/Vue组件,但它是服务端渲染的,无需任何JavaScript。
它分为两类:类组件(PHP类+Twig模板),适用于需要逻辑、服务或计算属性的组件;匿名组件(仅Twig模板,无PHP类),适用于简单的展示类元素。

When to Use TwigComponent

何时使用TwigComponent

Use TwigComponent when you need reusable markup with props but no server re-rendering after the initial render. If the component needs to react to user input (re-render via AJAX, data binding, actions), use LiveComponent instead.
Good candidates: buttons, alerts, cards, badges, icons, form widgets, layout sections, navigation items, table rows, modals (structure only).
当你需要带props的可复用标记,且初始渲染后不需要服务端重新渲染时,使用TwigComponent。如果组件需要响应用户输入(通过AJAX重新渲染、数据绑定、动作处理),请改用LiveComponent。
适用场景:按钮、警告提示、卡片、徽章、图标、表单组件、布局区块、导航项、表格行、模态框(仅结构)。

Installation

安装

bash
composer require symfony/ux-twig-component
bash
composer require symfony/ux-twig-component

Class Component

类组件

A PHP class annotated with
#[AsTwigComponent]
paired with a Twig template.
php
// src/Twig/Components/Alert.php
namespace App\Twig\Components;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
final class Alert
{
    public string $type = 'info';
    public string $message;
    public bool $dismissible = false;
}
twig
{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
    {{ message }}
    {% if dismissible %}
        <button type="button" class="close">&times;</button>
    {% endif %}
</div>
twig
{# Usage #}
<twig:Alert type="success" message="Saved!" />
<twig:Alert type="danger" message="Error occurred" dismissible />

{# With block content instead of message prop #}
<twig:Alert type="warning">
    <strong>Warning:</strong> Check your input
</twig:Alert>
#[AsTwigComponent]
注解标记的PHP类,搭配对应的Twig模板使用。
php
// src/Twig/Components/Alert.php
namespace App\Twig\Components;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
final class Alert
{
    public string $type = 'info';
    public string $message;
    public bool $dismissible = false;
}
twig
{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
    {{ message }}
    {% if dismissible %}
        <button type="button" class="close">&times;</button>
    {% endif %}
</div>
twig
{# Usage #}
<twig:Alert type="success" message="Saved!" />
<twig:Alert type="danger" message="Error occurred" dismissible />

{# With block content instead of message prop #}
<twig:Alert type="warning">
    <strong>Warning:</strong> Check your input
</twig:Alert>

Anonymous Component (Twig Only)

匿名组件(仅Twig)

No PHP class needed. Props are declared with
{% props %}
directly in the template. Use for simple presentational components with no logic.
twig
{# templates/components/Button.html.twig #}
{% props variant = 'primary', size = 'md', disabled = false %}

<button
    class="btn btn-{{ variant }} btn-{{ size }}"
    {{ disabled ? 'disabled' }}
    {{ attributes }}
>
    {% block content %}{% endblock %}
</button>
twig
<twig:Button variant="danger" size="lg">Delete</twig:Button>
无需PHP类。直接在模板中通过
{% props %}
声明props,适用于无逻辑的简单展示类组件。
twig
{# templates/components/Button.html.twig #}
{% props variant = 'primary', size = 'md', disabled = false %}

<button
    class="btn btn-{{ variant }} btn-{{ size }}"
    {{ disabled ? 'disabled' }}
    {{ attributes }}
>
    {% block content %}{% endblock %}
</button>
twig
<twig:Button variant="danger" size="lg">Delete</twig:Button>

Props

Props

Public Properties (Class Components)

公共属性(类组件)

Public properties become props. Required props have no default value.
php
#[AsTwigComponent]
final class Card
{
    public string $title;           // Required
    public ?string $subtitle = null; // Optional
    public bool $shadow = true;      // Optional with default
}
公共属性会自动成为props,无默认值的属性为必填props。
php
#[AsTwigComponent]
final class Card
{
    public string $title;           // Required
    public ?string $subtitle = null; // Optional
    public bool $shadow = true;      // Optional with default
}

mount() for Derived State

mount()方法用于派生状态

Use
mount()
to compute values from incoming props. The method runs once during component initialization.
php
#[AsTwigComponent]
final class UserCard
{
    public User $user;
    public string $displayName;

    public function mount(User $user): void
    {
        $this->user = $user;
        $this->displayName = $user->getFullName();
    }
}
twig
<twig:UserCard :user="currentUser" />
使用
mount()
方法根据传入的props计算值,该方法在组件初始化时仅执行一次。
php
#[AsTwigComponent]
final class UserCard
{
    public User $user;
    public string $displayName;

    public function mount(User $user): void
    {
        $this->user = $user;
        $this->displayName = $user->getFullName();
    }
}
twig
<twig:UserCard :user="currentUser" />

Dynamic Props (Colon Prefix)

动态Props(冒号前缀)

Prefix a prop with
:
to pass a Twig expression instead of a string literal.
twig
{# Pass a variable #}
<twig:Alert :type="alertType" :message="flashMessage" />

{# Pass an expression #}
<twig:UserList :users="users|filter(u => u.active)" />
给prop添加前缀
:
即可传入Twig表达式,而非字符串字面量。
twig
{# Pass a variable #}
<twig:Alert :type="alertType" :message="flashMessage" />

{# Pass an expression #}
<twig:UserList :users="users|filter(u => u.active)" />

Blocks (Slots)

块(插槽)

Blocks let parent templates inject content into specific areas of a component.
块允许父模板向组件的指定区域注入内容。

Default Block

默认块

Content between component tags goes to
{% block content %}
:
twig
{# Component template #}
<div class="card">{% block content %}{% endblock %}</div>

{# Usage #}
<twig:Card><p>This is the card content</p></twig:Card>
组件标签之间的内容会自动传入
{% block content %}
twig
{# Component template #}
<div class="card">{% block content %}{% endblock %}</div>

{# Usage #}
<twig:Card><p>This is the card content</p></twig:Card>

Named Blocks

命名块

twig
{# templates/components/Modal.html.twig #}
<dialog class="modal" {{ attributes }}>
    <header>{% block header %}Default Header{% endblock %}</header>
    <main>{% block content %}{% endblock %}</main>
    <footer>{% block footer %}{% endblock %}</footer>
</dialog>
twig
<twig:Modal>
    <twig:block name="header"><h2>Confirm Action</h2></twig:block>
    <twig:block name="content"><p>Are you sure?</p></twig:block>
    <twig:block name="footer">
        <button>Cancel</button>
        <button>Confirm</button>
    </twig:block>
</twig:Modal>
twig
{# templates/components/Modal.html.twig #}
<dialog class="modal" {{ attributes }}>
    <header>{% block header %}Default Header{% endblock %}</header>
    <main>{% block content %}{% endblock %}</main>
    <footer>{% block footer %}{% endblock %}</footer>
</dialog>
twig
<twig:Modal>
    <twig:block name="header"><h2>Confirm Action</h2></twig:block>
    <twig:block name="content"><p>Are you sure?</p></twig:block>
    <twig:block name="footer">
        <button>Cancel</button>
        <button>Confirm</button>
    </twig:block>
</twig:Modal>

Computed Properties

计算属性

Methods prefixed with
get
become accessible as
this.xxx
in templates. They are computed on each access (not cached across re-renders -- for caching, see LiveComponent's
computed
).
php
#[AsTwigComponent]
final class ProductCard
{
    public Product $product;

    public function getFormattedPrice(): string
    {
        return number_format($this->product->getPrice(), 2) . ' EUR';
    }

    public function isOnSale(): bool
    {
        return $this->product->getDiscount() > 0;
    }
}
twig
<div class="product">
    <span class="price">{{ this.formattedPrice }}</span>
    {% if this.onSale %}
        <span class="badge">Sale!</span>
    {% endif %}
</div>
get
为前缀的方法可以在模板中通过
this.xxx
访问,每次访问时都会重新计算(不会跨渲染缓存,如需缓存请参考LiveComponent的
computed
功能)。
php
#[AsTwigComponent]
final class ProductCard
{
    public Product $product;

    public function getFormattedPrice(): string
    {
        return number_format($this->product->getPrice(), 2) . ' EUR';
    }

    public function isOnSale(): bool
    {
        return $this->product->getDiscount() > 0;
    }
}
twig
<div class="product">
    <span class="price">{{ this.formattedPrice }}</span>
    {% if this.onSale %}
        <span class="badge">Sale!</span>
    {% endif %}
</div>

Attributes

Attributes

Extra HTML attributes passed to the component are available via
{{ attributes }}
. This is how you let consumers add custom classes, ids, data attributes, etc.
twig
{# Usage #}
<twig:Alert type="info" message="Hello" class="my-class" id="main-alert" data-controller="alert" />

{# In component template -- renders class, id, data-controller #}
<div {{ attributes }}>...</div>
传递给组件的额外HTML属性可以通过
{{ attributes }}
获取,通过该特性用户可以为组件添加自定义class、id、data属性等。
twig
{# Usage #}
<twig:Alert type="info" message="Hello" class="my-class" id="main-alert" data-controller="alert" />

{# In component template -- renders class, id, data-controller #}
<div {{ attributes }}>...</div>

Attributes Methods

Attributes方法

twig
{# Merge with defaults #}
<div {{ attributes.defaults({class: 'alert'}) }}>

{# Exclude specific #}
<div {{ attributes.without('id', 'class') }}>

{# Only render specific #}
<div id="{{ attributes.render('id') }}">

{# Check existence #}
{% if attributes.has('disabled') %}
twig
{# Merge with defaults #}
<div {{ attributes.defaults({class: 'alert'}) }}>

{# Exclude specific #}
<div {{ attributes.without('id', 'class') }}>

{# Only render specific #}
<div id="{{ attributes.render('id') }}">

{# Check existence #}
{% if attributes.has('disabled') %}

Components as Services

组件作为服务

Components are Symfony services -- autowiring works naturally. Use the constructor for dependencies, public properties for props.
php
#[AsTwigComponent]
final class FeaturedProducts
{
    public function __construct(
        private readonly ProductRepository $products,
    ) {}

    public function getProducts(): array
    {
        return $this->products->findFeatured(limit: 6);
    }
}
twig
{# templates/components/FeaturedProducts.html.twig #}
<div class="featured-products">
    {% for product in this.products %}
        <twig:ProductCard :product="product" />
    {% endfor %}
</div>
twig
{# Usage -- no props needed, data comes from service #}
<twig:FeaturedProducts />
组件本身就是Symfony服务,天然支持自动注入。可以通过构造函数注入依赖,公共属性作为props使用。
php
#[AsTwigComponent]
final class FeaturedProducts
{
    public function __construct(
        private readonly ProductRepository $products,
    ) {}

    public function getProducts(): array
    {
        return $this->products->findFeatured(limit: 6);
    }
}
twig
{# templates/components/FeaturedProducts.html.twig #}
<div class="featured-products">
    {% for product in this.products %}
        <twig:ProductCard :product="product" />
    {% endfor %}
</div>
twig
{# Usage -- no props needed, data comes from service #}
<twig:FeaturedProducts />

Lifecycle Hooks

生命周期钩子

php
use Symfony\UX\TwigComponent\Attribute\PreMount;
use Symfony\UX\TwigComponent\Attribute\PostMount;

#[AsTwigComponent]
final class DataTable
{
    public array $data;
    public string $sortBy = 'id';

    #[PreMount]
    public function preMount(array $data): array
    {
        // Modify/validate incoming data before property assignment
        $data['sortBy'] ??= 'id';
        return $data;
    }

    #[PostMount]
    public function postMount(): void
    {
        // Runs after all props are set
        $this->data = $this->sortData($this->data);
    }
}
php
use Symfony\UX\TwigComponent\Attribute\PreMount;
use Symfony\UX\TwigComponent\Attribute\PostMount;

#[AsTwigComponent]
final class DataTable
{
    public array $data;
    public string $sortBy = 'id';

    #[PreMount]
    public function preMount(array $data): array
    {
        // Modify/validate incoming data before property assignment
        $data['sortBy'] ??= 'id';
        return $data;
    }

    #[PostMount]
    public function postMount(): void
    {
        // Runs after all props are set
        $this->data = $this->sortData($this->data);
    }
}

Nested Components

嵌套组件

Components compose naturally -- nest them like HTML elements:
twig
<twig:Card>
    <twig:block name="header">
        <twig:Icon name="star" /> Featured
    </twig:block>
    <twig:block name="content">
        <twig:ProductList :products="featuredProducts">
            <twig:block name="empty">
                <twig:Alert type="info" message="No products found" />
            </twig:block>
        </twig:ProductList>
    </twig:block>
</twig:Card>
组件天然支持组合,可以像HTML元素一样嵌套使用:
twig
<twig:Card>
    <twig:block name="header">
        <twig:Icon name="star" /> Featured
    </twig:block>
    <twig:block name="content">
        <twig:ProductList :products="featuredProducts">
            <twig:block name="empty">
                <twig:Alert type="info" message="No products found" />
            </twig:block>
        </twig:ProductList>
    </twig:block>
</twig:Card>

Configuration

配置

yaml
undefined
yaml
undefined

config/packages/twig_component.yaml

config/packages/twig_component.yaml

twig_component: anonymous_template_directory: 'components/' defaults: App\Twig\Components: 'components/'
undefined
twig_component: anonymous_template_directory: 'components/' defaults: App\Twig\Components: 'components/'
undefined

HTML vs Twig Syntax

HTML语法 vs Twig语法

twig
{# HTML syntax (recommended -- better IDE support, more readable) #}
<twig:Alert type="success" message="Done!" />

{# Twig syntax (alternative -- useful in edge cases) #}
{% component 'Alert' with {type: 'success', message: 'Done!'} %}
{% endcomponent %}
Prefer HTML syntax (
<twig:...>
) in all cases. The Twig syntax (
{% component %}
) is legacy and less readable.
twig
{# HTML syntax (recommended -- better IDE support, more readable) #}
<twig:Alert type="success" message="Done!" />

{# Twig syntax (alternative -- useful in edge cases) #}
{% component 'Alert' with {type: 'success', message: 'Done!'} %}
{% endcomponent %}
所有场景下优先使用HTML语法(
<twig:...>
),Twig语法(
{% component %}
)是遗留写法,可读性更差。

CVE-2025-47946 -- Attribute Injection

CVE-2025-47946 -- 属性注入漏洞

TwigComponent had a security vulnerability (CVE-2025-47946) related to unsanitized HTML attribute injection via
ComponentAttributes
. Make sure you are on a patched version (check the Symfony security advisories). The
{{ attributes }}
helper now properly escapes values.
TwigComponent存在一个与通过
ComponentAttributes
注入未做安全处理的HTML属性相关的安全漏洞(CVE-2025-47946)。请确保你使用的是已修复的版本(可查看Symfony安全公告),当前版本的
{{ attributes }}
助手已经会对值进行正确转义。

References

参考资料

  • Full API (attribute options, hooks, configuration, all methods): references/api.md
  • Patterns (forms, tables, layouts, composition, real-world examples): references/patterns.md
  • 完整API(属性选项、钩子、配置、所有方法):references/api.md
  • 最佳实践(表单、表格、布局、组件组合、实际案例):references/patterns.md