🗃️

今さら振り返るLaravelのサービスコンテナ

2024/03/16に公開

サービスコンテナって何?

  • クラスの依存関係を管理し、自動的に解決するためのしくみ
  • アプリケーション内のオブジェクトの生成と依存関係の注入を行う
  • 各クラスのインスタンス化を制御し、必要な依存関係を自動的に渡す

サービスコンテナを使った場合のサンプルコード

よく見る形。ポイントは以下の2点。

  • AppServiceProviderUserRepositorybindしている(なくても自動解決してくれるが)
  • UserServiceをインスタンス化する時、UserRepositoryはbind済みなので引数に渡す必要がない
  • 仮にUserServiceの引数にFooRepositoryが増えた場合でも、呼び出し側の処理は変わらない!ウレシイ!
// app/Models/User.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

// ユーザーモデル
class User extends Model
{
   protected $fillable = ['name'];
}

// app/Repositories/UserRepository.php
namespace App\Repositories;

use App\Models\User;

class UserRepository
{
   /**
    * 指定されたIDのユーザーを取得する
    *
    * @param int $id ユーザーID
    * @return User ユーザーモデル
    * @throws \Illuminate\Database\Eloquent\ModelNotFoundException ユーザーが見つからない場合
    */
   public function find($id)
   {
       return User::findOrFail($id);
   }
}

// app/Services/UserService.php
namespace App\Services;

use App\Repositories\UserRepository;

class UserService
{
   /**
    * @var UserRepository ユーザーリポジトリ
    */
   private $userRepository;

   /**
    * UserServiceコンストラクタ
    *
    * @param UserRepository $userRepository ユーザーリポジトリ
    */
   public function __construct(UserRepository $userRepository)
   {
       $this->userRepository = $userRepository;
   }

   /**
    * 指定されたIDのユーザーを取得する
    *
    * @param int $id ユーザーID
    * @return User ユーザーモデル
    */
   public function getUser($id)
   {
       return $this->userRepository->find($id);
   }
}

// app/Providers/AppServiceProvider.php
namespace App\Providers;

use App\Repositories\UserRepository;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
   /**
    * サービスコンテナにクラスをバインドする
    *
    * @return void
    */
   public function register()
   {
       // UserRepositoryクラスをサービスコンテナにバインド
       // 依存関係解決時に、UserRepositoryの新しいインスタンスを生成
       $this->app->bind(UserRepository::class, function ($app) {
           // 実際はここでbindしなくても、app関数経由でクラスをインスタンス化する際には自動的に依存関係を解決してくれる!便利!
           return new UserRepository();
       });
   }
}

// routes/web.php
use App\Services\UserService;

/**
* ユーザー詳細ページのルート定義
*
* @param int $id ユーザーID
* @return \Illuminate\Contracts\View\View ユーザー詳細ページのビュー
*/
Route::get('/users/{id}', function ($id) {
   // サービスコンテナからUserServiceのインスタンスを取得(サービスコンテナでUserRepositoryの依存関係を自動解決してくれるので、引数なしでインスタンス化できる)
   $userService = app(UserService::class);

   // UserServiceを使用してユーザーを取得
   $user = $userService->getUser($id);

   // ユーザー情報をビューに渡して表示
   return view('user', ['user' => $user]);
});

サービスコンテナを使わない場合

  • app関数を使わず、newでインスタンス化するパターン
  • UserServiceの依存関係を手動で解決する必要あり
  • つまり、毎回UserRepositoryを渡す必要がある
  • UserServiceに引数が増えた場合、全箇所で修正が必要になる!つらい!
/**
* ユーザー詳細ページのルート定義
*
* @param int $id ユーザーID
* @return \Illuminate\Contracts\View\View ユーザー詳細ページのビュー
*/
Route::get('/users/{id}', function ($id) {
    $userRepository = new UserRepository();
    // UserRepository以外に引数が増えたら、他の全箇所修正になって死ねる
    $userService = new UserService($userRepository);
    $users = $userService->getUsers();

    return view('users.index', ['users' => $users]);
}

一体どういう仕組みなの?

  • Laravelの内部的にはPHP標準のReflectionClassを使っている
  • 依存関係を自動的に解決するためのロジックが組み込まれており、ReflectionClassを使用してクラスのコンストラクタを解析し、必要な依存関係を再帰的に解決している

RefelctionClassって何?

ここからは余談。個人的に調べたことをまとめる

  • PHP標準機能の一つ、リフレクション機能を提供するクラス
  • 実行時にクラスやオブジェクトの情報を調べることができる
  • クラスの名前空間、メソッド、属性、親クラス、インターフェイスなどの情報を取得できる

RefelctionClassっていつ使うの?

特定のクラスのprivateプロパティやメソッドにアクセスする場合

  • ReflectionClassを使用して、特定のクラスのprivateプロパティやメソッドにアクセス可
  • ただし、これはあくまで特殊なケースであり、一般的には推奨されない

動的にクラスのインスタンスを生成する場合

  • ReflectionClassを使用して、動的にクラスのインスタンスを生成できる
  • これは、クラス名が動的に決定される場合や、クラスの依存関係を動的に解決する必要がある場合に使用されることがある

クラスのメタ情報を取得する場合

  • ReflectionClassを使用して、クラスのメタ情報(クラス名、名前空間、親クラス、インターフェースなど)を取得することができる
  • クラスの構造を動的に解析したり、コード生成を行ったりする場合に使用されることがある

テストの際にモックやスタブを作成する場合

  • テストの際に、ReflectionClassを使用してモックやスタブを作成することがある
  • 特定のメソッドをオーバーライドしたり、privateメソッドをテストしたりすることができる

ReflectionClassの乱用を控えるべき理由

原則としては「あまり使わない方がいい」という立ち位置のクラス。コンテナ化だったりテストだったり、ReflectionClassでしか動的に対応できない場合にのみ使用されるべき。

大体のアプリケーションやライブラリにはReflectionClassを使わずともやりたいことができる機能が提供されてるので、まずはその方向で解決を図ろう。(Laravelのサービスコンテナがその一例)

コードの可読性の低下

  • ReflectionClassを使用すると、コードの動作が暗黙的になり、可読性が低下する
  • 通常のオブジェクト指向プログラミングの原則から逸脱し、コードの理解が難しくなる

保守性の低下

  • ReflectionClassを乱用すると、コードの保守性が低下する
  • 通常はアクセスできないはずのprivateプロパティやメソッドに直接アクセスすることで、クラスの内部実装に依存してしまい、将来的な変更が難しくなる

予期しない動作の発生

  • ReflectionClassを使用して、クラスの内部実装を変更したり、予期しない方法でメソッドを呼び出したりすると、予期しない動作が発生する
  • バグの原因になりがち、アプリケーションの安定性を損なう

テストの難しさ

  • ReflectionClassを乱用すると、テストが難しくなる
  • privateメソッドやプロパティに直接アクセスすることで、クラスの内部実装に依存したテストになってしまい、テストの保守性が低下する

オブジェクト指向の原則の違反

  • ReflectionClassを乱用すると、カプセル化や情報隠蔽などのオブジェクト指向の原則に反する
  • クラスの内部実装を直接操作することで、クラスの整合性が損なわれる

パフォーマンスへの影響

  • ReflectionClassの使用は、通常のメソッド呼び出しよりもオーバーヘッドが大きくなる
  • 頻繁にReflectionClassを使用すると、アプリケーションのパフォーマンスに影響を与える

まとめ

Laravelではサービスコンテナを使おう!DIバンザイ!

Discussion