LaravelビューとBladeテンプレートエンジン
BladeはLaravelの「レイアウトエンジン」です。データがユーザーに見えるページに変換され, レイアウト, コンポーネント, 継承によって繰り返しのページコードを排除します。
1. 学ぶ内容
- Bladeレイアウト継承:@extends, @section, @yield
- 制御構造:@if/@foreach/@switch/@auth/@guest
- Bladeコンポーネント:クラスコンポーネント vs 匿名コンポーネント
- View Composer:データの共有
- フロントエンドアセット:ViteによるTailwind CSS / Alpine.jsのコンパイル
2. フロントエンド開発者のリアルストーリー
(1) ペインポイント:毎ページでナビゲーションバーを繰り返し書く必要がある
CharlieはShopMetricsのために10ページを作成し, 60行のナビゲーションバーHTMLを各ページに手動でコピーしていました。Bobが「料金設定」リンクをナビゲーションバーからドロップダウンメニューに移動するよう依頼したとき, Charlieは10ファイルすべてを1つずつ編集しなければなりませんでした。7番目のファイルでクラスを見落とし, 本番サイトでナビゲーションバーのスタイルが不統一になり, Aliceから3件の苦情が来ました。
(2) Bladeレイアウトのソリューション
Bladeは@extendsでレイアウトを継承し, @sectionでコンテンツを埋めます。ナビゲーションバーは1度だけ定義され, サイト全体で自動的に同期されます。
// resources/views/layouts/app.blade.php — レイアウトを1度だけ定義
<body>
<nav>...</nav> <!-- ナビゲーションは全ページに表示 -->
@yield('content') <!-- 各ページがこのスロットを埋める -->
</body>
// resources/views/shops/index.blade.php — 継承して埋める
@extends('layouts.app')
@section('content')
<h1>All Shops</h1>
@endsection
(3) 成果
CharlieがBladeレイアウトを実装した後, ナビゲーションバーは単一ファイルの保守のみで済むようになりました。Bobの変更要求は「10ファイル修正」から「1ファイル修正」になり, 見落としはゼロになりました。
3. Bladeレイアウト継承
(1) レイアウトの定義
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'ShopMetrics')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
@include('partials.navbar')
<main class="container mx-auto">
@yield('content')
</main>
@include('partials.footer')
</body>
</html>
(2) レイアウトの継承
<!-- resources/views/shops/index.blade.php -->
@extends('layouts.app')
@section('title', 'All Shops')
@section('content')
<div class="grid grid-cols-3 gap-4">
@foreach($shops as $shop)
<div class="card">{{ $shop->name }}</div>
@endforeach
</div>
@endsection
| コマンド | 機能 | 例え |
|---|---|---|
@extends |
継承するレイアウトを指定 | テンプレートを選択 |
@section |
コンテンツブロックを定義 | テンプレートの空欄を埋める |
@yield |
コンテンツブロックを出力 | テンプレート内のプレースホルダー |
@include |
サブビューをインポート | 共通スニペットの挿入 |
(1) ▶ サンプル:ShopMetricsの2カラムレイアウト
<!-- resources/views/layouts/dashboard.blade.php -->
@extends('layouts.app')
@section('content')
<div class="flex">
@include('partials.sidebar')
<div class="flex-1 p-6">
@yield('dashboard-content')
</div>
</div>
@endsection
<!-- resources/views/dashboard/analytics.blade.php -->
@extends('layouts.dashboard')
@section('title', 'Analytics Dashboard')
@section('dashboard-content')
<h1>Analytics Overview</h1>
<div id="chart"></div>
@endsection
出力:
// 実行成功
4. 制御構造
(1) 条件文
@if($shop->is_active)
<span class="badge-green">Active</span>
@elseif($shop->is_suspended)
<span class="badge-yellow">Suspended</span>
@else
<span class="badge-gray">Inactive</span>
@endif
@auth
<a href="{{ route('dashboard') }}">Dashboard</a>
@endauth
@guest
<a href="{{ route('login') }}">Login</a>
@endguest
(2) ループ文
@foreach($shops as $shop)
<tr>
<td>{{ $shop->name }}</td>
<td>{{ $shop->revenue }}</td>
</tr>
@if($loop->last)
</tbody></table>
@endif
@endforeach
@forelse($orders as $order)
<li>{{ $order->total }}</li>
@empty
<li>注文はまだありません。</li>
@endforelse
| ループ変数 | 説明 |
|---|---|
$loop->index |
現在のインデックス (0始まり) |
$loop->iteration |
現在の周回 (1始まり) |
$loop->first |
最初の要素かどうか |
$loop->last |
最後の要素かどうか |
$loop->count |
合計数 |
(1) ▶ サンプル:ShopMetricsダッシュボードの条件付きレンダリング
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.dashboard')
@section('dashboard-content')
@auth
<h1>ようこそ, {{ auth()->user()->name }}さん!</h1>
@endauth
@if($shops->count() > 0)
<div class="grid grid-cols-3 gap-4">
@foreach($shops as $shop)
<div class="card {{ $loop->first ? 'border-blue-500' : '' }}">
<h3>{{ $shop->name }}</h3>
<p>売上: ${{ number_format($shop->revenue, 2) }}</p>
</div>
@endforeach
</div>
@else
<p>店舗はまだありません。<a href="{{ route('shops.create') }}">作成しましょう!</a></p>
@endif
@endsection
出力:
// 実行成功
5. Bladeコンポーネント
(1) 匿名コンポーネント
<!-- resources/views/components/shop-card.blade.php -->
@props(['shop', 'highlight' => false])
<div class="card {{ $highlight ? 'border-gold' : '' }}">
<h3>{{ $shop->name }}</h3>
<p>{{ $shop->description }}</p>
{{ $slot }}
</div>
<!-- 使用方法 -->
<x-shop-card :shop="$shop" :highlight="true">
<p>今週のおすすめ!</p>
</x-shop-card>
(2) クラスコンポーネント
php artisan make:component Alert
# 作成: app/View/Components/Alert.php
# および: resources/views/components/alert.blade.php
// app/View/Components/Alert.php
class Alert extends Component
{
public function __construct(
public string $type = 'info',
public ?string $message = null,
) {}
public function render(): View
{
return view('components.alert');
}
}
<!-- resources/views/components/alert.blade.php -->
<div class="alert alert-{{ $type }}">
{{ $message ?? $slot }}
</div>
<!-- 使用方法 -->
<x-alert type="success" message="Shop created!" />
<x-alert type="warning">設定を確認してください。</x-alert>
| 項目 | 匿名 | クラス |
|---|---|---|
| ファイル | Bladeテンプレートのみ | PHPクラス + Bladeテンプレート |
| ロジック | なし/最小限 | ビジネスロジックを含む場合あり |
| Props | @props()宣言 |
コンストラクタパラメータ |
| 適している | シンプルな表示コンポーネント | ロジックが必要なコンポーネント |
(1) ▶ サンプル:ShopMetrics統計カードコンポーネント
<!-- resources/views/components/stat-card.blade.php -->
@props(['label', 'value', 'icon', 'trend' => null])
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">{{ $label }}</p>
<p class="text-2xl font-bold">{{ $value }}</p>
@if($trend)
<p class="text-sm {{ $trend > 0 ? 'text-green-500' : 'text-red-500' }}">
{{ $trend > 0 ? '+' : '' }}{{ $trend }}%
</p>
@endif
</div>
<span class="text-3xl">{{ $icon }}</span>
</div>
</div>
<!-- ダッシュボードでの使用 -->
<x-stat-card label="Total Revenue" value="$12,450" icon="$" :trend="15.3" />
<x-stat-card label="Active Shops" value="23" icon="#" :trend="-2.1" />
出力:
// 実行成功
6. View Composer
View Composerはビューに共有データを自動的に注入し, 各コントローラーメソッドで繰り返し渡す必要をなくします。
// app/Providers/AppServiceProvider.php
public function boot(): void
{
// 全ダッシュボードビューにテナント情報を共有
View::composer('dashboard.*', function ($view) {
$view->with('tenant', Tenant::current());
});
// 特定のビューにナビゲーションデータを共有
View::composer(['layouts.app', 'shops.*'], NavigationComposer::class);
}
| タイプ | 実行タイミング | 目的 |
|---|---|---|
View::composer() |
ビューがレンダリングされるたび | データを動的に計算 |
View::creator() |
ビューのインスタンス化時 | より早くデータをバインド |
View::share() |
全ビュー | グローバル定数 |
(1) ▶ サンプル:ShopMetricsナビゲーションView Composer
// app/View/Composers/NavigationComposer.php
class NavigationComposer
{
public function compose(View $view): void
{
$view->with([
'navShops' => auth()->check()
? auth()->user()->shops()->take(5)->get()
: collect(),
'navNotifications' => auth()->check()
? auth()->user()->unreadNotifications()->count()
: 0,
]);
}
}
// AppServiceProviderで登録
View::composer('layouts.app', NavigationComposer::class);
出力:
// 実行成功
7. フロントエンドアセット:Vite
Laravel 11はデフォルトでViteを使用してフロントエンドアセットをコンパイルします。
(1) Tailwind CSSとAlpine.jsのインストール
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install alpinejs
(2) Viteの設定
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
}),
],
});
(3) Bladeでの読み込み
<!-- レイアウトのhead内 -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
| コマンド | 機能 |
|---|---|
npm run dev |
Vite開発サーバーを起動 (ホットリロード) |
npm run build |
本番用アセットをコンパイル (圧縮) |
(1) ▶ サンプル:Alpine.jsインタラクション付きShopMetricsナビゲーション
<!-- resources/views/partials/navbar.blade.php -->
<nav class="bg-white shadow" x-data="{ open: false }">
<div class="container mx-auto px-4">
<div class="flex justify-between h-16">
<a href="{{ route('home') }}" class="font-bold text-xl">ShopMetrics</a>
<div class="flex items-center">
@auth
<div class="relative" @click="open = !open">
<button class="flex items-center">
{{ auth()->user()->name }}
@if($navNotifications > 0)
<span class="badge-red">{{ $navNotifications }}</span>
@endif
</button>
<div x-show="open" x-transition class="dropdown">
<a href="{{ route('dashboard') }}">Dashboard</a>
<a href="{{ route('shops.index') }}">Shops</a>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit">Logout</button>
</form>
</div>
</div>
@else
<a href="{{ route('login') }}">Login</a>
@endauth
</div>
</div>
</div>
</nav>
出力:
// 実行成功
8. 総合例:ShopMetricsダッシュボードページ
<!-- ============================================
総合: ShopMetricsダッシュボードページ
内容: レイアウト継承, コンポーネント, ディレクティブ, Alpine.js
============================================ -->
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.dashboard')
@section('title', 'Dashboard — ShopMetrics')
@section('dashboard-content')
<div class="space-y-6" x-data="{ period: '7d' }">
<!-- 期間セレクター -->
<div class="flex gap-2">
<button @click="period = '7d'"
:class="period === '7d' ? 'btn-primary' : 'btn-secondary'">7日間</button>
<button @click="period = '30d'"
:class="period === '30d' ? 'btn-primary' : 'btn-secondary'">30日間</button>
<button @click="period = '90d'"
:class="period === '90d' ? 'btn-primary' : 'btn-secondary'">90日間</button>
</div>
<!-- 統計カード -->
<div class="grid grid-cols-4 gap-4">
<x-stat-card label="Total Revenue" value="${{ number_format($totalRevenue, 0) }}" icon="$" :trend="$revenueTrend" />
<x-stat-card label="Orders" value="{{ number_format($orderCount) }}" icon="#" :trend="$orderTrend" />
<x-stat-card label="Active Shops" value="{{ $activeShops }}" icon="#" :trend="$shopTrend" />
<x-stat-card label="Conversion Rate" value="{{ number_format($conversionRate, 1) }}%" icon="%" :trend="$conversionTrend" />
</div>
<!-- 最近の注文 -->
<div class="bg-white rounded-lg shadow">
<h2 class="p-4 border-b font-semibold">最近の注文</h2>
@forelse($recentOrders as $order)
<div class="p-4 border-b flex justify-between">
<span>{{ $order->product_name }}</span>
<span>${{ number_format($order->total, 2) }}</span>
</div>
@empty
<p class="p-4 text-gray-500">この期間の注文はありません。</p>
@endforelse
</div>
</div>
@endsection
❓ よくある質問
{{ dd($variable) }}を使用するとダンプしてページを一時停止できます。またはLaravel Debugbar拡張をインストールして, すべての変数, SQLクエリ, メモリ使用量を確認できます。view('xxx', ['key' => $value])で明示的にデータを渡し, そのビュー固有のデータに適しています。View Composerは共有データを自動的に注入し, 複数のビューで必要なデータ (ナビゲーションバーの内容や通知数など)に適しています。📖 まとめ
- Bladeは@extends, @section, @yieldを使ってレイアウト継承を実現し, 重複HTMLを排除する
- 制御ディレクティブ@if, @foreach, @auth, @guestでテンプレート内の条件ロジックを処理する
- 匿名コンポーネントはシンプルなコンポーネントに, クラスコンポーネントはロジック付きコンポーネントに適している
- View Composerはビューに共有データを自動注入し, コントローラーでの繰り返し値渡しを不要にする
- ViteはLaravel 11のデフォルトフロントエンドビルドツールで, ホットリロードをサポートする
- {{ }}は文字を自動エスケープしてXSSを防ぐ。生のHTML出力には{!! !!}を慎重に使用する
📝 練習問題
-
基本演習 (⭐):ShopMetricsの
layouts/app.blade.phpレイアウトファイルを作成し, ナビゲーションバーとフッターを含め, このレイアウトを継承するホームページhome/index.blade.phpを作成してコンテンツを埋めてください。 -
応用演習 (⭐⭐):
type(success/warning/error)とスロットコンテンツをサポートする<x-alert>Bladeコンポーネントを作成し, ShopMetricsで作成成功やバリデーション失敗のシナリオで使用してください。 -
チャレンジ (⭐⭐⭐):Alpine.jsを使って折りたたみ可能なサイドバーコンポーネントを実装し, ダッシュボードレイアウトに統合し, モバイルデバイスでの自動折りたたみをサポートし, 状態をlocalStorageに保存してください。



