RailsエンジニアのためのLaravel Service Container入門

技術

※本記事は「RailsからLaravelを眺める」シリーズの第1回です。Rails出身の私がLaravelを触りながら、Railsと比較して違いを整理していく連載になります。


はじめに

RailsエンジニアがLaravelを触ると、最初に「なんだこれ?」となるのが Service Container です。
Railsにはこの仕組みが存在しませんが、Laravelでは依存解決(Dependency Injection, DI)の中核としてあらゆる場所で登場します。

この記事ではRailsエンジニア視点から「Service Containerとは何か?」を整理し、Railsでの感覚と比較しながら解説していきます。


Railsにおける依存の扱い

Railsでは「規約と自動読み込み(autoloading)」によって、依存を意識する場面は少ないです。

class ReportsController < ApplicationController
  def index
    service = ReportService.new(ApiClient.new)
    @reports = service.fetch_reports
  end
end

ここでは単純に new しています。
依存が増えた場合も initializer にまとめる程度で、フレームワークとして「依存解決」を抽象化する仕組みはほぼ存在しません。

Railsは「魔法のように動く」便利さがある一方で、依存関係が暗黙的になりやすいという特徴があります。


LaravelのService Containerとは?

Laravelは依存解決を Service Container に集約しています。

サービスの登録

use App\Services\ReportService;
use App\Services\ApiClient;

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

サービスの利用

// 手動で解決
$service = app()->make(ReportService::class);

// コントローラでの自動解決
class ReportController extends Controller
{
    public function index(ReportService $service)
    {
        $reports = $service->fetchReports();
        return view('reports.index', compact('reports'));
    }
}

コンストラクタに型ヒントを書く だけで、依存が自動的に注入されるのがLaravel流。
Railsにはない「依存を明示化する文化」が根付いています。


実務シナリオ:APIクライアントをどう扱うか

Railsの場合

外部API(在庫管理など)を叩くクライアントを実装するケース。

# app/services/inventory_client.rb

class InventoryClient
  def initialize(api_key: ENV["INVENTORY_API_KEY"])
    @conn = Faraday.new(
      url: "https://inventory.example.com",
      headers: { "Authorization" => "Bearer #{api_key}" }
    )
  end

  def fetch_items
    @conn.get("/items").body
  end
end

class ItemsController < ApplicationController
  def index
    client = InventoryClient.new
    @items = client.fetch_items
  end
end

Railsでは「その場で new」が普通で、依存注入を意識する文化はあまり強くありません。


Laravelの場合

同じクライアントをLaravelで書くとこうなります。

// app/Services/InventoryClient.php

class InventoryClient
{
    protected $client;

    public function __construct(string $apiKey)
    {
        $this->client = new \GuzzleHttp\Client([
            'base_uri' => 'https://inventory.example.com',
            'headers' => ['Authorization' => "Bearer {$apiKey}"],
        ]);
    }

    public function fetchItems()
    {
        $response = $this->client->get('/items');
        return json_decode($response->getBody(), true);
    }
}
// app/Providers/AppServiceProvider.php

public function register(): void
{
    $this->app->singleton(InventoryClient::class, function ($app) {
        $apiKey = config('services.inventory.api_key');
        return new InventoryClient($apiKey);
    });
}
// controller

class ItemController extends Controller
{
    public function index(InventoryClient $client)
    {
        $items = $client->fetchItems();
        return view('items.index', compact('items'));
    }
}

ここでは Service Containerに登録しておくことで、コントローラに自動的に注入 されます。
依存の生成場所が一元管理されるので、大規模化や環境ごとの差し替えに強い構造になります。


テストの違い

Rails

RSpecで依存をスタブすることが多いです。

allow(InventoryClient).to receive(:new).and_return(double(fetch_items: []))

Laravel

Service Containerにテスト用実装をバインドすればOK。

$this->app->bind(InventoryClient::class, function () {
    return new class {
        public function fetchItems() {
            return [];
        }
    };
});

依存を差し替える仕組みがフレームワークに組み込まれているため、テストが自然に書きやすい のが強みです。


思想の違いまとめ

  • Rails
    • 規約と魔法で依存を意識しなくても動く
    • new の乱立や初期化が暗黙的になりがち
  • Laravel
    • 依存はすべてService Containerに集約
    • 型ヒントだけで依存解決でき、テスト・差し替えも容易

Railsは「速く作る」、Laravelは「依存を透明化してコントロールする」という違いが見えてきます。


まとめ

  • Rails → 依存は暗黙的、規約に任せて進められる
  • Laravel → 依存は明示的、Service Containerで管理して拡張性・テスト容易性を確保

特にAPIクライアントのような「環境依存・認証が絡む処理」では、LaravelのService Container設計が威力を発揮します。
Railsエンジニアは「Laravelは依存解決を表に出している」と理解すると、腑に落ちやすいでしょう。


次回は 「Eloquent vs ActiveRecord」 をテーマに、ORMの思想と書き心地の違いを比較していきます。

コメント

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