mcpField
T
twig-component

twig-component

github.com/smnandre開発180111 回インストール19 週間前に更新
リポジトリ

twig-component について

Symfony UX 向けに、PHP クラスと Twig テンプレートで再利用可能なサーバー描画 UI を作るスキルです。ロジックを持つクラスコンポーネントと、クライアント側 JavaScript を使わない Twig のみのコンポーネントを扱います。

主な機能

  1. 01PHP クラスと Twig テンプレートのコンポーネント
  2. 02Twig のみの匿名コンポーネント
  3. 03プロパティと派生値
  4. 04デフォルトおよび名前付きブロック
  5. 05HTML 属性の受け渡し
  6. 06Symfony サービスの依存性注入

活用シーン

  1. 01再利用可能なボタン、アラート、カードの構築
  2. 02レイアウト領域やナビゲーション項目の作成
  3. 03モーダル構造やフォームウィジェットの構成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/smnandre/symfony-ux-skills --skill twig-component

インストール後、Claude Code で/twig-componentを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
api.md
gotchas.md
patterns.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">&times;</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 テンプレート内にレンダリングできます。

コメント