Loading...
Loading...
Compare original and translation side by side
Need frontend interactivity?
|
+-- Pure JavaScript behavior (no server)?
| -> Stimulus
| (DOM manipulation, event handling, third-party libs)
|
+-- Navigation / partial page updates?
| -> Turbo
| +-- Full page AJAX -> Turbo Drive (automatic, zero config)
| +-- Single section update -> Turbo Frame
| +-- Multiple sections -> Turbo Stream
|
+-- Reusable UI component?
| |
| +-- Static (no live updates)?
| | -> TwigComponent
| | (props, blocks, computed properties)
| |
| +-- Dynamic (re-renders on interaction)?
| -> LiveComponent
| (data binding, actions, forms, real-time validation)
|
+-- Real-time (WebSocket/SSE)?
-> Turbo Stream + MercureNeed frontend interactivity?
|
+-- Pure JavaScript behavior (no server)?
| -> Stimulus
| (DOM manipulation, event handling, third-party libs)
|
+-- Navigation / partial page updates?
| -> Turbo
| +-- Full page AJAX -> Turbo Drive (automatic, zero config)
| +-- Single section update -> Turbo Frame
| +-- Multiple sections -> Turbo Stream
|
+-- Reusable UI component?
| |
| +-- Static (no live updates)?
| | -> TwigComponent
| | (props, blocks, computed properties)
| |
| +-- Dynamic (re-renders on interaction)?
| -> LiveComponent
| (data binding, actions, forms, real-time validation)
|
+-- Real-time (WebSocket/SSE)?
-> Turbo Stream + Mercure| Feature | Stimulus | Turbo | TwigComponent | LiveComponent |
|---|---|---|---|---|
| JavaScript required | Yes (minimal) | No | No | No |
| Server re-render | No | Yes (page/frame) | No | Yes (AJAX) |
| State management | JS only | URL/Server | Props (immutable) | LiveProp (mutable) |
| Two-way binding | Manual | No | No | data-model |
| Real-time capable | Manual | Yes (Streams+Mercure) | No | Yes (polling/emit) |
| Lazy loading | Yes (stimulusFetch) | Yes (lazy frames) | No | Yes (defer/lazy) |
| 功能 | Stimulus | Turbo | TwigComponent | LiveComponent |
|---|---|---|---|---|
| 需引入JavaScript | 是(极少) | 否 | 否 | 否 |
| 服务端重渲染 | 否 | 是(页面/Frame级别) | 否 | 是(AJAX) |
| 状态管理 | 仅JS | URL/服务端 | Props(不可变) | LiveProp(可变) |
| 双向绑定 | 手动实现 | 否 | 否 | data-model |
| 支持实时能力 | 手动实现 | 是(Streams+Mercure) | 否 | 是(轮询/事件触发) |
| 懒加载 | 支持(stimulusFetch) | 支持(lazy frames) | 否 | 支持(defer/lazy) |
undefinedundefinedundefinedundefined#[AsTwigComponent]
final class Alert
{
public string $type = 'info';
public string $message;
}{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
{{ message }}
</div><twig:Alert type="success" message="Saved!" />#[AsTwigComponent]
final class Alert
{
public string $type = 'info';
public string $message;
}{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
{{ message }}
</div><twig:Alert type="success" message="Saved!" />#[AsTwigComponent]
final class Dropdown
{
public string $label;
}{# templates/components/Dropdown.html.twig #}
<div data-controller="dropdown" {{ attributes }}>
<button data-action="click->dropdown#toggle">{{ label }}</button>
<div data-dropdown-target="menu" hidden>
{% block content %}{% endblock %}
</div>
</div>#[AsTwigComponent]
final class Dropdown
{
public string $label;
}{# templates/components/Dropdown.html.twig #}
<div data-controller="dropdown" {{ attributes }}>
<button data-action="click->dropdown#toggle">{{ label }}</button>
<div data-dropdown-target="menu" hidden>
{% block content %}{% endblock %}
</div>
</div>#[AsLiveComponent]
final class SearchBox
{
use DefaultActionTrait;
#[LiveProp(writable: true, url: true)]
public string $query = '';
public function __construct(
private readonly ProductRepository $products,
) {}
public function getResults(): array
{
return $this->products->search($this->query);
}
}<div {{ attributes }}>
<input data-model="debounce(300)|query" placeholder="Search...">
<div data-loading="addClass(opacity-50)">
{% for item in this.results %}
<div>{{ item.name }}</div>
{% endfor %}
</div>
</div>#[AsLiveComponent]
final class SearchBox
{
use DefaultActionTrait;
#[LiveProp(writable: true, url: true)]
public string $query = '';
public function __construct(
private readonly ProductRepository $products,
) {}
public function getResults(): array
{
return $this->products->search($this->query);
}
}<div {{ attributes }}>
<input data-model="debounce(300)|query" placeholder="Search...">
<div data-loading="addClass(opacity-50)">
{% for item in this.results %}
<div>{{ item.name }}</div>
{% endfor %}
</div>
</div><turbo-frame id="product-list">
{% for product in products %}
<a href="{{ path('product_show', {id: product.id}) }}">
{{ product.name }}
</a>
{% endfor %}
</turbo-frame><turbo-frame id="product-list">
{% for product in products %}
<a href="{{ path('product_show', {id: product.id}) }}">
{{ product.name }}
</a>
{% endfor %}
</turbo-frame>#[Route('/comments', methods: ['POST'])]
public function create(Request $request): Response
{
// ... save comment
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->render('comment/create.stream.html.twig', [
'comment' => $comment,
'count' => $count,
]);
}{# create.stream.html.twig #}
<turbo-stream action="append" target="comments">
<template>{{ include('comment/_comment.html.twig') }}</template>
</turbo-stream>
<turbo-stream action="update" target="comment-count">
<template>{{ count }}</template>
</turbo-stream><twig:Turbo:Stream:Append target="comments">
{{ include('comment/_comment.html.twig') }}
</twig:Turbo:Stream:Append>#[Route('/comments', methods: ['POST'])]
public function create(Request $request): Response
{
// ... save comment
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->render('comment/create.stream.html.twig', [
'comment' => $comment,
'count' => $count,
]);
}{# create.stream.html.twig #}
<turbo-stream action="append" target="comments">
<template>{{ include('comment/_comment.html.twig') }}</template>
</turbo-stream>
<turbo-stream action="update" target="comment-count">
<template>{{ count }}</template>
</turbo-stream><twig:Turbo:Stream:Append target="comments">
{{ include('comment/_comment.html.twig') }}
</twig:Turbo:Stream:Append><turbo-frame id="search-section">
<twig:ProductSearch />
</turbo-frame><turbo-frame id="search-section">
<twig:ProductSearch />
</turbo-frame>use Symfony\UX\Turbo\Attribute\Broadcast;
#[Broadcast]
class Message
{
// Entity changes broadcast automatically
}<turbo-stream-source src="{{ mercure('chat')|escape('html_attr') }}">
</turbo-stream-source>
<div id="messages">...</div>use Symfony\UX\Turbo\Attribute\Broadcast;
#[Broadcast]
class Message
{
// Entity changes broadcast automatically
}<turbo-stream-source src="{{ mercure('chat')|escape('html_attr') }}">
</turbo-stream-source>
<div id="messages">...</div>data-turbo="false"data-turbo="false"+-----------------------------------------------------+
| Page |
| +------------------------------------------------+ |
| | Turbo Drive (automatic full-page AJAX) | |
| | +------------------------------------------+ | |
| | | Turbo Frame (partial section) | | |
| | | +------------------------------------+ | | |
| | | | LiveComponent (reactive) | | | |
| | | | +------------------------------+ | | | |
| | | | | TwigComponent (static) | | | | |
| | | | | + Stimulus (JS behavior) | | | | |
| | | | +------------------------------+ | | | |
| | | +------------------------------------+ | | |
| | +------------------------------------------+ | |
| +------------------------------------------------+ |
+-----------------------------------------------------++-----------------------------------------------------+
| 页面 |
| +------------------------------------------------+ |
| | Turbo Drive (自动全页AJAX) | |
| | +------------------------------------------+ | |
| | | Turbo Frame (局部区块) | | |
| | | +------------------------------------+ | | |
| | | | LiveComponent (响应式) | | | |
| | | | +------------------------------+ | | | |
| | | | | TwigComponent (静态) | | | | |
| | | | | + Stimulus (JS行为) | | | | |
| | | | +------------------------------+ | | | |
| | | +------------------------------------+ | | |
| | +------------------------------------------+ | |
| +------------------------------------------------+ |
+-----------------------------------------------------+src/
Twig/
Components/
Alert.php # TwigComponent
Button.php # TwigComponent
SearchBox.php # LiveComponent
ProductForm.php # LiveComponent
templates/
components/
Alert.html.twig
Button.html.twig
SearchBox.html.twig
ProductForm.html.twig
assets/
controllers/
dropdown_controller.js # Stimulus
modal_controller.js # Stimulus
chart_controller.js # Stimulussrc/
Twig/
Components/
Alert.php # TwigComponent
Button.php # TwigComponent
SearchBox.php # LiveComponent
ProductForm.php # LiveComponent
templates/
components/
Alert.html.twig
Button.html.twig
SearchBox.html.twig
ProductForm.html.twig
assets/
controllers/
dropdown_controller.js # Stimulus
modal_controller.js # Stimulus
chart_controller.js # Stimulus<twig:Turbo:Stream:*><twig:Turbo:Stream:*>