PHPにおけるサービスレイヤーパターンの効果的な実装と管理

PHPにおけるサービスレイヤーパターンの効果的な実装と管理 プログラミング

MVCアーキテクチャからサービスレイヤーパターンへの移行は、多くのPHPプロジェクトで見られます。
これは自然な進化と言えるでしょう。

しかし、ただコードをコントローラーからサービスレイヤーに移動させるだけでは不十分です。
本記事では、サービスレイヤーパターンを効果的に実装し管理するための実践的なアプローチを紹介します。

サービスレイヤーの役割と課題

典型的なMVCアプローチでは、コントローラーがビジネスロジックを処理します。

そして、モデルがデータアクセスを担当します。
プロジェクトが成長すると、コントローラーは肥大化します。

その結果、テストや保守が困難になります。
サービスレイヤーの導入はこの問題を解決するはずです。

しかし、多くの場合、新たな問題が発生します。
「玄関を綺麗に保つために、すべての物をガレージに移動したら、今度はガレージが散らかってしまった」

これは複雑性を単に別の場所に移動させただけの状態です。
根本的な問題は解決していません。

効果的なサービスレイヤー管理のアプローチ

レイヤー間の明確な境界を設定する

サービスレイヤーはHTTPリクエスト/レスポンスの詳細から独立している必要があります。
コントローラーの役割は次の2つです。

  1. HTTPリクエストをドメインメッセージに変換する
  2. ドメインの結果をHTTPレスポンスにシリアライズする

悪い例:

// コントローラー内でビジネスロジックを処理
public function process(Request $request): Response
{
    $userId = $request->get('user_id');
    $amount = $request->get('amount');

    // ビジネスロジックがコントローラー内に存在
    if ($amount > 1000) {
        return new Response('金額が上限を超えています', 400);
    }

    $db->executeQuery("UPDATE users SET balance = balance - $amount WHERE id = $userId");
    return new Response('処理完了', 200);
}

良い例:

// コントローラー
public function process(Request $request): Response
{
    $orderId = $request->get('order_id');
    $percent = $request->get('percent');

    try {
        $order = $this->orderService->applyDiscount($orderId, $percent);
        return new Response('割引適用完了', 200);
    } catch (InvalidDiscountException $e) {
        return new Response($e->getMessage(), 400);
    }
}

// サービスレイヤー
public function applyDiscount(int $orderId, float $percent): Order
{
    $order = $this->orderRepository->find($orderId);
    if ($percent > 50) {
        throw new InvalidDiscountException('50%以上の割引はできません');
    }

    $order->applyDiscount($percent);
    $this->orderRepository->save($order);

    return $order;
}

この分離により、サービスレイヤーは純粋にビジネスロジックに集中できます。
また、テストや再利用が容易になります。

アクションパターン/コマンドパターンの活用

大きなサービスクラスを多数のメソッドで構成するのではなく、別のアプローチも考えられます。
単一責任を持つ小さなアクションクラスに分割すると効果的です。

// 大きなサービスクラスではなく
class UserService
{
    public function register(array $data) { /* ... */ }
    public function authenticate(string $email, string $password) { /* ... */ }
    public function resetPassword(string $email) { /* ... */ }
    // 他多数のメソッド
}

// 個別のアクションクラスに分割する
class RegisterUserAction
{
    public function execute(array $data)
    {
        // ユーザー登録に特化したロジック
    }
}

class AuthenticateUserAction
{
    public function execute(string $email, string $password)
    {
        // 認証に特化したロジック
    }
}

各アクションは単一の処理に集中します。
そして、必要なインフラストラクチャコンポーネントへの依存関係を明示的に宣言します。

このアプローチはコードの見通しを良くします。
また、テストも簡略化されます。

ドメイン駆動設計(DDD)の概念の適用

サービスレイヤーの効果的な組織化には、DDDの概念が非常に有効です。

戦略的DDD
ドメインの大きな構造を把握します。
そして、境界付けられたコンテキストを識別します。
これにより、サービスを論理的なグループに整理できます。

戦術的DDD
エンティティ、値オブジェクト、ドメインサービス、リポジトリなどの構成要素を活用します。
これらでドメインロジックを表現します。
例えば、オンラインショップを構築する場合、次のように分割できます。

  • 「カタログ」
  • 「注文」
  • 「支払い」
  • 「配送」

これらの境界付けられたコンテキスト内でサービスやアクションを整理します。
すると、コードの理解と保守が容易になります。

実践的なサービスレイヤー設計の例

オンラインショップの注文処理を例に考えてみましょう:

// 従来のアプローチ(巨大なサービスクラス)
class OrderService
{
    public function createOrder(int $userId, array $items, string $paymentMethod)
    {
        // 在庫チェック
        // 価格計算
        // 注文レコード作成
        // 支払い処理
        // メール送信
        // ロギング
        // すべてが一つのメソッド内に...
    }
}

// 改善されたアプローチ(単一責任のアクション)
class CreateOrderAction
{
    public function __construct(
        private StockChecker $stockChecker,
        private PriceCalculator $priceCalculator,
        private OrderRepository $orderRepository,
        private PaymentProcessor $paymentProcessor,
        private OrderConfirmationMailer $mailer,
        private LoggerInterface $logger
    ) {}

    public function execute(CreateOrderCommand $command): Order
    {
        // 在庫チェック
        $this->stockChecker->checkAvailability($command->getItems());

        // 価格計算
        $totalPrice = $this->priceCalculator->calculate($command->getItems());

        // 注文作成
        $order = Order::create(
            $command->getUserId(),
            $command->getItems(),
            $totalPrice,
            $command->getPaymentMethod()
        );
        $this->orderRepository->save($order);

        // 支払い処理
        $this->paymentProcessor->process($order);

        // メール送信
        $this->mailer->sendConfirmation($order);

        // ロギング
        $this->logger->info('Order created', ['order_id' => $order->getId()]);

        return $order;
    }
}

このアプローチは、依存関係が明示的で、テストが容易な設計をもたらします。
各コンポーネントはモックに置き換えられます。

そのため、単体テストを書きやすくなります。

サービスレイヤーのテスト戦略

サービスレイヤーは、ビジネスロジックの中心的な部分です。
そのため、テストカバレッジを高く保つことが重要です。

// アクションのテスト例
public function testCreateOrderAction()
{
    // モックの設定
    $stockChecker = $this->createMock(StockChecker::class);
    $stockChecker->expects($this->once())
        ->method('checkAvailability')
        ->with($this->anything());

    $priceCalculator = $this->createMock(PriceCalculator::class);
    $priceCalculator->expects($this->once())
        ->method('calculate')
        ->willReturn(100.0);

    // 他のモックも同様に設定...

    // テスト対象のアクションを作成
    $action = new CreateOrderAction(
        $stockChecker,
        $priceCalculator,
        /* 他の依存関係 */
    );

    // 実行
    $command = new CreateOrderCommand(1, [/* 商品データ */], 'credit_card');
    $order = $action->execute($command);

    // 検証
    $this->assertEquals(100.0, $order->getTotalPrice());
    // その他の検証...
}

単一責任の原則に従ったアクションクラスは、このようにテストしやすくなります。

進化するアーキテクチャの検討

プロジェクトの規模や要件に合わせて、適切なアプローチを選択することが重要です。
小規模なプロジェクトでは従来のMVCでも十分な場合があります。

一方、複雑さが増すと、より構造化されたアプローチが有効になります。
さらに洗練されたパターンも検討する価値があります。

例えば:

  • 垂直スライスアーキテクチャ(Vertical Slice Architecture)
  • CQRS(Command Query Responsibility Segregation)

これらのパターンは、特に大規模で複雑なドメインに対して効果を発揮します。

まとめ

サービスレイヤーパターンを効果的に実装するには、単にコードを移動させるだけでは不十分です。
適切な構造化と責任の分離が必要です。

アクションパターンやDDDの概念を応用すれば、保守性が高くテストしやすいコードベースを構築できます。
最終的には、プロジェクトの特性に合わせてアプローチを調整することが重要です。

過度に複雑な設計は避けましょう。
チームが理解し保守できる範囲でパターンを適用すべきです。

サービスレイヤーの導入は目的ではなく手段です。
より読みやすく、テストしやすく、保守性の高いコードを作ることが本来の目標なのです。

タイトルとURLをコピーしました