1.

Laravel でコントローラから別コントローラを呼ぶ方法(とそれが原則アンチパターンな理由)

編集
この記事の要点
  • 大前提: コントローラから別コントローラを直接呼ぶのは 原則アンチパターン。共通処理は Service クラスに抽出するのが正解
  • どうしても必要なら: app(OtherController::class)->method($req) または app()->call([OtherController::class, "method"])
  • Service 化でコードを共有: app/Services/UserService.php を作って両方のコントローラから呼び出す
  • API を内部で叩く方法: Route::dispatch()Http::get(url("/api/...")) もあるがパフォーマンスが落ちる
  • FormRequestMiddleware を再利用すれば、コントローラを呼び合う必要はほぼ無くなる

結論: まずはアンチパターンと知ること

「コントローラ A の中でコントローラ B のメソッドを呼びたい」という発想が出てきたら、設計を見直すサインです。Laravel/Symfony 系フレームワークでは、コントローラは「HTTP リクエストを受けて、レスポンスを返す薄い入口」として設計されており、コントローラ同士が呼び合う構造はテストが困難・依存が複雑・責務が肥大化します。

とはいえ、現実には「動いているものを今すぐ直したい」場面もあるので、まず実現方法を紹介し、その後で正しいリファクタリング方法を解説します。

方法1: app()->call() でコントローラを呼ぶ

Laravel のサービスコンテナ経由で別コントローラのアクションを呼び出せます:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function store(Request $request)
    {
        // ユーザー登録処理を UserController に委譲
        $userResponse = app()->call(
            [UserController::class, 'store'],
            ['request' => $request]
        );

        // OrderController 固有の処理
        // ...
        return response()->json(['order_id' => 123]);
    }
}

app()->call() はメソッドの型ヒントを自動解決してくれるので、Request も DI されます。

方法2: コンテナから解決して直接呼ぶ

class OrderController extends Controller
{
    public function store(Request $request)
    {
        // インスタンス化(依存も自動注入)
        $userController = app(UserController::class);

        // メソッドを直接呼び出し
        $result = $userController->store($request);

        return $result;
    }
}

方法3: コンストラクタインジェクション

class OrderController extends Controller
{
    public function __construct(
        private UserController $userController
    ) {}

    public function store(Request $request)
    {
        $userResult = $this->userController->store($request);
        // ...
    }
}

なぜアンチパターンなのか

問題具体的な症状
責務の肥大化コントローラがビジネスロジックを抱える「ファットコントローラ」化
テストが困難コントローラ A をテストするのにコントローラ B もモック必要
Request/Response 依存HTTP コンテキストがないと呼べないメソッドができる
循環依存A→B→A と参照が回り、DI コンテナが破綻
ルーティングと実装が乖離URL から到達不能なメソッドが増える

正解: Service クラスに抽出する

コントローラ A と B で共通したい処理は、Service クラスとして独立させ、両方のコントローラから DI します。

Before: コントローラ呼び出し

// ❌ アンチパターン
class OrderController extends Controller
{
    public function store(Request $r)
    {
        // ユーザー作成を UserController に依存
        $user = app(UserController::class)->store($r);
        // ...
    }
}

After: Service 抽出

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

use App\Models\User;

class UserService
{
    public function create(array $data): User
    {
        return User::create([
            'name'  => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }
}

// app/Http/Controllers/UserController.php
class UserController extends Controller
{
    public function __construct(private UserService $userService) {}

    public function store(Request $request)
    {
        $user = $this->userService->create($request->validated());
        return response()->json($user);
    }
}

// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
    public function __construct(private UserService $userService) {}

    public function store(Request $request)
    {
        // ユーザー作成
        $user = $this->userService->create($request->only(['name','email','password']));
        // 注文作成
        // ...
        return response()->json(['user' => $user, 'order_id' => 123]);
    }
}

これで両方のコントローラは UserService に依存するだけになり、HTTP コンテキストに縛られず、テストも書きやすくなります。

方法4: 内部 HTTP リクエスト

マイクロサービス的に「あくまで HTTP 経由で叩きたい」場合:

use Illuminate\Support\Facades\Http;

class OrderController extends Controller
{
    public function store(Request $request)
    {
        $response = Http::post(url('/api/users'), $request->all());
        $user = $response->json();
        // ...
    }
}

注意: 内部 HTTP は シリアライズ/デシリアライズ/ネットワークスタックのオーバーヘッドが乗ります。同一プロセス内なら絶対にやめましょう。

関連: FormRequest と Middleware の活用

「コントローラ間で同じバリデーションをしたい」「同じ認可をしたい」のなら、コントローラを呼び合わなくても解決できます:

  • FormRequest: バリデーションルールを再利用可能なクラスに
  • Middleware: 認証/認可/ログ等の横断処理
  • Policy: モデル単位の認可ロジック
  • Event/Listener: 副作用処理(メール送信等)の分離

FAQ

Q: 既存のレガシーコードで Service 化が大変
A: まずは app(OtherController::class)->method() でつないでおき、テストを書きながら徐々に Service へ抽出するのが現実解。

Q: Resource Controller の index 等を内部で呼びたい
A: API 内部呼び出しは Service 経由でデータ取得し、別途 JsonResource で整形するのが推奨。

Q: Symfony / Rails でも同じ?
A: はい。Rails では Service Object パターン、Symfony では Service コンテナ + Application Service が同等の考え方です。

編集
Post Share
子ページ

子ページはありません

同階層のページ

同階層のページはありません