LaravelのMVCパターン完全解説!実践で使える7つの重要ポイント

目次

目次へ

LaravelのMVCパターンとは?基礎から解説

LaravelのMVCパターンは、Webアプリケーションの構造を整理し、保守性と拡張性を高めるためのアーキテクチャパターンです。Model(モデル)、View(ビュー)、Controller(コントローラー)の3つの層に分離することで、コードの責務を明確に分け、開発効率を向上させます。

MVCパターンが解決する3つの開発の課題

  1. コードの混在による保守性の低下
  • ビジネスロジック、表示ロジック、データ操作が1つのファイルに混在する問題を解決
  • 各層の責務を明確に分離することで、コードの見通しが改善
  • 修正や機能追加時の影響範囲を特定しやすくなる
  1. チーム開発における生産性の低下
  • 複数の開発者が同じファイルを同時に編集することによる競合を減少
  • 各層の担当者(フロントエンド・バックエンド)が並行して開発可能
  • 統一された構造により、新メンバーの学習コストを削減
  1. テストの困難さ
  • 各層を独立してテスト可能な構造を実現
  • ユニットテストの記述が容易になる
  • モックやスタブを使用した isolated テストの実装が可能

LaravelでMVCパターンを採用するメリット

  1. フレームワークレベルでの強力なサポート
  • Eloquent ORMによる直感的なモデル層の実装
  • Bladeテンプレートエンジンによる効率的なビュー層の開発
  • ルーティングとコントローラーの密接な統合
  1. 豊富な開発支援機能
   // モデルの例:Eloquent ORMによる簡潔な記述
   class User extends Model
   {
       // リレーションシップの定義が容易
       public function posts()
       {
           return $this->hasMany(Post::class);
       }
   }
  1. セキュリティ対策の標準装備
  • XSS対策:Bladeテンプレートによる自動エスケープ
  • CSRF対策:フォーム送信時の自動的なトークン検証
  • SQLインジェクション対策:Eloquent ORMとクエリビルダ
  1. 大規模アプリケーションへの対応
  • サービスプロバイダーによる機能の分割と依存性の管理
  • ミドルウェアによるリクエスト/レスポンスの柔軟な制御
  • イベント/リスナーによる疎結合な機能拡張

これらの特徴により、LaravelのMVCパターンは、小規模から大規模まで、様々なWebアプリケーション開発に適した選択肢となっています。特に、チーム開発においては、統一された設計方針とコーディング規約を自然に導入できる点が大きな利点です。

Laravelにおける各層の役割と実装方法

モデル層でのデータ操作とビジネスロジックの実装

モデル層は、アプリケーションのビジネスロジックとデータ操作を担当する重要な層です。Laravelでは、Eloquent ORMを使用して、直感的かつ強力なモデルの実装が可能です。

  1. 基本的なモデルの実装
// app/Models/Product.php
class Product extends Model
{
    // Mass Assignment 保護の設定
    protected $fillable = [
        'name', 'price', 'description', 'stock'
    ];

    // カスタムアクセサの定義
    public function getDisplayPriceAttribute()
    {
        return '¥' . number_format($this->price);
    }

    // スコープの定義
    public function scopeInStock($query)
    {
        return $query->where('stock', '>', 0);
    }

    // リレーションシップの定義
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}
  1. ビジネスロジックの実装例
public function decreaseStock($quantity)
{
    if ($this->stock < $quantity) {
        throw new InsufficientStockException();
    }

    $this->stock -= $quantity;
    $this->save();

    if ($this->stock <= $this->reorder_point) {
        event(new LowStockAlert($this));
    }
}

Controller層でのリクエスト処理とレスポンス生成

コントローラー層は、HTTPリクエストを受け取り、適切な処理を行い、レスポンスを返す役割を担います。

  1. リソースコントローラーの実装
// app/Http/Controllers/ProductController.php
class ProductController extends Controller
{
    public function index()
    {
        $products = Product::inStock()
            ->with('category')
            ->paginate(20);

        return view('products.index', compact('products'));
    }

    public function store(ProductRequest $request)
    {
        $product = Product::create($request->validated());

        return redirect()
            ->route('products.show', $product)
            ->with('success', '商品を登録しました');
    }
}
  1. フォームリクエストによるバリデーション
// app/Http/Requests/ProductRequest.php
class ProductRequest extends FormRequest
{
    public function rules()
    {
        return [
            'name' => 'required|max:255',
            'price' => 'required|integer|min:0',
            'stock' => 'required|integer|min:0',
            'category_id' => 'required|exists:categories,id'
        ];
    }
}

View層でのデータ表示とユーザーインターフェイス

ビュー層は、ユーザーに情報を表示し、インタラクションを提供する役割を担います。Laravelでは、Bladeテンプレートエンジンを使用して効率的なビューの作成が可能です。

  1. レイアウトの実装
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html>
<head>
    <title>@yield('title') - {{ config('app.name') }}</title>
</head>
<body>
    @include('layouts.header')

    @if(session('success'))
        <div class="alert alert-success">
            {{ session('success') }}
        </div>
    @endif

    @yield('content')

    @include('layouts.footer')
</body>
</html>
  1. コンポーネントの活用
<!-- resources/views/components/product-card.blade.php -->
@props(['product'])

<div class="product-card">
    <h3>{{ $product->name }}</h3>
    <p class="price">{{ $product->display_price }}</p>
    <p class="stock">在庫: {{ $product->stock }}個</p>

    @if($product->stock > 0)
        <form action="{{ route('cart.add', $product) }}" method="POST">
            @csrf
            <button type="submit">カートに追加</button>
        </form>
    @else
        <p class="out-of-stock">在庫切れ</p>
    @endif
</div>
  1. ビューでのデータ表示
<!-- resources/views/products/index.blade.php -->
@extends('layouts.app')

@section('title', '商品一覧')

@section('content')
    <div class="products-grid">
        @foreach($products as $product)
            <x-product-card :product="$product" />
        @endforeach
    </div>

    {{ $products->links() }}
@endsection

このように、各層が明確な責務を持ち、それぞれが独立して機能しながらも、密接に連携してアプリケーション全体を構築します。この構造により、コードの保守性が高まり、機能の追加や変更が容易になります。

実践MVC設計パターンの実装手順

ステップ1:モデルのデータベースと設計

モデル設計は、アプリケーションの基盤となる重要なステップです。以下の手順で実装を進めます。

  1. マイグレーションファイルの作成
// database/migrations/2024_02_07_create_orders_table.php
public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained();
        $table->string('order_number')->unique();
        $table->decimal('total_amount', 10, 2);
        $table->enum('status', ['pending', 'processing', 'completed', 'cancelled']);
        $table->timestamps();

        // インデックスの追加による検索パフォーマンスの最適化
        $table->index(['status', 'created_at']);
    });
}
  1. モデルクラスの実装
// app/Models/Order.php
class Order extends Model
{
    use HasFactory;

    protected $fillable = [
        'order_number',
        'total_amount',
        'status'
    ];

    // ステータス定数の定義
    const STATUS_PENDING = 'pending';
    const STATUS_PROCESSING = 'processing';
    const STATUS_COMPLETED = 'completed';
    const STATUS_CANCELLED = 'cancelled';

    // リレーションシップの定義
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function items()
    {
        return $this->hasMany(OrderItem::class);
    }

    // ステータス変更のビジネスロジック
    public function markAsProcessing()
    {
        if ($this->status !== self::STATUS_PENDING) {
            throw new InvalidOrderStatusTransitionException();
        }

        $this->update(['status' => self::STATUS_PROCESSING]);
        event(new OrderStatusChanged($this));
    }
}

ステップ2:コントローラーの実装とルーティング設定

  1. ルーティングの定義
// routes/web.php
Route::middleware(['auth'])->group(function () {
    Route::resource('orders', OrderController::class);

    // カスタムアクション用のルート
    Route::post('orders/{order}/process', [OrderController::class, 'process'])
        ->name('orders.process');
});
  1. コントローラーの実装
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
    public function __construct(
        private OrderRepository $orders,
        private OrderProcessor $processor
    ) {}

    public function index()
    {
        $orders = $this->orders->getPaginatedOrders(
            auth()->id(),
            request('status')
        );

        return view('orders.index', compact('orders'));
    }

    public function store(CreateOrderRequest $request)
    {
        DB::transaction(function () use ($request) {
            $order = $this->orders->create($request->validated());
            $this->processor->processNewOrder($order);
        });

        return redirect()
            ->route('orders.show', $order)
            ->with('success', '注文を受け付けました');
    }

    public function process(Order $order)
    {
        $this->authorize('process', $order);

        try {
            $this->processor->startProcessing($order);
            return back()->with('success', '注文の処理を開始しました');
        } catch (InvalidOrderStatusTransitionException $e) {
            return back()->with('error', '現在の注文ステータスでは処理を開始できません');
        }
    }
}

ステップ3:ビューの作成とブレードテンプレートの活用

  1. 共通レイアウトの作成
<!-- 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') - 注文管理システム</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <x-navigation />

    <main class="container mx-auto px-4 py-6">
        <x-alerts />
        @yield('content')
    </main>

    <x-footer />
</body>
</html>
  1. 注文一覧ビューの実装
<!-- resources/views/orders/index.blade.php -->
@extends('layouts.app')

@section('title', '注文一覧')

@section('content')
    <div class="space-y-6">
        <x-order-filters :status="request('status')" />

        <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
            @foreach($orders as $order)
                <x-order-card :order="$order">
                    @can('process', $order)
                        <x-slot name="actions">
                            <form action="{{ route('orders.process', $order) }}" method="POST">
                                @csrf
                                <x-button type="submit">処理開始</x-button>
                            </form>
                        </x-slot>
                    @endcan
                </x-order-card>
            @endforeach
        </div>

        {{ $orders->withQueryString()->links() }}
    </div>
@endsection
  1. 再利用可能なコンポーネントの作成
<!-- resources/views/components/order-card.blade.php -->
@props(['order'])

<div class="bg-white rounded-lg shadow p-6">
    <div class="flex justify-between items-start">
        <div>
            <h3 class="text-lg font-semibold">
                注文番号: {{ $order->order_number }}
            </h3>
            <p class="text-gray-600">
                {{ $order->created_at->format('Y/m/d H:i') }}
            </p>
        </div>
        <x-status-badge :status="$order->status" />
    </div>

    <div class="mt-4">
        <p class="font-medium">
            合計金額: {{ number_format($order->total_amount) }}円
        </p>
    </div>

    @if(isset($actions))
        <div class="mt-4 space-x-2">
            {{ $actions }}
        </div>
    @endif
</div>

この実装手順に従うことで、保守性が高く、拡張性のあるMVCアプリケーションを構築できます。各層の責務を明確に分離し、適切なデザインパターンを適用することで、コードの品質と開発効率を向上させることができます。

LaravelのMVCパターンにおけるベストプラクティス

ファットモデル・シンコントローラーの原則

ファットモデル・シンコントローラーの原則は、ビジネスロジックをモデルに集中させ、コントローラーをできるだけシンプルに保つアプローチです。

  1. 良い実装例
// app/Models/Order.php
class Order extends Model
{
    public function calculateTotalAmount()
    {
        return $this->items->sum(function ($item) {
            return $item->quantity * $item->unit_price;
        });
    }

    public function canBeCancelled()
    {
        return in_array($this->status, [
            self::STATUS_PENDING,
            self::STATUS_PROCESSING
        ]);
    }

    public function cancel()
    {
        if (!$this->canBeCancelled()) {
            throw new OrderCancellationException('この注文はキャンセルできません');
        }

        $this->status = self::STATUS_CANCELLED;
        $this->cancelled_at = now();
        $this->save();

        event(new OrderCancelled($this));
    }
}

// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
    public function cancel(Order $order)
    {
        $this->authorize('cancel', $order);

        try {
            $order->cancel();
            return back()->with('success', '注文をキャンセルしました');
        } catch (OrderCancellationException $e) {
            return back()->with('error', $e->getMessage());
        }
    }
}

サービスクラスを活用した責務の分離

複雑なビジネスロジックは、専用のサービスクラスに分離することで、コードの再利用性と保守性を向上させます。

// app/Services/OrderService.php
class OrderService
{
    public function __construct(
        private PaymentGateway $paymentGateway,
        private InventoryManager $inventoryManager,
        private NotificationService $notificationService
    ) {}

    public function createOrder(array $data): Order
    {
        return DB::transaction(function () use ($data) {
            // 注文の作成
            $order = Order::create([
                'user_id' => auth()->id(),
                'total_amount' => $this->calculateTotal($data['items'])
            ]);

            // 注文詳細の作成
            $this->createOrderItems($order, $data['items']);

            // 在庫の確保
            $this->inventoryManager->reserveStock($order);

            // 支払い処理
            $this->processPayment($order, $data['payment']);

            // 通知の送信
            $this->notificationService->sendOrderConfirmation($order);

            return $order;
        });
    }

    private function calculateTotal(array $items): float
    {
        return collect($items)->sum(function ($item) {
            return $item['quantity'] * $item['price'];
        });
    }
}

リポジトリパターンの導入方法

リポジトリパターンを導入することで、データアクセスロジックを抽象化し、モデルとの結合度を低減できます。

  1. インターフェースの定義
// app/Repositories/Contracts/OrderRepositoryInterface.php
interface OrderRepositoryInterface
{
    public function findByOrderNumber(string $orderNumber): ?Order;
    public function getPendingOrders(): Collection;
    public function getOrdersByStatus(string $status, int $perPage = 15);
}
  1. リポジトリクラスの実装
// app/Repositories/Eloquent/OrderRepository.php
class OrderRepository implements OrderRepositoryInterface
{
    public function findByOrderNumber(string $orderNumber): ?Order
    {
        return Order::where('order_number', $orderNumber)
            ->with(['items', 'user'])
            ->first();
    }

    public function getPendingOrders(): Collection
    {
        return Order::where('status', Order::STATUS_PENDING)
            ->with(['items'])
            ->orderBy('created_at')
            ->get();
    }

    public function getOrdersByStatus(string $status, int $perPage = 15)
    {
        return Order::where('status', $status)
            ->with(['items', 'user'])
            ->latest()
            ->paginate($perPage);
    }
}
  1. サービスプロバイダーでの登録
// app/Providers/RepositoryServiceProvider.php
class RepositoryServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(
            OrderRepositoryInterface::class,
            OrderRepository::class
        );
    }
}
  1. コントローラーでの使用
class OrderController extends Controller
{
    public function __construct(
        private OrderRepositoryInterface $orderRepository,
        private OrderService $orderService
    ) {}

    public function index(Request $request)
    {
        $status = $request->get('status', Order::STATUS_PENDING);
        $orders = $this->orderRepository->getOrdersByStatus($status);

        return view('orders.index', compact('orders'));
    }
}

これらのベストプラクティスを適用することで、以下のメリットが得られます:

  • コードの責務が明確に分離され、保守性が向上
  • ビジネスロジックの再利用が容易になる
  • テストが書きやすくなる
  • 依存関係が明確になり、変更の影響範囲が把握しやすい
  • チーム開発での作業分担がしやすくなる

特に大規模なアプリケーション開発では、これらのパターンを適切に組み合わせることで、持続可能な開発体制を構築できます。

よくあるMVC実装の問題点と解決方法

コントローラーが肥大化する問題の対処法

コントローラーの肥大化は、MVCパターンにおいて最も一般的な問題の一つです。

  1. 問題のある実装例
class OrderController extends Controller
{
    public function store(Request $request)
    {
        // 入力バリデーション
        $validated = $request->validate([
            'items' => 'required|array',
            'items.*.id' => 'required|exists:products,id',
            'items.*.quantity' => 'required|integer|min:1',
            'shipping_address' => 'required|string',
            'payment_method' => 'required|in:credit_card,bank_transfer'
        ]);

        // 商品情報の取得と在庫チェック
        $items = collect($validated['items'])->map(function ($item) {
            $product = Product::find($item['id']);
            if ($product->stock < $item['quantity']) {
                throw new InsufficientStockException();
            }
            return ['product' => $product, 'quantity' => $item['quantity']];
        });

        // 合計金額の計算
        $totalAmount = $items->sum(function ($item) {
            return $item['product']->price * $item['quantity'];
        });

        // 支払い処理
        $payment = PaymentGateway::process([
            'amount' => $totalAmount,
            'method' => $validated['payment_method']
        ]);

        // 注文の作成
        $order = Order::create([
            'user_id' => auth()->id(),
            'total_amount' => $totalAmount,
            'shipping_address' => $validated['shipping_address'],
            'payment_id' => $payment->id
        ]);

        // 注文詳細の作成と在庫の更新
        foreach ($items as $item) {
            $order->items()->create([
                'product_id' => $item['product']->id,
                'quantity' => $item['quantity'],
                'price' => $item['product']->price
            ]);

            $item['product']->decrement('stock', $item['quantity']);
        }

        // メール送信
        Mail::to($order->user)->send(new OrderConfirmation($order));

        return redirect()->route('orders.show', $order);
    }
}
  1. 改善された実装例
class OrderController extends Controller
{
    public function __construct(
        private OrderService $orderService,
        private ProductRepository $products
    ) {}

    public function store(CreateOrderRequest $request)
    {
        try {
            $order = $this->orderService->createOrder(
                $request->validated()
            );

            return redirect()
                ->route('orders.show', $order)
                ->with('success', '注文を受け付けました');
        } catch (InsufficientStockException $e) {
            return back()
                ->withErrors(['stock' => '在庫が不足しています'])
                ->withInput();
        } catch (PaymentFailedException $e) {
            return back()
                ->withErrors(['payment' => '決済処理に失敗しました'])
                ->withInput();
        }
    }
}

モデルの責務が肥大化する問題の解決策

モデルに過度に責務が集中する問題は、以下のような方法で解決できます。

  1. トレイトを使用した機能の分割
// app/Models/Traits/HasOrderStatus.php
trait HasOrderStatus
{
    public function isPending(): bool
    {
        return $this->status === self::STATUS_PENDING;
    }

    public function markAsProcessing(): void
    {
        $this->update(['status' => self::STATUS_PROCESSING]);
        event(new OrderStatusChanged($this));
    }
}

// app/Models/Traits/ManagesInventory.php
trait ManagesInventory
{
    public function adjustStock(int $quantity): void
    {
        $this->increment('stock', $quantity);

        if ($this->stock <= $this->reorder_point) {
            event(new LowStockAlert($this));
        }
    }
}

// app/Models/Order.php
class Order extends Model
{
    use HasOrderStatus;

    // モデルの基本的な設定のみを記述
}
  1. 値オブジェクトの活用
// app/ValueObjects/Money.php
class Money
{
    public function __construct(
        private float $amount,
        private string $currency = 'JPY'
    ) {}

    public function add(Money $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidCurrencyException();
        }
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function format(): string
    {
        return number_format($this->amount) . ' ' . $this->currency;
    }
}

ビジネスロジックの配置場所の適切な判断方法

ビジネスロジックの配置は、以下の原則に従って判断します:

  1. ドメインサービスの活用
// app/Services/Domain/OrderFulfillmentService.php
class OrderFulfillmentService
{
    public function __construct(
        private InventoryManager $inventory,
        private ShippingProvider $shipping
    ) {}

    public function fulfill(Order $order): void
    {
        if (!$order->isPaid()) {
            throw new UnpaidOrderException();
        }

        DB::transaction(function () use ($order) {
            // 在庫の引き当て
            $this->inventory->allocate($order);

            // 配送手配
            $trackingNumber = $this->shipping->arrange($order);

            // 注文ステータスの更新
            $order->update([
                'status' => Order::STATUS_SHIPPING,
                'tracking_number' => $trackingNumber
            ]);
        });
    }
}
  1. アクションクラスの使用
// app/Actions/Orders/CancelOrder.php
class CancelOrder
{
    public function __construct(
        private RefundService $refundService,
        private InventoryManager $inventoryManager
    ) {}

    public function execute(Order $order): void
    {
        if (!$order->canBeCancelled()) {
            throw new OrderCancellationException();
        }

        DB::transaction(function () use ($order) {
            // 返金処理
            $this->refundService->refund($order->payment);

            // 在庫の戻し
            $this->inventoryManager->returnItems($order->items);

            // 注文のキャンセル
            $order->markAsCancelled();
        });
    }
}

これらの解決策を適用することで、以下のような利点が得られます:

  • コードの責務が適切に分散され、保守性が向上
  • 単一責任の原則に従った設計が可能
  • テストがしやすくなる
  • コードの再利用性が向上
  • 変更の影響範囲が限定的になる

特に大規模なアプリケーションでは、これらのパターンを状況に応じて適切に組み合わせることが重要です。

実践的なコード例で学ぶMVCパターン

ユーザー認証システムの実装例

LaravelのMVCパターンを活用した認証システムの実装例を示します。

  1. 認証用のモデル実装
// app/Models/User.php
class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens;

    protected $fillable = [
        'name', 'email', 'password',
        'last_login_at', 'email_verified_at'
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    // 二要素認証の状態確認
    public function hasTwoFactorEnabled(): bool
    {
        return !is_null($this->two_factor_secret);
    }

    // アカウントのロック状態確認
    public function isLocked(): bool
    {
        return $this->login_attempts >= 5 &&
            $this->locked_until > now();
    }

    // ログイン試行の記録
    public function recordLoginAttempt(): void
    {
        $this->increment('login_attempts');

        if ($this->login_attempts >= 5) {
            $this->locked_until = now()->addMinutes(30);
            $this->save();
        }
    }
}
  1. 認証コントローラーの実装
// app/Http/Controllers/Auth/LoginController.php
class LoginController extends Controller
{
    public function __construct(
        private AuthenticationService $authService
    ) {}

    public function login(LoginRequest $request)
    {
        try {
            $result = $this->authService->attemptLogin(
                $request->validated()
            );

            if ($result->requiresTwoFactor()) {
                return redirect()->route('2fa.challenge');
            }

            return redirect()->intended(route('dashboard'));
        } catch (AccountLockedException $e) {
            return back()
                ->withErrors(['email' => '一時的にアカウントがロックされています']);
        }
    }

    public function twoFactorChallenge(TwoFactorRequest $request)
    {
        try {
            $this->authService->verifyTwoFactor(
                $request->validated()
            );

            return redirect()->intended(route('dashboard'));
        } catch (InvalidTwoFactorCodeException $e) {
            return back()->withErrors(['code' => '無効な認証コードです']);
        }
    }
}

商品管理システムの実装例

商品管理システムにおけるMVCパターンの実装例を示します。

  1. 商品カテゴリーの管理
// app/Models/Category.php
class Category extends Model
{
    use HasSlug;

    protected $fillable = ['name', 'description', 'parent_id'];

    public function children()
    {
        return $this->hasMany(Category::class, 'parent_id');
    }

    public function products()
    {
        return $this->hasMany(Product::class);
    }
}

// app/Http/Controllers/Admin/CategoryController.php
class CategoryController extends Controller
{
    public function __construct(
        private CategoryRepository $categories
    ) {}

    public function index()
    {
        $categories = $this->categories->getTreeStructure();
        return view('admin.categories.index', compact('categories'));
    }

    public function store(CategoryRequest $request)
    {
        $category = $this->categories->create($request->validated());

        return redirect()
            ->route('admin.categories.index')
            ->with('success', 'カテゴリーを作成しました');
    }
}
  1. 商品在庫の管理
// app/Models/Product.php
class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'name', 'description', 'price',
        'stock', 'category_id', 'sku'
    ];

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function isLowStock(): bool
    {
        return $this->stock <= $this->reorder_point;
    }
}

// app/Http/Controllers/Admin/ProductController.php
class ProductController extends Controller
{
    public function __construct(
        private ProductService $productService,
        private CategoryRepository $categories
    ) {}

    public function create()
    {
        $categories = $this->categories->getSelectOptions();
        return view('admin.products.create', compact('categories'));
    }

    public function store(CreateProductRequest $request)
    {
        $product = $this->productService->createProduct(
            $request->validated()
        );

        return redirect()
            ->route('admin.products.show', $product)
            ->with('success', '商品を登録しました');
    }
}

APIエンドポイントの実装例

RESTful APIにおけるMVCパターンの実装例を示します。

  1. APIリソースの定義
// app/Http/Resources/ProductResource.php
class ProductResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->description,
            'price' => [
                'amount' => $this->price,
                'formatted' => '¥' . number_format($this->price)
            ],
            'category' => new CategoryResource($this->whenLoaded('category')),
            'stock_status' => $this->stock > 0 ? 'in_stock' : 'out_of_stock',
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
        ];
    }
}
  1. APIコントローラーの実装
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
    public function __construct(
        private ProductRepository $products
    ) {}

    public function index(ProductIndexRequest $request)
    {
        $products = $this->products->searchProducts(
            $request->validated(),
            $request->input('per_page', 15)
        );

        return ProductResource::collection($products);
    }

    public function store(CreateProductRequest $request)
    {
        $product = $this->products->create($request->validated());

        return new ProductResource($product);
    }

    public function update(UpdateProductRequest $request, Product $product)
    {
        $this->products->update($product, $request->validated());

        return new ProductResource($product->fresh());
    }
}

// app/Http/Controllers/Api/OrderController.php
class OrderController extends Controller
{
    public function __construct(
        private OrderService $orderService
    ) {}

    public function store(CreateOrderRequest $request)
    {
        try {
            $order = $this->orderService->createOrder(
                $request->validated()
            );

            return new OrderResource($order);
        } catch (OutOfStockException $e) {
            return response()->json([
                'message' => '在庫が不足しています',
                'errors' => [
                    'stock' => ['指定された商品の在庫が不足しています']
                ]
            ], 422);
        }
    }
}

これらの実装例は、以下の設計原則に基づいています:

  • 単一責任の原則(SRP)
  • 依存性の注入(DI)
  • インターフェースの分離
  • リポジトリパターンの活用
  • サービスレイヤーの適切な使用

これらの原則を意識しながら実装することで、保守性が高く、拡張性のあるコードを実現できます。

MVCパターンを活用した開発の次のステップ

クリーンアーキテクチャへの発展

MVCパターンをベースに、クリーンアーキテクチャの考え方を取り入れることで、より保守性の高いアプリケーションを実現できます。

  1. ユースケース層の導入
// app/UseCases/CreateOrder/CreateOrderUseCase.php
class CreateOrderUseCase
{
    public function __construct(
        private OrderRepository $orders,
        private ProductRepository $products,
        private PaymentGateway $paymentGateway
    ) {}

    public function execute(CreateOrderInput $input): CreateOrderOutput
    {
        // 入力データの検証
        $products = $this->validateProducts($input->getProductIds());

        // ビジネスルールの適用
        $order = new Order([
            'user_id' => $input->getUserId(),
            'total_amount' => $this->calculateTotal($products, $input->getQuantities())
        ]);

        // トランザクション処理
        DB::transaction(function () use ($order, $products, $input) {
            // 在庫の確認と更新
            $this->updateInventory($products, $input->getQuantities());

            // 支払い処理
            $payment = $this->processPayment($input->getPaymentData());

            // 注文の保存
            $this->orders->save($order);

            // 注文詳細の作成
            $this->createOrderItems($order, $products, $input->getQuantities());
        });

        return new CreateOrderOutput($order);
    }
}

// app/UseCases/CreateOrder/CreateOrderInput.php
class CreateOrderInput
{
    public function __construct(
        private array $productIds,
        private array $quantities,
        private int $userId,
        private array $paymentData
    ) {}

    // Getterメソッド...
}
  1. エンティティとドメインオブジェクトの分離
// app/Domain/Order/Order.php
class Order
{
    private Collection $items;
    private OrderStatus $status;
    private Money $totalAmount;

    public function addItem(Product $product, int $quantity): void
    {
        if ($this->status->isPaid()) {
            throw new OrderAlreadyPaidException();
        }

        $this->items->add(new OrderItem($product, $quantity));
        $this->recalculateTotal();
    }

    public function pay(Payment $payment): void
    {
        if (!$this->canBePaid()) {
            throw new InvalidOrderStateException();
        }

        $this->status = OrderStatus::paid();
        $this->payment = $payment;
    }
}

ドメイン駆動設計との組み合わせ

ドメイン駆動設計(DDD)の考え方を取り入れることで、ビジネスロジックをより明確に表現できます。

  1. 集約ルートの実装
// app/Domain/Order/OrderAggregate.php
class OrderAggregate implements AggregateRoot
{
    private Collection $domainEvents;

    public function __construct(
        private OrderId $id,
        private UserId $userId,
        private OrderStatus $status,
        private Collection $items
    ) {
        $this->domainEvents = new Collection;
    }

    public function addItem(ProductId $productId, Quantity $quantity): void
    {
        // ビジネスルールの検証
        if ($this->status->isNotDraft()) {
            throw new OrderNotEditableException();
        }

        $this->items->add(new OrderItem($productId, $quantity));

        // ドメインイベントの発行
        $this->raise(new OrderItemAdded($this->id, $productId, $quantity));
    }

    public function confirm(): void
    {
        if ($this->items->isEmpty()) {
            throw new EmptyOrderException();
        }

        $this->status = OrderStatus::confirmed();
        $this->raise(new OrderConfirmed($this->id));
    }
}
  1. ドメインサービスの実装
// app/Domain/Order/Services/OrderPricingService.php
class OrderPricingService
{
    public function __construct(
        private ProductPriceRepository $prices,
        private DiscountPolicy $discountPolicy
    ) {}

    public function calculateTotal(OrderAggregate $order): Money
    {
        $subtotal = $this->calculateSubtotal($order);
        $discount = $this->discountPolicy->calculateDiscount($order);

        return $subtotal->subtract($discount);
    }
}

テスタビリティの向上とユニットテストの実現

テスト容易性を考慮した設計により、信頼性の高いアプリケーションを実現できます。

  1. ユースケースのテスト
class CreateOrderUseCaseTest extends TestCase
{
    private CreateOrderUseCase $useCase;
    private MockInterface $orderRepository;
    private MockInterface $productRepository;
    private MockInterface $paymentGateway;

    protected function setUp(): void
    {
        parent::setUp();

        $this->orderRepository = Mockery::mock(OrderRepository::class);
        $this->productRepository = Mockery::mock(ProductRepository::class);
        $this->paymentGateway = Mockery::mock(PaymentGateway::class);

        $this->useCase = new CreateOrderUseCase(
            $this->orderRepository,
            $this->productRepository,
            $this->paymentGateway
        );
    }

    public function testCreateOrderSuccessfully(): void
    {
        // テストデータの準備
        $input = new CreateOrderInput(
            productIds: [1, 2],
            quantities: [2, 1],
            userId: 1,
            paymentData: ['method' => 'credit_card']
        );

        // モックの設定
        $this->productRepository
            ->shouldReceive('findMany')
            ->with([1, 2])
            ->andReturn(collect([
                new Product(['id' => 1, 'price' => 1000]),
                new Product(['id' => 2, 'price' => 2000])
            ]));

        // テストの実行
        $output = $this->useCase->execute($input);

        // アサーション
        $this->assertEquals(4000, $output->getOrder()->total_amount);
        $this->assertEquals(OrderStatus::PENDING, $output->getOrder()->status);
    }
}
  1. ドメインモデルのテスト
class OrderAggregateTest extends TestCase
{
    public function testCannotAddItemToConfirmedOrder(): void
    {
        // テストデータの準備
        $order = new OrderAggregate(
            id: new OrderId(1),
            userId: new UserId(1),
            status: OrderStatus::confirmed(),
            items: new Collection
        );

        // 例外の検証
        $this->expectException(OrderNotEditableException::class);

        // テストの実行
        $order->addItem(
            new ProductId(1),
            new Quantity(2)
        );
    }

    public function testCalculateTotalAmount(): void
    {
        // テストデータの準備
        $order = new OrderAggregate(
            id: new OrderId(1),
            userId: new UserId(1),
            status: OrderStatus::draft(),
            items: new Collection
        );

        $order->addItem(new ProductId(1), new Quantity(2));
        $order->addItem(new ProductId(2), new Quantity(1));

        // ドメインサービスの利用
        $pricingService = new OrderPricingService(
            new InMemoryProductPriceRepository([
                1 => new Money(1000),
                2 => new Money(2000)
            ]),
            new NoDiscountPolicy()
        );

        // テストの実行
        $total = $pricingService->calculateTotal($order);

        // アサーション
        $this->assertEquals(
            new Money(4000),
            $total
        );
    }
}

これらの発展的なアプローチを導入することで、以下のメリットが得られます:

  • ビジネスロジックの明確な表現
  • ドメインの変更に対する柔軟な対応
  • テストの容易性向上
  • コードの再利用性の向上
  • チーム間のコミュニケーション改善

特に大規模なアプリケーション開発では、これらのアプローチを段階的に導入することで、持続可能な開発体制を構築できます。