blade-conventions について
blade-conventionsは、BagistoのBladeビューを作成・編集するためのコーディングスキルです。コンポーネントの使い方、BladeとVueのバインディング、パッケージの名前空間、書式規則をまとめ、新しいテンプレートを既存コードに合わせます。
主な機能
- 01Bladeコンポーネントのパターンを解説
- 02BladeとVueのバインディングを説明
- 03パッケージ名前空間と翻訳を網羅
- 04フォームと拡張フックを解説
- 05テンプレートの書式規則を指定
活用シーン
- 01AdminまたはShopのビューを作成
- 02再利用可能なBladeコンポーネントを構築
- 03Vueコンポーネントとフォームを接続
- 04カスタムモジュールの書式を統一
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bagisto/ agent-skills --skill blade-conventionsインストール後、Claude Code で/blade-conventionsを入力して読み込まれたか確認します。
詳細
Bagisto Blade の規約
概要
Bagisto の UI は Blade テンプレートで構成されており、Blade コンポーネントとインラインの Vue 3 レイヤーを多用しています。新しいビューは周囲のコードと見分けがつかないようにする必要があります。同じコンポーネントの書き方、同じ : と :: のバインディング規則、同じインデントと属性の書式に従ってください。このスキルでは、生成される Blade がコードベースと完全に一致するよう、これらの規約をまとめています。
これらの規約はすべてのパッケージで共通です — Admin、Shop、および Webkul スタイルのカスタムモジュールに適用されます。パッケージ間で変わるのは、名前空間トークン(x-<ns>:: コンポーネントのプレフィックス、<ns>:: 翻訳の名前空間、view_render_event の名前)のみです。詳しくは、以下の「パッケージごとの名前空間」を参照してください。判断に迷った場合は、同じパッケージ内の近くにあるビューを開き、それに合わせてください。
コア UI は packages/Webkul/Admin/src/Resources/views/ と packages/Webkul/Shop/src/Resources/views/ にあります。独自のパッケージを含むその他のパッケージも、それぞれの src/Resources/views/ の下で同じディレクトリ構成に従います。コンポーネントを提供する各パッケージには使用例を示す components/example.blade.php があるので、これを正式なリファレンスとして扱ってください。
パッケージごとの名前空間(Admin、Shop、カスタムモジュール)
パッケージのトークンは、ServiceProvider::boot() 内の次の 2 つの呼び出しで決まります。
// Registers the `<ns>::` view + translation namespace → view('<ns>::…'), @lang('<ns>::…')
$this->loadViewsFrom(__DIR__.'/../Resources/views', '<ns>');
// Registers the `x-<ns>::` anonymous component prefix → <x-<ns>::button />
Blade::anonymousComponentPath(__DIR__.'/../Resources/views/components', '<ns>');
したがって、Admin は admin(→ x-admin::、@lang('admin::…'))を登録し、Shop は shop を登録します。また、独自パッケージ(たとえば RMA)では rma(→ x-rma::、@lang('rma::…'))を登録します。
Admin/Shop 以外で作業する場合のポイントは次のとおりです。
- 共有コンポーネントは自由に再利用できます。
x-admin::とx-shop::のコンポーネント/レイアウトはグローバルに登録されるため、RMA の管理ページも<x-admin::layouts>でラップし、<x-admin::datagrid>、<x-admin::form.control-group.*>、<x-admin::modal>などを使用します。顧客向けのページでは<x-shop::layouts>とショップのコンポーネントを使用します。これらを再実装する必要はありません。 - 新しい独自コンポーネントにのみ、パッケージの名前空間をプレフィックスとして付けます:
<x-rma::return-request-card>。 - 翻訳には独自の名前空間を使用します:
@lang('rma::app.return-requests.index.title')。キーはpackages/Webkul/RMA/src/Resources/lang/以下に配置します。 view_render_eventの名前はパッケージ/領域に従います。 例:bagisto.rma.return-requests.list.before(そのパッケージ内の近くにあるイベントの命名に合わせます)。- それ以外はすべて同じです:
:/::のバインディング規則、Vue の<v-x>と x-template の組み合わせ、@props、書式、ACL、スクリプトのスタッキングはすべてそのまま適用されます。
パッケージ間で切り替える際の簡易対応表:
| トークン | Admin | Shop | 独自パッケージ(例:RMA) |
|---|---|---|---|
| 独自コンポーネントのプレフィックス | x-admin:: | x-shop:: | x-rma:: |
| 翻訳の名前空間 | admin::app.… | shop::app.… | rma::app.… |
| ラップするレイアウト | x-admin::layouts | x-shop::layouts | x-admin::/x-shop:: のレイアウトを再利用 |
| イベントのプレフィックス | bagisto.admin.… | bagisto.shop.… | bagisto.rma.… |
適用する場面
次の場合に適用します:
- Admin または Shop パッケージ内の
.blade.phpを新規作成または編集する場合 - 再利用可能な Blade コンポーネント(匿名または Vue 対応)を構築する場合
- フォーム、データグリッド、モーダル、ドロワー、タブ、レイアウトを接続する場合
- プロジェクトの属性バインディング、インデント、空行のスタイルに合わせる場合
ディレクトリとコンポーネントの構成
Admin と Shop は同じ構成です。ページは機能ごとのフォルダー(catalog/、sales/、checkout/、customers/ など)に置かれ、共有コンポーネントは components/ に置かれます。両方に共通するコンポーネント:accordion, button, datagrid, drawer, dropdown, form, layouts, media, modal, quantity-changer, shimmer, table, tabs, tinymce。
名前空間付きの呼び出し:<x-admin::name>/<x-shop::name>。ドットでネストします:<x-admin::form.control-group.control>、<x-admin::charts.bar>。
コンポーネントの呼び出しとデータバインディング(最も重要なルール)
3 種類の異なる属性形式があります。用途に応じて使い分けてください。
| 構文 | 解決方法 | 用途 | 例 |
|---|---|---|---|
attr="text" | 静的文字列 | リテラル | name="quantity" |
:attr="expr" | Blade/PHP 式 | PHP の値、ルート、trans()、old() | :src="route('admin.sales.orders.index')" |
::attr="expr" | : をエスケープ → Vue 用のリテラルな :attr | Vue コンポーネントに渡すデータ | ::value="item?.quantity"、::labels="chartLabels" |
::(コロン 2 つ)は、Blade が単一の : をエスケープするためのものです。その結果、レンダリングされた HTML には、Vue が実行時にバインドする :attr="expr" が含まれます。: と :: の使い分けを誤ることが、バグの最もよくある原因です。
:: が使えるのは Blade コンポーネントのタグだけです(<x-admin::…>、<x-shop::…>、<x-marketplace::…>)。コンポーネントのコンパイル時に Blade がエスケープを解除します。通常のカスタム要素(<v-quantity-changer>、<v-field>、自分で作成したすべての <v-…>)は、リテラルな HTML としてそのまま渡されます。そのため、::attr はコロン 2 つのままブラウザーに届き、Vue はこれをバインディングとして認識しないため、プロパティは気づかれないまま渡されません。通常の <v-…> 要素では、コロンを 1 つだけ使ってください。
<x-admin::quantity-changer ::value="item.qty" /> {{-- component → renders :value --}}
<v-quantity-changer :value="item.qty"></v-quantity-changer> {{-- plain element → write : --}}
この失敗は表面化しないため、誤解しやすいものです。プロパティが undefined となり、それをたどる computed がレンダリング時にエラーを起こすため、コンポーネントは最後に描画した表示のまま(通常は読み込み中のシマー)でフリーズします。<v-…> コンポーネントがプレースホルダーを表示し続ける場合は、まずコロンを確認してください。
名前付きスロットには <x-slot:name> … </x-slot> を使います。
<x-admin::drawer>
<x-slot:toggle>Toggle</x-slot>
<x-slot:content>Body</x-slot>
</x-admin::drawer>
2 種類のコンポーネント
1. 匿名 Blade コンポーネント(@props + $attributes)
@props([
'isActive' => false,
'position' => 'right',
])
<div {{ $attributes->merge(['class' => 'box-shadow rounded bg-white dark:bg-gray-900']) }}>
{{ $slot }}
</div>
@props([...])で入力を宣言します。$attributes->merge([...])で追加属性を渡します。- デフォルトスロットには
{{ $slot }}、名前付きスロットには{{ $toggle }}などを使います。
2. Vue ベースのコンポーネント(主流のパターン)
薄いカスタム要素ラッパー、インライン x-template、グローバルな app への登録を組み合わせます。
@props([
'name' => '',
'value' => 1,
])
<v-quantity-changer
{{ $attributes->merge(['class' => 'flex items-center']) }}
name="{{ $name }}"
value="{{ $value }}"
>
</v-quantity-changer>
@pushOnce('scripts')
<script
type="text/x-template"
id="v-quantity-changer-template"
>
<div>
<span
class="icon-minus cursor-pointer"
role="button"
@click="decrease"
></span>
<p>@{{ quantity }}</p>
</div>
</script>
<script type="module">
app.component("v-quantity-changer", {
template: '#v-quantity-changer-template',
props: ['name', 'value'],
data() {
return {
quantity: this.value,
};
},
methods: {
decrease() {
this.$emit('change', --this.quantity);
},
},
});
</script>
@endPushOnce
このパターンのルール:
- ラッパー要素は
<v-name>、テンプレート ID は#v-name-templateです。 app.component("v-name", { template: '#v-name-template', ... })で登録します。- スクリプトは
@pushOnce('scripts')…@endPushOnceで囲みます(レイアウトで@stack('scripts')がレンダリングされるため、コンポーネントの使用回数にかかわらず、このブロックは 1 回だけ出力されます)。 - リテラルな Vue のマスタッシュ構文は
@{{ expr }}として出力し、Blade がレンダリングしようとしないようにします。 - データを渡すには
::attr(Vue バインディング)またはattr="{{ $php }}"(サーバーの値)を使います。
ページの基本構造
<x-admin::layouts>
<x-slot:title>
@lang('admin::app.catalog.attributes.index.title')
</x-slot>
<div class="flex items-center justify-between">
<p class="text-xl font-bold text-gray-800 dark:text-white">
@lang('admin::app.catalog.attributes.index.title')
</p>
@if (bouncer()->hasPermission('catalog.attributes.create'))
<a href="{{ route('admin.catalog.attributes.create') }}">
<div class="primary-button">
@lang('admin::app.catalog.attributes.index.create-btn')
</div>
</a>
@endif
</div>
{!! view_render_event('bagisto.admin.catalog.attributes.list.before') !!}
<x-admin::datagrid :src="route('admin.catalog.attributes.index')" />
{!! view_render_event('bagisto.admin.catalog.attributes.list.after') !!}
</x-admin::layouts>
ショップのページでは <x-shop::layouts> を使用します。SEO タグには @push('meta') を追加でき、ビュー用ヘルパーには @inject('helper', '...') を使用できます。
横断的なイディオム
- 拡張用フック — 意味のあるコンテンツを
{!! view_render_event('bagisto.<area>.<path>.before') !!}と.afterで囲みます。ドット区切りの名前はビューのパスに従います。大半のページに含まれています。 - 翻訳 — UI 文字列をハードコードしないでください。
@lang('admin::app.…')/@lang('shop::app.…')またはtrans('…')を使用し、必ずパッケージ名前空間を指定します。キーを追加する場合は、Resources/lang/以下のすべてのロケールに追加してください。 - ACL(管理画面のみ) — 作成/編集/削除ボタンとデータグリッドのアクションは、
@if (bouncer()->hasPermission('resource.action'))で制御します。 - フォーム —
<x-admin::form :action="route(...)" method="POST">で VeeValidate のv-formを囲みます。フィールドは次のコントロールグループ 3 点セットで構築します。
バリデーションはクライアント側で行われます。エラーコンポーネントの<x-admin::form.control-group> <x-admin::form.control-group.label class="required"> @lang('...') </x-admin::form.control-group.label> <x-admin::form.control-group.control type="text" name="admin_name" rules="required" :value="old('admin_name')" :label="trans('...')" /> <x-admin::form.control-group.error control-name="admin_name" /> </x-admin::form.control-group>control-nameは、フィールドのnameと一致させる必要があります。 - DataGrids —
<x-admin::datagrid :src="route('…')" />を使います。列、フィルター、アクションは PHP のDataGridクラスで定義します。Blade タグは JSON エンドポイントを指定するだけです。 - Blade と Vue のエスケープ —
@{{ vueVar }}はリテラルな Vue のマスタッシュ構文を出力します。ブロック内の@や{{ }}に Blade が触れないようにするには、そのブロックをv-preで囲みます。 - テーマ設定/i18n のシェル — レイアウトでは
class="… dark …"とdir="{{ core()->getCurrentLocale()->direction }}"を設定します。テーマと RTL に対応するには、Tailwind のdark:およびltr:/rtl:バリアントを使用します。
書式とスタイルのルール
これらのルールは .blade.php では手動で適用する必要があります(Pint は Blade の書式を整えません)。正確に合わせてください。
インデント
- タブは使わず、4つのスペースを使います。ネストの深さごとに、きれいに4つずつインデントします。
属性の配置
- 属性が複数ある場合 → 1行に1つ配置し、タグから+4スペース分インデントします。閉じる
>(または/>)は、開始タグの<tagに揃えて独立した行に配置します。<x-admin::form.control-group.control type="text" name="admin_name" :value="old('admin_name')" /> - 属性が1つの場合 → インラインに配置します。例:
<x-admin::form.control-group.error control-name="admin_name" />。 - 属性の行は連続させます。1つのタグ内で属性の間に空行を入れないでください。
- 属性の順序は柔軟ですが、制御フロー/Vue ディレクティブ(
v-if、v-else)→class/:class→ 通常の属性とaria-*属性 → イベントハンドラー(@click、@change)を最後の順にします。
@props — => を揃える
@props([
'name' => '',
'value' => 1,
'minValue' => 1,
'removable' => false,
])
キーの幅を調整して、=> の矢印を揃えます。@props ブロックは、読者が表のように見渡す宣言です。そのため、ここだけは揃える価値があります。
@php … @endphp 内の PHP — Pint のルールを手動で適用
@php ブロック内では通常の PHP を記述するため、.php ファイルに対して vendor/bin/pint を実行した場合とまったく同じ書式になります。特に、=> の左右にはそれぞれ半角スペースを1つだけ置き、位置合わせのための空白は入れません。
@php
$steps = [
'label' => trans($prefix.'.source'),
'hint' => trans($prefix.'.source-hint'),
'validate' => ['file'],
];
@endphp
Pint は .blade.php ファイルの書式を整えないため、これを強制するものはありません。自分でこの書式にする必要があります。構文の書き方に迷ったら、一時的な .php ファイルに記述して Pint を実行し、その結果をコピーしてください。スペース、演算子、末尾のカンマ、短いクロージャなど、Pint のほかの出力についても同様です。
@php の外側にあるもの、つまり @props、ディレクティブの引数、インラインの {{ }} 式は、Blade 独自のレイヤーに属するため、このページの Blade ルールに従います。
空行
- 同階層のブロック/要素の間、および
view_render_eventフックの前後には空行を1行入れます。 @props([...])ブロックの後、マークアップの前には空行を1行入れます。
ディレクティブの大文字・小文字/命名
- 標準の push ブロックは
@pushOnce('scripts')…@endPushOnceです(@endPushOnceのPが大文字である点に注意してください)。CSS には'styles'を使うことがありますが、まれです。
コメント
package-development スキルには、Blade にも適用されるルールが2つあります。この2つは相反するため、両方を適用してください。
- 「すべてのメソッドにドックブロックを付ける」 —
<script>ブロック内の Vue のmethodまたはcomputedはメソッドです。何をするのかを説明する/** … */を付けます。文頭は大文字にし、ピリオドで終えます。@phpブロック内のすべての関数にも同じように適用します。 - 「コードから読み取れないことだけをコメントする」 — メソッド本体の内部とマークアップでは、原則としてコメントを付けません。局所的には分からない制約、回避策、または削除できそうに見えても重要な行について説明します。マークアップや呼び出しの内容を言い換えるだけのコメントは省きます。
以下では、各レイヤーで使う構文を説明します。
コメント構文はそれぞれ独自のレイヤーに属します。コメントの位置に応じて構文を選び、そのレイヤーの大文字・小文字と句読点のルールに従ってください。
-
Blade
{{-- --}}— Blade/PHP レベルの注記に使用します(ファイルヘッダー、@phpのロジック、制御フローの説明)。ブラウザーには出力されません。- コメントが文(または複数の文)の場合は、先頭を大文字にし、文末に句読点を付けた文章として記述します。複数行のヘッダーでは、ビューの内容とその理由を説明します:
{{-- The friendly "What kind of product?" create funnel (stepped scratch page only). It maps a plain kind + a "Does it have variations?" answer to a Bagisto product type behind the scenes. --}} - 短いラベル/タイトル(文ではないもの)の場合は、末尾にピリオドを付けず、タイトルケースを使用します:
{{-- Media --}}、{{-- Type-Specific Controls --}}。 @php … @endphpの中では使用しません。 Blade はその中のコメントを削除せず、コンパイル済み PHP にそのままコピーするため、構文エラーが発生します。@phpブロック内は通常の PHP を記述する場所なので、PHP のコメント構文とそれに対応する規則を使用します。ブロック全体の説明が必要な場合は、ブロックの上に{{-- --}}ヘッダーを記述します:{{-- Everything the wizard needs to describe its steps. --}} @php // The upload-size config is stored in MB; VeeValidate's size rule expects KB. $maxUploadKb = $maxUploadMb * 1024; @endphp
- コメントが文(または複数の文)の場合は、先頭を大文字にし、文末に句読点を付けた文章として記述します。複数行のヘッダーでは、ビューの内容とその理由を説明します:
-
HTML `` — x-template/マークアップ内のセクション区切りに使用します。短くまとめ、タイトルケースのラベル、または自然な疑問文にします。ブロックが表示する見出しに合わせます。ラベルの末尾にピリオドは付けません。疑問文には
?を付けてもかまいません: -
JS
/** … */(JSDoc ブロック) —<script>内で使用します。Vue レイヤーで使用するコメント形式はこれだけです。//による行コメントは使用しません。すべてのcomputed/methodに、先頭を大文字にし、ピリオドで終わる文を1つ記述します。メソッド内の処理には、本当に自明でない場合に限りコメントを付けます:/** * The axis checkboxes only appear for a variable kind, once the seller has said "yes" * and picked a family. */ showAxes() { return this.isVariable && this.hasVariations === true && !! this.familyId; }簡単な1行処理にもドックブロックを付けますが、文は1つだけにし、処理自体から明らかな内容に余計な説明を加えないでください。
-
CSS
/** … */(JSDoc ブロック) —<style>内で使用します。形式は JS と同じです。ルールが必要な理由を、句読点で締めくくった文で説明します(特に、:checkedによる状態やスクロールバーの非表示など、自明でないもの):/** * Hide the navigation radios; they only drive the :checked state. */ #rma-steps .rma-step-radio { … }
基本ルール:文 → 先頭を大文字にし、句読点を付ける。タイトル/ラベルのみ → タイトルケースにし、ピリオドを付けない。 <script>/<style> 内では、常に /** … */ ブロック形式を使い、// や単独の /* */ は使用しません。
レシピ:Vue 対応コンポーネントの新規作成
views/components/<name>/index.blade.phpを作成します。- サーバー側の入力には
@props([...])を使用します(=>の位置をそろえます)。 <v-<name> {{ $attributes->merge([...]) }} …>をレンダリングし、attr="{{ $php }}"でデータを渡します。@pushOnce('scripts')内に、<script type="text/x-template" id="v-<name>-template">と<script type="module"> app.component("v-<name>", { template: '#v-<name>-template', props, data, computed, methods }) </script>を記述し、@endPushOnceで閉じます。- 実行時の値は
@{{ … }}として出力します。親データは::attrでバインドします。 <x-{admin|shop}::<name> ::value="…" @change="…" />として使用します。
レシピ:ページの新規作成
<x-slot:title>を含めて、<x-admin::layouts>/<x-shop::layouts>で囲みます。- ヘッダー行(タイトルと、
bouncer()で権限を確認するアクションボタン)を追加します。 - メインコンテンツを
view_render_eventの.before/.afterで囲みます。 - すべての文字列を、パッケージの名前空間を指定した
@lang/transに通します。
行うこと/行わないこと
- 行うこと 既存の
<x-{admin|shop}::…>コンポーネントとスロットを再利用します。行わないこと コンポーネントが提供しているマークアップを独自に作成します。 - 行うこと Vue レイヤーで使用する値には
::、PHP の値には:を選びます。 - 行うこと
@propsブロック内では=>の位置を揃えます。行わないこと@php … @endphp内や.phpファイル内で位置を揃えます。そこは通常の PHP なので、Pint の単一スペースのルールが適用されます。 - 行うこと コンポーネントのスクリプトを
@pushOnce('scripts')…@endPushOnceで囲みます。 - 行うこと すべての文字列を
@lang/trans経由にし、新しいキーをすべてのロケールに追加します。 - 行わないこと タグの属性の間に空行を入れます。行うこと 兄弟ブロックの間には空行を入れます。
- 行うこと
bouncer()->hasPermission(...)で管理者のアクションを制御します。