twig-component
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTwigComponent
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-componentbash
composer require symfony/ux-twig-componentClass Component
类组件
A PHP class annotated with paired with a Twig template.
#[AsTwigComponent]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">×</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>用注解标记的PHP类,搭配对应的Twig模板使用。
#[AsTwigComponent]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">×</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 directly in the template. Use for simple presentational components with no logic.
{% 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>无需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 to compute values from incoming props. The method runs once during component initialization.
mount()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" />使用方法根据传入的props计算值,该方法在组件初始化时仅执行一次。
mount()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 become accessible as in templates. They are computed on each access (not cached across re-renders -- for caching, see LiveComponent's ).
getthis.xxxcomputedphp
#[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>以为前缀的方法可以在模板中通过访问,每次访问时都会重新计算(不会跨渲染缓存,如需缓存请参考LiveComponent的功能)。
getthis.xxxcomputedphp
#[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 . This is how you let consumers add custom classes, ids, data attributes, etc.
{{ attributes }}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属性可以通过获取,通过该特性用户可以为组件添加自定义class、id、data属性等。
{{ attributes }}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
undefinedyaml
undefinedconfig/packages/twig_component.yaml
config/packages/twig_component.yaml
twig_component:
anonymous_template_directory: 'components/'
defaults:
App\Twig\Components: 'components/'
undefinedtwig_component:
anonymous_template_directory: 'components/'
defaults:
App\Twig\Components: 'components/'
undefinedHTML 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 () in all cases. The Twig syntax () is legacy and less readable.
<twig:...>{% component %}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 . Make sure you are on a patched version (check the Symfony security advisories). The helper now properly escapes values.
ComponentAttributes{{ attributes }}TwigComponent存在一个与通过注入未做安全处理的HTML属性相关的安全漏洞(CVE-2025-47946)。请确保你使用的是已修复的版本(可查看Symfony安全公告),当前版本的助手已经会对值进行正确转义。
ComponentAttributes{{ 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