Laravel

Laravel Blade応用ガイド|コンポーネント・ディレクティブ・レイアウト継承

Laravel Blade テンプレート

Laravel Blade応用ガイド
コンポーネント・ディレクティブ・レイアウト継承

LaravelのBladeテンプレートの応用テクニックを解説。レイアウト継承、コンポーネント、カスタムディレクティブまで実践的に学べます。

こんな人向けの記事です

  • Bladeテンプレートをもっと活用したい
  • コンポーネントの使い方を理解したい
  • カスタムディレクティブを作りたい

Step 1レイアウトの継承(@extends, @section, @yield)

Bladeのレイアウト継承は、サイト全体の共通レイアウトを1つのファイルに定義し、各ページで必要な部分だけを差し替える仕組みです。HTMLの重複を大幅に削減できます。

親レイアウトの作成

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'My App')</title>
    @yield('styles')
</head>
<body>
    <header>
        <nav>共通ナビゲーション</nav>
    </header>

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

    <footer>
        <p>&copy; 2026 My App</p>
    </footer>

    @yield('scripts')
</body>
</html>

@yield('名前') は、子テンプレートから差し込まれるコンテンツの「穴」を定義します。第2引数でデフォルト値を指定できます。

子テンプレートでの継承

resources/views/posts/index.blade.php
@extends('layouts.app')

@section('title', '記事一覧')

@section('styles')
    <link rel="stylesheet" href="/css/posts.css">
@endsection

@section('content')
    <h1>記事一覧</h1>
    @foreach($posts as $post)
        <article>
            <h2>{{ $post->title }}</h2>
            <p>{{ $post->excerpt }}</p>
        </article>
    @endforeach
@endsection

@section('scripts')
    <script src="/js/posts.js"></script>
@endsection
ディレクティブ役割使用場所
@yield('name')コンテンツの挿入場所を定義親レイアウト
@yield('name', 'default')デフォルト値付きの挿入場所親レイアウト
@extends('layout')親レイアウトを継承子テンプレート先頭
@section('name') ... @endsectionセクションの内容を定義子テンプレート
@section('name', '1行テキスト')1行のセクション定義子テンプレート
@parent親セクションの内容を保持して追加子テンプレートのsection内

@parent で親の内容を保持する

resources/views/layouts/app.blade.php
@section('sidebar')
    <nav>共通サイドバー</nav>
@show
resources/views/posts/show.blade.php
@section('sidebar')
    @parent
    <nav>記事固有のサイドバー</nav>
@endsection

@show と @endsection の違い:親テンプレートでセクションを定義してその場に表示する場合は @show を使います。@endsection はセクション定義の終了のみで、表示は @yield に任せます。

注意@extends は必ずテンプレートの最初の行に記述してください。前にHTMLや空白があるとレイアウト継承が正しく動作しません。

Step 2コンポーネントとスロット

Laravel 7以降で導入されたBladeコンポーネントは、再利用可能なUI部品を作るための仕組みです。クラスベースと匿名コンポーネントの2種類があります。

匿名コンポーネントの作成

クラスファイル不要で、Bladeテンプレートだけで定義できる方法です。

resources/views/components/alert.blade.php
@props(['type' => 'info', 'dismissible' => false])

<div class="alert alert-{{ $type }} {{ $dismissible ? 'alert-dismissible' : '' }}">
    {{ $slot }}
    @if($dismissible)
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    @endif
</div>
使用する側のテンプレート
<x-alert type="success" :dismissible="true">
    保存が完了しました!
</x-alert>

<x-alert type="danger">
    エラーが発生しました。
</x-alert>

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

ターミナル
php artisan make:component Card

このコマンドで2つのファイルが生成されます。

app/View/Components/Card.php
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class Card extends Component
{
    public string $title;
    public string $color;

    public function __construct(string $title, string $color = 'white')
    {
        $this->title = $title;
        $this->color = $color;
    }

    public function isHighlighted(): bool
    {
        return $this->color !== 'white';
    }

    public function render()
    {
        return view('components.card');
    }
}
resources/views/components/card.blade.php
<div class="card" style="background-color: {{ $color }}">
    <div class="card-header">
        <h3>{{ $title }}</h3>
    </div>
    <div class="card-body">
        {{ $slot }}
    </div>
    @if(isset($footer))
        <div class="card-footer">
            {{ $footer }}
        </div>
    @endif
</div>

名前付きスロット

コンポーネントの使用例
<x-card title="お知らせ" color="#f0f8ff">
    <p>新機能がリリースされました。</p>

    <x-slot:footer>
        <a href="/news">詳細を見る</a>
    </x-slot:footer>
</x-card>
概念説明
@propsコンポーネントが受け取るプロパティを定義@props(['type' => 'info'])
{{ $slot }}デフォルトスロット(タグ間のコンテンツ)<x-alert>ここ</x-alert>
<x-slot:name>名前付きスロット<x-slot:footer>...</x-slot>
: プレフィックスPHP式として属性値を渡す:dismissible="true"
{{ $attributes }}未定義の属性をそのまま出力class, id等のHTML属性

$attributes の活用{{ $attributes->merge(['class' => 'default-class']) }} とすることで、デフォルトのCSSクラスを設定しつつ、呼び出し側で追加のクラスやHTML属性を渡せます。

Step 3@include と @each

@include はテンプレートの一部を別ファイルから読み込むディレクティブです。コンポーネントよりも軽量で、単純な部品の再利用に向いています。

基本的な @include

resources/views/posts/show.blade.php
{{-- 基本のinclude --}}
@include('partials.header')

{{-- 変数を渡して読み込み --}}
@include('partials.sidebar', ['categories' => $categories])

{{-- 条件付きinclude --}}
@includeWhen($user->isAdmin(), 'partials.admin-panel')

{{-- テンプレートが存在する場合のみinclude --}}
@includeIf('partials.optional-widget')

{{-- 条件が偽のときinclude --}}
@includeUnless($user->isGuest(), 'partials.user-menu')

@each でコレクションをループ表示

@each は配列やコレクションの各要素に対してパーシャルを適用します。@foreach + @include の省略記法です。

resources/views/components/comment-item.blade.php
<div class="comment">
    <p class="comment-author">{{ $comment->user->name }}</p>
    <p class="comment-body">{{ $comment->body }}</p>
    <span class="comment-date">{{ $comment->created_at->diffForHumans() }}</span>
</div>
resources/views/posts/show.blade.php
{{-- @each('パーシャル名', $コレクション, '変数名', '空の時のパーシャル') --}}
@each('components.comment-item', $comments, 'comment', 'components.no-comments')
ディレクティブ用途引数
@includeパーシャルを読み込みテンプレート名, データ配列(任意)
@includeWhen条件が真のとき読み込み条件, テンプレート名, データ配列
@includeUnless条件が偽のとき読み込み条件, テンプレート名, データ配列
@includeIfテンプレートが存在すれば読み込みテンプレート名, データ配列
@eachコレクションをループ表示テンプレート名, コレクション, 変数名, 空時テンプレート

注意@include で読み込まれたテンプレートは、親テンプレートの変数をすべて引き継ぎます。意図しない変数の参照を避けるため、必要な変数だけを第2引数で明示的に渡すことを推奨します。

Step 4カスタムディレクティブ

Bladeでは独自のディレクティブを定義して、テンプレート内で使い回すことができます。AppServiceProviderboot メソッドで登録します。

基本的なカスタムディレクティブ

app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    // 日時フォーマットのディレクティブ
    Blade::directive('datetime', function (string $expression) {
        return "<?php echo ($expression)->format('Y年m月d日 H:i'); ?>";
    });

    // 金額フォーマットのディレクティブ
    Blade::directive('money', function (string $expression) {
        return "<?php echo '¥' . number_format($expression); ?>";
    });

    // 改行をbrタグに変換
    Blade::directive('nl2br', function (string $expression) {
        return "<?php echo nl2br(e($expression)); ?>";
    });
}
テンプレートでの使用
投稿日:@datetime($post->created_at)
価格:@money($product->price)
本文:@nl2br($post->body)

カスタム if ディレクティブ

Blade::if() を使うと、条件分岐のカスタムディレクティブを簡潔に定義できます。

app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    // 環境チェック
    Blade::if('env', function (string $environment) {
        return app()->environment($environment);
    });

    // 管理者チェック
    Blade::if('admin', function () {
        return auth()->check() && auth()->user()->is_admin;
    });

    // ロールチェック
    Blade::if('role', function (string $role) {
        return auth()->check() && auth()->user()->hasRole($role);
    });
}
テンプレートでの使用
@env('production')
    <!-- 本番環境のみ表示 -->
    <script src="/js/analytics.js"></script>
@endenv

@admin
    <a href="/admin">管理画面</a>
@else
    <p>一般ユーザーです</p>
@endadmin

@role('editor')
    <button>記事を編集</button>
@endrole

キャッシュのクリア:カスタムディレクティブを追加・変更した後は php artisan view:clear を実行してテンプレートキャッシュをクリアしてください。変更が反映されない場合はこれが原因です。

Step 5サービスインジェクション

@inject ディレクティブを使うと、Laravelのサービスコンテナからサービスクラスのインスタンスをテンプレートに直接注入できます。コントローラーを経由せずにデータを取得したい場合に便利です。

app/Services/StatsService.php
<?php

namespace App\Services;

use App\Models\Post;
use App\Models\User;

class StatsService
{
    public function totalPosts(): int
    {
        return Post::count();
    }

    public function totalUsers(): int
    {
        return User::count();
    }

    public function popularPosts(int $limit = 5)
    {
        return Post::orderBy('views', 'desc')->take($limit)->get();
    }
}
resources/views/partials/sidebar.blade.php
@inject('stats', 'App\Services\StatsService')

<div class="sidebar-stats">
    <p>総記事数:{{ $stats->totalPosts() }}</p>
    <p>総ユーザー数:{{ $stats->totalUsers() }}</p>

    <h4>人気記事</h4>
    <ul>
        @foreach($stats->popularPosts() as $post)
            <li><a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a></li>
        @endforeach
    </ul>
</div>

第1引数がテンプレート内で使う変数名、第2引数が完全修飾クラス名です。

設定値の注入例
{{-- Carbonインスタンスの注入 --}}
@inject('carbon', 'Carbon\Carbon')
<p>現在時刻:{{ $carbon::now()->format('Y年m月d日 H:i') }}</p>

{{-- メーラーの注入 --}}
@inject('mailer', 'Illuminate\Mail\Mailer')
{{-- サービスのメソッドを直接呼び出せる --}}

注意@inject を多用するとテンプレートにビジネスロジックが入り込み、コードの見通しが悪くなります。基本的にはコントローラーやView Composerからデータを渡し、@inject はサイドバーのような共通パーツの簡易的なデータ取得に限定しましょう。

View Composerとの使い分け

手法定義場所適した用途
@injectテンプレート内特定テンプレートでの簡易的なデータ取得
View ComposerServiceProvider複数テンプレートへの共通データ提供
コントローラーControllerメインのビジネスロジックとデータ提供

Step 6Bladeでのフォーム作成(CSRF・メソッドスプーフィング)

LaravelでHTMLフォームを作成する際、セキュリティのためのCSRFトークンと、HTMLフォームが対応しないHTTPメソッドのスプーフィングが必要です。Bladeではこれらを簡単に実装できます。

CSRFトークンの設定

Laravelでは POST / PUT / PATCH / DELETE リクエストを送るフォームに必ずCSRFトークンが必要です。

resources/views/posts/create.blade.php
<form action="{{ route('posts.store') }}" method="POST">
    @csrf

    <div class="form-group">
        <label for="title">タイトル</label>
        <input type="text" name="title" id="title"
               value="{{ old('title') }}"
               class="@error('title') is-invalid @enderror">
        @error('title')
            <span class="invalid-feedback">{{ $message }}</span>
        @enderror
    </div>

    <div class="form-group">
        <label for="body">本文</label>
        <textarea name="body" id="body">{{ old('body') }}</textarea>
        @error('body')
            <span class="invalid-feedback">{{ $message }}</span>
        @enderror
    </div>

    <button type="submit">投稿する</button>
</form>

@csrf は以下のhiddenフィールドに展開されます。

出力されるHTML
<input type="hidden" name="_token" value="CSRFトークン文字列">

メソッドスプーフィング

HTMLフォームは GET と POST しかサポートしません。PUT / PATCH / DELETE を使うには @method ディレクティブでスプーフィングします。

更新フォーム(PUT)
<form action="{{ route('posts.update', $post) }}" method="POST">
    @csrf
    @method('PUT')

    <input type="text" name="title" value="{{ old('title', $post->title) }}">
    <textarea name="body">{{ old('body', $post->body) }}</textarea>
    <button type="submit">更新する</button>
</form>
削除フォーム(DELETE)
<form action="{{ route('posts.destroy', $post) }}" method="POST"
      onsubmit="return confirm('本当に削除しますか?')">
    @csrf
    @method('DELETE')

    <button type="submit" class="btn btn-danger">削除する</button>
</form>

バリデーションエラーの表示

エラー一覧の表示
{{-- すべてのエラーを一覧表示 --}}
@if($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

{{-- 特定フィールドのエラー表示 --}}
@error('email')
    <span class="text-danger">{{ $message }}</span>
@enderror

{{-- 名前付きエラーバッグ --}}
@error('email', 'login')
    <span class="text-danger">{{ $message }}</span>
@enderror
ディレクティブ用途出力
@csrfCSRFトークンの埋め込み<input type="hidden" name="_token" ...>
@method('PUT')HTTPメソッドのスプーフィング<input type="hidden" name="_method" value="PUT">
@error('field')特定フィールドのエラー表示エラーメッセージ($message変数)
old('field')前回入力値の復元バリデーション失敗時のフォーム値保持

old() の第2引数old('title', $post->title) のように第2引数を設定すると、セッションに前回入力値がない場合(初回表示時)はデフォルト値が使われます。編集フォームでは必ず活用しましょう。

まとめBlade応用テクニックのチェックリスト

  • @extends / @section / @yield でレイアウトを継承できる
  • @parent で親セクションの内容を保持しながら追加できる
  • 匿名コンポーネントとクラスベースコンポーネントを使い分けられる
  • $slot と名前付きスロットでコンポーネントにコンテンツを渡せる
  • @include / @each でパーシャルを効率的に活用できる
  • Blade::directive() / Blade::if() でカスタムディレクティブを作成できる
  • @inject でサービスコンテナからクラスを注入できる
  • @csrf@method で安全なフォームを作成できる
  • @errorold() でバリデーションエラーを表示・入力値を復元できる