twig-component について
Symfony UX 向けに、PHP クラスと Twig テンプレートで再利用可能なサーバー描画 UI を作るスキルです。ロジックを持つクラスコンポーネントと、クライアント側 JavaScript を使わない Twig のみのコンポーネントを扱います。
主な機能
- 01PHP クラスと Twig テンプレートのコンポーネント
- 02Twig のみの匿名コンポーネント
- 03プロパティと派生値
- 04デフォルトおよび名前付きブロック
- 05HTML 属性の受け渡し
- 06Symfony サービスの依存性注入
活用シーン
- 01再利用可能なボタン、アラート、カードの構築
- 02レイアウト領域やナビゲーション項目の作成
- 03モーダル構造やフォームウィジェットの構成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ smnandre/ symfony-ux-skills --skill twig-componentインストール後、Claude Code で/twig-componentを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api.mdgotchas.mdpatterns.md詳細
TwigComponent
PHP クラスと Twig テンプレートを使った再利用可能な UI コンポーネントです。React/Vue コンポーネントのようなものですが、JavaScript を一切使わずにサーバー側でレンダリングされます。
2 種類のコンポーネントがあります。ロジック、サービス、算出プロパティが必要なコンポーネント向けのクラスコンポーネント(PHP クラス+Twig テンプレート)と、シンプルな表示要素向けの匿名コンポーネント(Twig のみ、PHP クラスなし)です。
TwigComponent を使用する場面
初回のレンダリング後にサーバー側で再レンダリングする必要はないものの、props を使った再利用可能なマークアップが必要な場合は TwigComponent を使用してください。ユーザー入力への反応(AJAX による再レンダリング、データバインディング、アクション)が必要なコンポーネントには、代わりに LiveComponent を使用してください。
適した例:ボタン、アラート、カード、バッジ、アイコン、フォームウィジェット、レイアウトセクション、ナビゲーション項目、テーブル行、モーダル(構造のみ)。
インストール
composer require symfony/ux-twig-component
クラスコンポーネント
#[AsTwigComponent] アノテーションを付けた PHP クラスと Twig テンプレートを組み合わせます。
// 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;
}
{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
{{ message }}
{% if dismissible %}
<button type="button" class="close">×</button>
{% endif %}
</div>
{# 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>
匿名コンポーネント(Twig のみ)
PHP クラスは不要です。テンプレート内で {% props %} を使って props を宣言します。ロジックのないシンプルな表示コンポーネントに使用してください。
{# 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:Button variant="danger" size="lg">Delete</twig:Button>
Props
パブリックプロパティ(クラスコンポーネント)
パブリックプロパティは props になります。必須の props にはデフォルト値を設定しません。
#[AsTwigComponent]
final class Card
{
public string $title; // Required
public ?string $subtitle = null; // Optional
public bool $shadow = true; // Optional with default
}
派生状態のための mount()
受け取った props から値を算出するには mount() を使用します。このメソッドはコンポーネントの初期化時に一度だけ実行されます。
#[AsTwigComponent]
final class UserCard
{
public User $user;
public string $displayName;
public function mount(User $user): void
{
$this->user = $user;
$this->displayName = $user->getFullName();
}
}
<twig:UserCard :user="currentUser" />
動的な props(コロン接頭辞)
prop の先頭に : を付けると、文字列リテラルではなく Twig 式を渡せます。
{# Pass a variable #}
<twig:Alert :type="alertType" :message="flashMessage" />
{# Pass an expression #}
<twig:UserList :users="users|filter(u => u.active)" />
ブロック(スロット)
ブロックを使うと、親テンプレートからコンポーネント内の特定の領域にコンテンツを挿入できます。
デフォルトブロック
コンポーネントタグの間にあるコンテンツは {% block content %} に渡されます。
{# Component template #}
<div class="card">{% block content %}{% endblock %}</div>
{# Usage #}
<twig:Card><p>This is the card content</p></twig:Card>
名前付きブロック
{# 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: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>
算出プロパティ
get で始まるメソッドは、テンプレート内で this.xxx としてアクセスできます。アクセスするたびに算出されます(再レンダリングをまたいでキャッシュされることはありません。キャッシュについては LiveComponent の computed を参照してください)。
#[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;
}
}
<div class="product">
<span class="price">{{ this.formattedPrice }}</span>
{% if this.onSale %}
<span class="badge">Sale!</span>
{% endif %}
</div>
属性
コンポーネントに渡された追加の HTML 属性は、{{ attributes }} から利用できます。これにより、利用側でクラス、ID、データ属性などを追加できます。
{# 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>
属性のメソッド
{# 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') %}
サービスとしてのコンポーネント
コンポーネントは Symfony サービスです。オートワイヤリングをそのまま利用できます。依存関係にはコンストラクターを、props にはパブリックプロパティを使用してください。
#[AsTwigComponent]
final class FeaturedProducts
{
public function __construct(
private readonly ProductRepository $products,
) {}
public function getProducts(): array
{
return $this->products->findFeatured(limit: 6);
}
}
{# templates/components/FeaturedProducts.html.twig #}
<div class="featured-products">
{% for product in this.products %}
<twig:ProductCard :product="product" />
{% endfor %}
</div>
{# Usage -- no props needed, data comes from service #}
<twig:FeaturedProducts />
ライフサイクルフック
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);
}
}
ネストされたコンポーネント
コンポーネントは自然に組み合わせられます。HTML 要素のようにネストしてください。
<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>
設定
# config/packages/twig_component.yaml
twig_component:
anonymous_template_directory: 'components/'
defaults:
App\Twig\Components\: 'components/'
HTML 構文と 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 %})はレガシーで、可読性も劣ります。
参考資料
- 完全な API(属性オプション、フック、設定、すべてのメソッド):references/api.md
- パターン(フォーム、テーブル、レイアウト、組み合わせ、実例):references/patterns.md
- 注意点(属性、命名、ネストされたコンポーネント、よくある落とし穴):references/gotchas.md
関連項目
- UX Icons は TwigComponent テンプレートと自然に連携します。コンポーネントのマークアップ内で
<twig:ux:icon name="lucide:check" />を使用できます。 - UX Map は
{{ ux_map(map, {style: 'height: 400px;'}) }}を使って TwigComponent テンプレート内にレンダリングできます。