TIL2025-01-19-Serviceクラス

Laravel のコントローラーから別のコントローラーの呼び出しはOK?

  • Laravelでは、1つのコントローラーから別のコントローラーのメソッドを直接呼び出すことは可能ですが、一般的には推奨されません
  • 理由は、Laravel の設計上、コントローラはリクエストとレスポンスの橋渡し役という位置づけだからです。

    コントローラ内にロジックをすべて詰め込むのではなく、あくまでリクエストを適切なサービスやモデルに振り分ける役割を果たします。

  • Laravel では、サービスクラス(もしくはサービス層)を使うことで、コントローラにロジックを書きすぎないようにします。
  • ⇒ つまり、リクエストされたURLのコントローラーが処理を担当します。
  • ⇒ もし、コントローラーに書くロジックが多ければ、サービスクラスを使用します。

 

Laravel のコントローラーにprivate メソッドを書いてもよい?

  • コントローラーに多くのprivate メソッド を記述することは、あまりおすすめされません

1. コントローラーが肥大化する

  • コントローラーはリクエストとレスポンスの橋渡し役が主な責務です。しかし、多くの private メソッドを追加すると、コントローラーが肥大化し、ビジネスロジックと混ざってしまいます。
  • 結果として、コードの可読性や保守性が低下します。

2. 関心の分離の原則に反する

  • Laravel では、MVC アーキテクチャの設計に従い、役割を分離することを重視しています。コントローラーに多くの private メソッドを書くと、ビューやモデルが担当すべきロジックまで抱え込むことがあります。

3. 再利用性が低い

  • コントローラーの private メソッドは、そのコントローラー内でしか使用できません。一方、サービスクラスやヘルパー関数にロジックを切り出すと、複数のコントローラーや他のコンポーネントでも再利用可能になります。

まとめ

  • 簡単な補助処理は許容されますが、ビジネスロジックはサービスクラスやヘルパー関数に切り出すのがベストです。
  • 指針:
    • コントローラーは「何をするか」を記述する場所。
    • 「どうするか」はサービスクラスやモデルに任せましょう。

 

ビジネスロジックはサービスクラスにすべて書いてよいの?

  • 良いアプローチですが、適切な業務分担を考慮するべきです。Active Record パターンを採用する Laravel では、モデルもビジネスロジックを持つことが期待されています。
  • サービスクラスは「複数のモデルを操作する」「アプリケーション固有の業務プロセスを処理する」など、複雑なビジネスロジックを記述する場所。
  • モデルは「単純なデータ操作」や「データベーステーブルに密接に関連するロジック」を記述する場所。
  • 判断基準:
    • 「複数のステップがあるか?」 → サービスクラスに。
    • 「単純な状態操作やクエリか?」 → モデルに。

 

モデルに書くロジックの内容は?

  • データベースとのやり取りに関する責務を持たせます。
  • 適切なロジックの例:
    • スコープ ⇒ where文によるレコード取得
    • アクセサ・ミューテータ
    • リレーションの定義
    • 属性や簡単な計算

スコープメソッド

  • ロジックを完結にして、再利用可能にします。
  • scope の後の部分をそのままクエリビルダーのメソッドとして利用できます。たとえば、scopeCompletedcompleted という名前で呼び出せます。
namespace AppModels;

use IlluminateDatabaseEloquentModel;

class Task extends Model
{
    protected $fillable = ['title', 'description', 'status'];

    // スコープ
    public function scopeCompleted($query)
    {
        return $query->where('status', 'completed');
    }

    // アクセサ
    public function getStatusLabelAttribute()
    {
        return $this->status === 'completed' ? '完了' : '未完了';
    }
}

 

// 完了済みのタスクをすべて取得
$completedTasks = Task::completed()->get();

// 以下は同じ処理ですが、scopeメソッドを使用することで簡単に記載できます。
$completedTasks = Task::where('status', 'completed')->get();

 

スコープメソッドの便利な使用方法

1. 単独で使用する例

use AppModelsTask;
// 完了済みのタスクをすべて取得
$completedTasks = Task::completed()->get();

 

このコードは以下と同じ処理を行いますが、scopeCompleted を使うことでコードを簡潔にできます:

$completedTasks = Task::where('status', 'completed')->get()

 


2. 他のクエリ条件と組み合わせる例

スコープはクエリビルダーでチェーン可能です。他の条件と組み合わせることで、柔軟なクエリを構築できます。

// 完了済みかつ作成者が特定のユーザーのタスクを取得
$completedTasksByUser = Task::completed()
    ->where('user_id', 1)
    ->get();

 

このコードは以下と同じ意味を持ちますが、スコープを使うことで可読性が向上します:

$completedTasksByUser = Task::where('status', 'completed')
    ->where('user_id', 1)
    ->get();

 


3. ペジネーションと組み合わせる例

// 完了済みのタスクを1ページ10件ずつ取得
$completedTasks = Task::completed()->paginate(10);

 

get() の代わりに paginate() を使用することで、スコープを使ったクエリにペジネーションを追加できます。


4. 集計関数と組み合わせる例

スコープは count()sum() などの集計メソッドとも組み合わせて利用できます。

// 完了済みタスクの数を取得
$completedCount = Task::completed()->count();

 


5. リレーションと組み合わせる例

スコープはリレーション内でも利用できます。たとえば、ユーザーが持つ完了済みタスクを取得する場合:

use AppModelsUser;
// ユーザー1の完了済みタスクを取得
$user = User::find(1);
$completedTasks = $user->tasks()->completed()->get();

 

このコードは、以下のようなリレーションを利用して動作します:

// User モデル
public function tasks()
{
    return $this->hasMany(Task::class);
}

 

アクセサ

  • データを加工して返します。
  • getXxxAttribute というメソッド名で定義します。Xxx の部分は、アクセサを適用したいカラム名(スネークケース)をパスカルケースに変換したものです。
$task = Task::find(1); // IDが1のタスクを取得
echo $task->status_label; // 例: "完了"

 

 

サービスクラスのインスタンスが生成されるタイミングはいつ?

  • コントローラが呼び出され、Laravel がコントローラの依存関係を解決する際に生成されます。
  • 具体的には、RouteController が実行されるときに、コントローラのコンストラクタが呼び出されます。このとき、コンストラクタで必要とされているクラス(TaskService)が サービスコンテナによって初めて生成 されます。
  • サービスコンテナの記述によって、以下のパターンに別れます。
  • サービスコンテナのディレクトリ → app/Providers/AppServiceProvider.php

1. シングルトンの場合

もし AppServiceProvider で以下のように singleton として登録している場合:


$this->app->singleton(TaskService::class, function ($app) {
    return new TaskService();
});

 

  • TaskService のインスタンスは 最初に必要になったときに1回だけ生成 され、以降は同じインスタンスが使い回されます。

2. 都度生成(トランジェント)の場合

もし bind で登録している場合:


$this->app->bind(TaskService::class, function ($app) {
    return new TaskService();
});

 

  • TaskService のインスタンスは コントローラが呼び出されるたびに新しいインスタンスが生成 されます。

3. 自動解決の場合

もしサービスプロバイダで特に登録していない場合:

  • Laravel は new TaskService() を実行して、依存関係を自動的に解決します。この挙動は bind と同様に、 毎回新しいインスタンスが生成 されます。
具体的な流れをコードベースで解説

1. ルートでコントローラを呼び出す


// web.php
use AppHttpControllersTaskController;
Route::get('/tasks', [TaskController::class, 'index']);

 

  • ここで /tasks にリクエストが来ると、Laravel は TaskControllerindex メソッドを呼び出そうとします。
  • その際、コントローラのインスタンスを生成 する必要があるため、Laravel はサービスコンテナに処理を委ねます。

2. サービスコンテナがコントローラの依存関係を解決する


class TaskController extends Controller
{
    protected $taskService;
    public function __construct(TaskService $taskService)
    {
        $this->taskService = $taskService;
    }
    public function index()
    {
        return $this->taskService->getAllTasks();
    }
}

 

  • Laravel は TaskController のコンストラクタを確認し、TaskService 型の依存関係が必要であることを把握します。
  • サービスコンテナは、以下のどちらかで TaskService のインスタンスを生成します:
    • クラスの自動解決: サービスコンテナが自動的に TaskServicenew TaskService() で解決します。
    • カスタム登録: サービスプロバイダで登録されている TaskService の解決方法(singleton など)を実行します。

3. 必要であれば依存関係の依存も解決する

もし TaskService のコンストラクタがさらに依存関係を持っていた場合、サービスコンテナはその依存も再帰的に解決します。

例:


class TaskService
{
    protected $taskRepository;
    public function __construct(TaskRepository $taskRepository)
    {
        $this->taskRepository = $taskRepository;
    }
}

 

  • サービスコンテナは TaskRepository のインスタンスも生成し、それを使って TaskService を初期化します。

 

 

番外編:プログラムのすきなところ

プログラムは無形商品ですが、巨大な建物、構造物だと思っています。建物には多くの人が出入りしてお仕事や買い物をしたり、娯楽を楽しんでいます。そこはとても快適に過ごせる環境になっています。

すべての人が心地良く過ごすためには多くの要素が必要です。建物が頑丈で安全なこと、空調や照度を適切に調整していること、商品の仕入れ導線があること、停止しないで点検できる仕組みなど…プログラムという構造物の中を歩くと、設計の美しさに息を飲みます。

真のプログラマーこそ、先回りして便利にしておくので、後工程はお客様だと意識しているのだと思います。

そうなりたいですし、開発中に天才の思想に触れたとき、プログラムは面白いと思います。

問いと知識への渇きが続くのは、プログラムのすきなところです。