※本記事は「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の思想と書き心地の違いを比較していきます。
コメント