Laravel Blade応用ガイド
コンポーネント・ディレクティブ・レイアウト継承
LaravelのBladeテンプレートの応用テクニックを解説。レイアウト継承、コンポーネント、カスタムディレクティブまで実践的に学べます。
こんな人向けの記事です
- Bladeテンプレートをもっと活用したい
- コンポーネントの使い方を理解したい
- カスタムディレクティブを作りたい
Step 1レイアウトの継承(@extends, @section, @yield)
Bladeのレイアウト継承は、サイト全体の共通レイアウトを1つのファイルに定義し、各ページで必要な部分だけを差し替える仕組みです。HTMLの重複を大幅に削減できます。
親レイアウトの作成
<!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>© 2026 My App</p>
</footer>
@yield('scripts')
</body>
</html>
@yield('名前') は、子テンプレートから差し込まれるコンテンツの「穴」を定義します。第2引数でデフォルト値を指定できます。
子テンプレートでの継承
@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 で親の内容を保持する
@section('sidebar')
<nav>共通サイドバー</nav>
@show
@section('sidebar')
@parent
<nav>記事固有のサイドバー</nav>
@endsection
@show と @endsection の違い:親テンプレートでセクションを定義してその場に表示する場合は @show を使います。@endsection はセクション定義の終了のみで、表示は @yield に任せます。
注意:@extends は必ずテンプレートの最初の行に記述してください。前にHTMLや空白があるとレイアウト継承が正しく動作しません。
Step 2コンポーネントとスロット
Laravel 7以降で導入されたBladeコンポーネントは、再利用可能なUI部品を作るための仕組みです。クラスベースと匿名コンポーネントの2種類があります。
匿名コンポーネントの作成
クラスファイル不要で、Bladeテンプレートだけで定義できる方法です。
@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つのファイルが生成されます。
<?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');
}
}
<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
{{-- 基本の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 の省略記法です。
<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>
{{-- @each('パーシャル名', $コレクション, '変数名', '空の時のパーシャル') --}}
@each('components.comment-item', $comments, 'comment', 'components.no-comments')
| ディレクティブ | 用途 | 引数 |
|---|---|---|
@include | パーシャルを読み込み | テンプレート名, データ配列(任意) |
@includeWhen | 条件が真のとき読み込み | 条件, テンプレート名, データ配列 |
@includeUnless | 条件が偽のとき読み込み | 条件, テンプレート名, データ配列 |
@includeIf | テンプレートが存在すれば読み込み | テンプレート名, データ配列 |
@each | コレクションをループ表示 | テンプレート名, コレクション, 変数名, 空時テンプレート |
注意:@include で読み込まれたテンプレートは、親テンプレートの変数をすべて引き継ぎます。意図しない変数の参照を避けるため、必要な変数だけを第2引数で明示的に渡すことを推奨します。
Step 4カスタムディレクティブ
Bladeでは独自のディレクティブを定義して、テンプレート内で使い回すことができます。AppServiceProvider の boot メソッドで登録します。
基本的なカスタムディレクティブ
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() を使うと、条件分岐のカスタムディレクティブを簡潔に定義できます。
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のサービスコンテナからサービスクラスのインスタンスをテンプレートに直接注入できます。コントローラーを経由せずにデータを取得したい場合に便利です。
<?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();
}
}
@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 Composer | ServiceProvider | 複数テンプレートへの共通データ提供 |
| コントローラー | Controller | メインのビジネスロジックとデータ提供 |
Step 6Bladeでのフォーム作成(CSRF・メソッドスプーフィング)
LaravelでHTMLフォームを作成する際、セキュリティのためのCSRFトークンと、HTMLフォームが対応しないHTTPメソッドのスプーフィングが必要です。Bladeではこれらを簡単に実装できます。
CSRFトークンの設定
Laravelでは POST / PUT / PATCH / DELETE リクエストを送るフォームに必ずCSRFトークンが必要です。
<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フィールドに展開されます。
<input type="hidden" name="_token" value="CSRFトークン文字列">
メソッドスプーフィング
HTMLフォームは GET と POST しかサポートしません。PUT / PATCH / DELETE を使うには @method ディレクティブでスプーフィングします。
<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>
<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
| ディレクティブ | 用途 | 出力 |
|---|---|---|
@csrf | CSRFトークンの埋め込み | <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で安全なフォームを作成できる@errorとold()でバリデーションエラーを表示・入力値を復元できる