【PHP】LaravelでBladeコンポーネントとレイアウトを使う方法

対象バージョン: Laravel 13.x / PHP 8.3+

Blade コンポーネントを使うと、ボタンやカード、ページ全体のレイアウトを再利用可能な部品としてまとめられます。この記事では、匿名コンポーネントとクラスベースコンポーネント、スロット、属性のマージ、レイアウトコンポーネントまでを順に説明します。テンプレート継承(@extends / @section / @yield)は、対応関係だけ短く触れます。

この記事で分かること

  • resources/views/components に置く匿名コンポーネントの作り方と、ドット記法によるネスト
  • @props、スロット、$attributes->merge() / $attributes->class() の使い方
  • レイアウト用コンポーネントでページを包む方法
  • php artisan make:component でクラスベースコンポーネントを作る方法
  • テンプレート継承との対応関係(@parent の注意を含む)
  • Feature テストで描画結果を確認する方法

前提

Laravel 13.x は PHP 8.3 以上が必要です。動作確認は laravel/framework v13.36.0 で行いました。インストールやルーティング、Blade の基本({{ }} による表示)は【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成を参照してください。

メール用の <x-mail::message> などは Mailable のテンプレート向けです。メール送信そのものは【PHP】LaravelでMailableを使ってメールを送信する方法を、通知まわりは【PHP】Laravelで通知(Notifications)を実装する方法を参照してください。

匿名コンポーネントを作る

匿名コンポーネントは、クラスを持たない単一の Blade ファイルです。resources/views/components/ に置けば、そのファイル名でタグとして呼び出せます。

resources/views/components/button.blade.php を次のように書きます。

@props(['label', 'variant' => 'primary'])

<button {{ $attributes->merge(['class' => 'btn btn-'.$variant, 'type' => 'button']) }}>{{ $label }}</button>

呼び出し側は次のとおりです。

<x-button label="Save" />
<x-button label="Delete" variant="danger" class="ms-2" id="delete-btn" type="submit" />

@props に列挙したキーはデータ変数になります。キーに値を付けるとその値がデフォルトです。@props に無い属性は $attributes バッグに入ります。

ネストしたディレクトリはドット記法で呼び出します。たとえば resources/views/components/card/note.blade.php は <x-card.note> です。ディレクトリと同名のファイル(components/card/card.blade.php)や components/menu/index.blade.php は、それぞれ <x-card> / <x-menu> として使えます。

クラスなしの Blade だけを作る場合は、次のコマンドも使えます。

php artisan make:component forms.input --view

resources/views/components/forms/input.blade.php だけが作られ、<x-forms.input /> で呼び出せます。

属性の渡し方

文字列はそのまま属性として渡します。変数や式を渡すときは、属性名の前にコロンを付けます。

<x-alert type="error" :message="$message" class="mt-4" />
<x-card class="mb-4" :active="true">

type="error" は文字列 "error" です。:message="$message" は PHP の $message の値を渡します。:active="true" は真偽値の true を渡します。

スロット

x-slot タグに入っていない内容は、コンポーネント側で {{ $slot }} として受け取れます。名前付きスロットは <x-slot:title>...</x-slot> のように書き、コンポーネント側では $title として使えます。

カードの例です。

resources/views/components/card/card.blade.php:

@props(['active' => false])

<section {{ $attributes->class(['card', 'card-active' => $active]) }}>
    <h2 class="card-title">{{ $title }}</h2>
    <div class="card-body">{{ $slot }}</div>
</section>

呼び出し側:

<x-card class="mb-4" :active="true">
    <x-slot:title>Card heading</x-slot>
    Card body content
    <x-card.note data-role="note">Nested note</x-card.note>
</x-card>

属性のマージ(class とそれ以外)

{{ $attributes }} は、@props に含まれない追加属性をすべて出力します。デフォルトを足したいときは $attributes->merge() を使います。

ここで重要なのは、class とそれ以外の属性で挙動が違う点です。

  • class: コンポーネント側のクラスと呼び出し側のクラスが結合されます。上のボタン例では、デフォルトの btn btn-danger に呼び出し側の ms-2 が付いて class="btn btn-danger ms-2" になります。
  • type などの非 class 属性: merge() で渡した値はデフォルトです。呼び出し側が同じ属性を指定すると、呼び出し側の値に置き換わります。ボタン例ではデフォルト type="button" に対し、呼び出し側の type="submit" が使われます。

条件付きでクラスを足すときは $attributes->class() が便利です。キーにクラス名、値に真偽値を書くと、真のときだけそのクラスが付きます。そのうえで呼び出し側のクラスも結合されます。上のカード例では :active="true" と class="mb-4" により、描画結果は class="card card-active mb-4" になります。

レイアウトコンポーネント

ページ共通のヘッダやフッタは、レイアウト用コンポーネントにまとめるのが分かりやすいやり方です。

resources/views/components/layout.blade.php:

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>{{ $title ?? 'Blade Demo' }}</title>
    </head>
    <body>
        <header id="site-header">My Blade Site</header>
        <main>
            {{ $slot }}
        </main>
        <footer id="site-footer">Footer text</footer>
    </body>
</html>

ページ側は、レイアウトで包むだけです。

<x-layout>
    <x-slot:title>
        Components Demo
    </x-slot>

    <x-button label="Save" />
    {{-- 他のコンポーネント --}}
</x-layout>

<x-slot:title> を渡すとそのタイトルが使われ、省略すると 'Blade Demo' が使われます。ルートから view('demo', ['message' => '...']) のように返すと、HTTP 200 の HTML としてレイアウトと部品がまとめて返ります。

クラスベースコンポーネント

ロジックを PHP クラスに置きたいときは、クラスベースコンポーネントを使います。

php artisan make:component Alert

次の2ファイルが作られます。

  • app/View/Components/Alert.php(App\View\Components、Illuminate\View\Component を継承、render() が view('components.alert') を返す)
  • resources/views/components/alert.blade.php

コンストラクタの public プロパティは、ビュー側で同名の変数として使えます。

<?php

namespace App\View\Components;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class Alert extends Component
{
    public function __construct(
        public string $type,
        public string $message,
    ) {}

    public function render(): View|Closure|string
    {
        return view('components.alert');
    }
}

ビュー側:

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $message }}
</div>

呼び出し:

<x-alert type="error" :message="$message" class="mt-4" />

描画結果の例は class="alert alert-error mt-4" です。app/View/Components と resources/views/components 配下のコンポーネントは自動で発見されるため、別途登録する必要はありません。

テンプレート継承との対応(参考)

Laravel には、コンポーネント以前から使われてきたテンプレート継承もあります。対応関係だけ押さえておきます。

コンポーネント テンプレート継承
<x-layout>...</x-layout> @extends('layouts.app')
{{ $slot }} / 名前付きスロット @yield('content') / @section
(レイアウト側の既定セクション相当) @section('sidebar') ... @show と子の @parent

レイアウト側の例:

<html>
    <head>
        <title>App Name - @yield('title')</title>
    </head>
    <body>
        @section('sidebar')
            This is the master sidebar.
        @show

        <div class="container">
            @yield('content')
        </div>
    </body>
</html>

子ビュー側:

@extends('layouts.app')

@section('title', 'Page Title')

@section('sidebar')
    @parent

    <p>This is appended to the master sidebar.</p>
@endsection

@section('content')
    <p>This is my body content.</p>
@endsection

子のセクション内で @parent を書くと、レイアウト側の同名セクション内容のあとに子の内容が続きます。公式ドキュメントの表示ではエスケープのため @@parent と見えることがありますが、Blade ファイルに書くディレクティブは @parent(@ は1つ)です。@@parent をそのまま書くと、画面上に @parent という文字が出るだけで継承は動きません。

新規の画面ではコンポーネントを主に使い、既存の @extends ベースの画面を直すときだけ継承を使う、という切り分けで十分なことが多いです。

描画結果をテストする

Feature テストでは、ルートへ get() して HTML に期待文字列が含まれるかを見ると、レイアウトと部品の組み合わせを確認しやすいです。

<?php

namespace Tests\Feature;

use Tests\TestCase;

class BladeComponentsTest extends TestCase
{
    public function test_demo_page_renders_layout_and_components(): void
    {
        $response = $this->get('/demo');

        $response->assertStatus(200);
        $response->assertSee('<title>Components Demo</title>', false);
        $response->assertSee('My Blade Site');
        $response->assertSee('class="btn btn-danger ms-2"', false);
        $response->assertSee('class="alert alert-error mt-4"', false);
    }

    public function test_layout_uses_default_title_without_slot(): void
    {
        $this->get('/plain')->assertOk()->assertSee('<title>Blade Demo</title>', false);
    }
}

assertSee の第2引数に false を渡すと、HTML タグをエスケープせずに照合できます。テスト全般の進め方は【PHP】Laravelでテストを実装する方法を参照してください。

php artisan test

まとめ

  • 匿名コンポーネントは resources/views/components に Blade を置くだけで <x-...> として使える。ネストはドット記法
  • @props でデータ変数を宣言し、それ以外は $attributes へ入る。式は :属性名 で渡す
  • 既定のスロットは {{ $slot }}、名前付きは <x-slot:名前> → $名前
  • $attributes->merge() では class が結合され、type などの非 class 属性は呼び出し側で上書きされる
  • レイアウトもコンポーネント(<x-layout>)として書ける。クラスベースは make:component で PHP クラスとビューが対になる
  • テンプレート継承は短く対応づける程度でよい。@parent は @ を1つだけ書く
  • テストは get() と assertSee で描画結果を確認する

関連記事

参考リンク

コメントする