Laravel サービスコンテナ（インターフェイスと実装の結合）方法

この投稿は、Laravel公式のサービスコンテナに関する内容です
https://readouble.com/laravel/8.x/ja/container.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/96b4b1cd-6243-ca4b-9995-338811de1cd1.png)

事の発端はService層やRepository層を設けて開発する場合、対象クラスのDI定義を都度定義するのは、設定漏らしやすいので『絶対にやりたくない！』です。

```php
Providers/AppServiceProvider.php
 
  public function register()
  {
     $this->app->bind(Interface001::class, Imple001::class);
     $this->app->bind(Interface002::class, Imple002::class);
     $this->app->bind(Interface003::class, Imple003::class);
     $this->app->bind(Interface004::class, Imple004::class);
     ・・・
     気が狂いそう
  }
```

どうにかしてインターフェイスと実装クラスの紐づけを自動化できないか考えて、とりあえず解決に至ったので残しておこう。

今回の解決は特定のディレクトリ構成に基づいてサービスコンテナに登録仕組みとしています。

> ディレクトリ構成

**サービスコンテナに登録するインターフェイス、クラスの作成ルール**

- ディレクトリ直下にインターフェイスを定義
  必ず`I`から始める。(IXxxx.php)
- Implディレクトリに実装クラスを定義
  必ずインターフェイスの先頭1文字を除外したクラス名にする。(Xxxx.php)

という構成にします。

```php
ディレクトリ例 (Services, Repositories下が管理対象)

app
┣ Services
┃ ┣ IXxxService.php
┃ ┗ Impl
┃  ┗ XxxService.php
┣ Repositories
┃ ┣ IXxxRepository.php
┃ ┗ Impl
┗  ┗ XxxRepository.php
```

### AppServiceProvider.php の実装

```php
<?php

namespace App\Providers;

use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

use Illuminate\Support\Facades\File;
use Symfony\Component\Finder\Finder;

/**
 * Class AppServiceProvider
 */
class AppServiceProvider extends ServiceProvider
{
    /**
     * {@inheritdoc}
     */
    public function register()
    {
        $dirs = [
            'Services',
            'Repositories'
        ];
        foreach ($dirs as $dir) {
            $this->bind($dir);
        }
    }
    /**
     * bindを行う
     *
     * @param string $dir 対象のディレクトリ
     * @return void
     */
    private function bind(string $dir)
    {
        $finder = new Finder();
        $iterator = $finder
            ->in(app_path().'/'.$dir)
            ->depth('< 2') // Implディレクトリが含まれないように
            ->name('I*.php')
            ->files();
        foreach ($iterator as $fileinfo) {
            $filepath = $fileinfo->getPathname();
            list($interface, $impl) = $this->getBinds($filepath);
            if (is_null($impl)) {
                // インターフェイスのみ定義している場合、bindしない
                continue;
            }

            $this->app->bind($interface, $impl);
        }
    }
    /**
     * バインドするインターフェイスと実装クラスのクラスパスを取得する
     *
     * @param string $filepath
     * @return array interface, impl
     */
    private function getBinds(string $filepath): array
    {
        $fileInfo = pathinfo($filepath);

        $interfaceDir      = $fileInfo['dirname'];
        $interfaceFileName = $fileInfo['filename'];
        $interfacePath     = "{$interfaceDir}/{$interfaceFileName}";
        $interfaceClass    = Str::replace(app_path(), 'App', $interfacePath);
        $interfaceClass    = Str::replace('/', '\\', $interfaceClass);

        $implDir      = "{$interfaceDir}/Impl";
        $implFileName = substr($interfaceFileName, 1);
        $implFilePath = "{$implDir}/{$implFileName}";
        $implFIlePathWithExtention = "{$implFilePath}.{$fileInfo['extension']}";
        $implClass    = Str::replace(app_path(), 'App', $implFilePath);
        $implClass    = Str::replace('/', '\\', $implClass);

        $implClass = File::exists($implFIlePathWithExtention) ? $implClass : null;

        return [$interfaceClass, $implClass];
    }
    ・・・
}
```

#### 動作確認

tinkerでコンテナから取得できるか確認。OK！

```php
php artisan tinker
>>> app()->make(App\Services\Auth\ILoginService::class);
=> App\Services\Auth\Impl\LoginService {#4733}
```

Controllerクラスで使用する場合

```php
use App\Services\Auth\ILoginService;

class XxxController extends Controller {
  public function __constructor(ILoginService $loginService)
  {
    $this->loginService = $loginService;
  }
  public function login(Request $request)
  {
    // こんな感じで使える。
    $this->loginService->login($request->all());
  }
}
```

問題が出るまでこれでやってみる。