PHP,Laravelのクラス間の依存関係について(DI,インスタンス化)

## はじめに

私は今までSpringBootメインで開発してきたのでクラス間の依存性の注入(DI)にあまり悩んできませんでした。しかし現在の職場ではPHP,Laravelをメインに使用していて、DIの使用がspringほど扱いやすくなかったので、状況に応じたDIやインスタンス化の使い方を考えました。
この記事では、**新規開発**と**既存システム**でのDI戦略の違い、そしてEloquentモデルでDIが難しい理由と対処法について、実践的なコード例とともに解説します。

## 前提：なぜDIが重要なのか

```php
// ❌ DIを使わない場合
class OrderService
{
    public function processOrder(Order $order)
    {
        $paymentService = new PaymentService(); // 強い結合
        $emailService = new EmailService();     // テストが困難
        
        $paymentService->charge($order);
        $emailService->sendConfirmation($order);
    }
}

// ✅ DIを使う場合
class OrderService
{
    public function __construct(
        private PaymentService $paymentService,
        private EmailService $emailService
    ) {}
    
    public function processOrder(Order $order)
    {
        $this->paymentService->charge($order);
        $this->emailService->sendConfirmation($order);
    }
}
```

DIを使うことで、**テスタビリティ**、**保守性**、**柔軟性**が向上します。

## 新規開発：コンストラクタインジェクションを採用

### 基本パターン

```php
<?php

namespace App\Services;

use App\Repositories\UserRepository;
use App\Services\NotificationService;

class UserService
{
    public function __construct(
        private UserRepository $userRepository,
        private NotificationService $notificationService
    ) {}
    
    public function createUser(array $userData): User
    {
        $user = $this->userRepository->create($userData);
        $this->notificationService->sendWelcomeEmail($user);
        
        return $user;
    }
}
```

## 既存システム：後方互換性を保つパターン

既存システムに急にDIを導入すると、`new ClassName()`で呼び出している箇所すべてでエラーが発生します。

### 解決策：デフォルト引数パターン

```php
<?php

namespace App\Services;

class LegacyOrderService
{
    private PaymentService $paymentService;
    private EmailService $emailService;
    
    public function __construct(
        PaymentService $paymentService = null,
        EmailService $emailService = null
    ) {
        // 既存コードからは引数なしでnewされても動く
        $this->paymentService = $paymentService ?: app(PaymentService::class);
        $this->emailService = $emailService ?: app(EmailService::class);
    }
    
    public function processOrder(Order $order)
    {
        $this->paymentService->charge($order);
        $this->emailService->sendConfirmation($order);
    }
}

// 既存コード（そのまま動く）
$service = new LegacyOrderService();
$service->processOrder($order);

// 新しいコード（DIも使える）
$service = app(LegacyOrderService::class);
$service->processOrder($order);

// テスト（モックを注入可能）
$mockPayment = Mockery::mock(PaymentService::class);
$mockEmail = Mockery::mock(EmailService::class);
$service = new LegacyOrderService($mockPayment, $mockEmail);
```

### Null合体演算子でよりエレガントに

```php
public function __construct(
    PaymentService $paymentService = null,
    EmailService $emailService = null
) {
    $this->paymentService = $paymentService ?? app(PaymentService::class);
    $this->emailService = $emailService ?? app(EmailService::class);
}
```

### 段階的移行戦略

1. **Phase 1**: デフォルト引数パターンで既存コードの動作を保証
2. **Phase 2**: 新しく書くコードではDIコンテナから取得
3. **Phase 3**: 既存の`new ClassName()`を`app(ClassName::class)`に置換
4. **Phase 4**: デフォルト引数を削除してフルDIに移行

## Eloquentモデルでのジレンマと解決策

### なぜEloquentでDIが困難なのか

```php
// Laravelが内部的に行うモデルのインスタンス化
$users = User::where('active', true)->get(); // 引数なしのコンストラクタが呼ばれる
$user = User::find(1);                       // Laravelが勝手にnewする
```

EloquentはActive Recordパターンを採用しており、**フレームワークが自動的にインスタンス化**するため、コンストラクタでDIを使うと問題が発生します。

### ❌ Eloquentでやってはいけない例

```php
<?php

namespace App\Models;

use App\Services\UserService;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    // これはNG！
    public function __construct(UserService $userService)
    {
        parent::__construct();
        $this->userService = $userService;
    }
    
    // User::find(1) でエラーになる
}
```

### ✅ 解決策1: Lazy Loading パターン

```php
<?php

namespace App\Models;

use App\Services\UserService;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    private ?UserService $userService = null;
    
    private function getUserService(): UserService
    {
        // 必要な時だけインスタンス化（Lazy Loading）
        return $this->userService ??= app(UserService::class);
    }
    
    public function sendNotification(): void
    {
        $this->getUserService()->sendNotification($this);
    }
    
    public function updateProfile(array $data): void
    {
        $this->getUserService()->updateProfile($this, $data);
    }
}
```

### ✅ 解決策2: サービスクラスに責務を移譲

```php
<?php

namespace App\Services;

class UserService
{
    public function __construct(
        private NotificationService $notificationService,
        private ProfileService $profileService
    ) {}
    
    public function sendNotification(User $user): void
    {
        $this->notificationService->send($user, 'welcome');
    }
    
    public function updateProfile(User $user, array $data): void
    {
        $this->profileService->update($user, $data);
    }
}

// 使用例
class UserController extends Controller
{
    public function __construct(private UserService $userService) {}
    
    public function update(Request $request, User $user)
    {
        $this->userService->updateProfile($user, $request->validated());
        return response()->json(['status' => 'success']);
    }
}
```

### ✅ 解決策3: ファサードパターン

```php
<?php

namespace App\Models;

use App\Facades\UserServiceFacade;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function sendNotification(): void
    {
        UserServiceFacade::sendNotification($this);
    }
    
    public function updateProfile(array $data): void
    {
        UserServiceFacade::updateProfile($this, $data);
    }
}

// ファサード定義
class UserServiceFacade extends Facade
{
    protected static function getFacadeAccessor()
    {
        return UserService::class;
    }
}
```

## パフォーマンスの考慮事項

### Lazy Loadingの効果

```php
class User extends Model
{
    private ?ExpensiveService $expensiveService = null;
    
    private function getExpensiveService(): ExpensiveService
    {
        // 実際に使われるまでインスタンス化されない
        return $this->expensiveService ??= app(ExpensiveService::class);
    }
    
    public function someMethod(): void
    {
        // この時点で初めてExpensiveServiceが作られる
        $this->getExpensiveService()->doSomething();
    }
}

// 1000件のUserを取得しても、someMethod()を呼ばなければ
// ExpensiveServiceは1つも作られない
$users = User::all(); // ExpensiveServiceは作られない

foreach ($users as $user) {
    if ($user->needs_processing) {
        $user->someMethod(); // この時だけExpensiveServiceが作られる
    }
}
```

## まとめ

### 使い分けの指針

| 状況 | 推奨アプローチ | 理由 |
|------|---------------|------|
| **新規サービスクラス** | コンストラクタインジェクション | 明確で安全 |
| **既存サービスクラス** | デフォルト引数パターン | 後方互換性 |
| **Eloquentモデル** | Lazy Loading または サービス移譲 | フレームワーク制約 |


### ベストプラクティス

1. **新規開発では積極的にDIを使う**
2. **既存システムでは段階的に導入する**
3. **Eloquentモデルには複雑な責務を持たせない**
4. **テスタビリティを常に意識する**
5. **パフォーマンスが重要な箇所ではLazy Loadingを活用する**

LaravelでのDIは、適切に使い分けることで開発効率とコード品質を大幅に向上させることができます。既存システムでも諦めずに、段階的にDIを導入していきましょう。

---

## 参考リンク

- [Laravel Service Container](https://laravel.com/docs/container)
- [Laravel Dependency Injection](https://laravel.com/docs/providers)
- [PHP The Right Way - Dependency Injection](https://phptherightway.com/#dependency_injection)