Laravelの構成概念 第3回 サービスプロバイダ編

## はじめに

前回のサービスコンテナ編の続きとして、最後にサービスプロバイダのご紹介をします。

## シリーズ

- [Laravelの構成概念 第1回 ライフサイクル編](https://qiita.com/ucan-lab/items/5e0a4584c5321a502da7)
- [Laravelの構成概念 第2回 サービスコンテナ編](https://qiita.com/ucan-lab/items/6f354cf6ae5e807dbada)
- **Laravelの構成概念 第3回 サービスプロバイダ編**

## 環境

- PHP 8.0.3
- Laravel 8.38.0

## 参考

- [Laravel 8.x ライフサイクル](https://readouble.com/laravel/8.x/ja/lifecycle.html)
- [Laravel 8.x サービスコンテナ](https://readouble.com/laravel/8.x/ja/container.html)
- [Laravel 8.x サービスプロバイダ](https://readouble.com/laravel/8.x/ja/providers.html)
- [Laravel 8.x ファサード](https://readouble.com/laravel/8.x/ja/facades.html)
  - (今回はあまり触れません)
- [Laravel API 8.x](https://laravel.com/api/8.x)
- [Laravel フレームワーク](https://github.com/laravel/laravel)
- [Laravel コアフレームワーク](https://github.com/laravel/framework)

Laravelの構成概念については公式ドキュメントにより詳しい内容がまとまっているので詳細を知りたい方は上記の公式ドキュメントをご覧ください。

## 用語

### Laravel サービスコンテナ

クラスの依存関係を管理し、依存注入(DI)を実行するための機能です。

- サービスコンテナにクラスを登録する(結合)
- サービスコンテナに登録されたクラスのインスタンスを取り出す(依存解決)

サービスコンテナはサービス(クラス)を入れておく箱のイメージです。

### Laravel サービスプロバイダ

Laravelアプリケーション全体の起動処理における初期起動処理を行っています。
サービスプロバイダからサービスコンテナへDIの設定(コンテナ結合)を行います。

サービスプロバイダーでは、コンテナ結合以外にもイベントリスナ、フィルター、ルーティングの設定なども行います。
(今回は触れません)

---

## Laravel 標準のサービスプロバイダ

`config/app.php` の `providers` に登録されているサービスプロバイダーはLaravelアプリケーションにリクエストの際に自動的に読み込まれます。

新しく自分で作ったサービスプロバイダはこのリストに追加していきます。

```php:config/app.php
    /*
    |--------------------------------------------------------------------------
    | Autoloaded Service Providers
    |--------------------------------------------------------------------------
    |
    | The service providers listed here will be automatically loaded on the
    | request to your application. Feel free to add your own services to
    | this array to grant expanded functionality to your applications.
    |
    */

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

    ],
```

## 基本のサービスプロバイダ

サービスプロバイダの雛形を作成するコマンドが用意されています。

```shell-session
$ php artisan make:provider ClockServiceProvider
```

追加したプロバイダーは `config/app.php` へ追記します。

```php:config/app.php
    'providers' => [
        // ...

        App\Providers\ClockServiceProvider::class,
    ],
```

```php:app/Providers/ClockServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Service\Clock;
use App\Service\ClockInterface;
use Illuminate\Support\ServiceProvider;

final class ClockServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(ClockInterface::class, Clock::class);
    }
}
```

シングルトンの結合を使って、ClockInterfaceにClockクラスをバインドします。
シングルトンの結合は1回のみ依存解決されます。

今回はClock用のサービスプロバイダーを作りましたが、コンテナ結合したいだけなら全部 `AppServiceProvider` にまとめてしまって良いと思います。

また、別のライブラリの設定用途等の場合はサービスプロバイダは分けた方が良いです。

### サンプル: Clock関連のファイル

動作確認のため、サンプルファイルを用意しました。

```php:app/Service/ClockInterface.php
<?php declare(strict_types=1);

namespace App\Service;

use DateTimeImmutable;

interface ClockInterface
{
    /**
     * @return DateTimeImmutable
     */
    public function now(): DateTimeImmutable;
}
```

[PSR-20 Clock](https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md) を参考に `ClockInterface` を定義します。システムの現在時刻を定義するインターフェースです。

※ [DateTimeImmutable](https://www.php.net/manual/ja/class.datetimeimmutable.php) はクラスの挙動は [DateTime](https://www.php.net/manual/ja/class.datetime.php) とほぼ同じ振る舞いをします。ただし、自分自身は変更せずに新しいオブジェクトを返すという点だけが異なります。

```php:app/Service/Clock.php
<?php declare(strict_types=1);

namespace App\Service;

use DateTimeImmutable;

final class Clock implements ClockInterface
{
    /**
     * @return DateTimeImmutable
     */
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable();
    }
}
```

`ClockInterface` を実装した `Clock` クラスです。

```php:app/Http/Controllers/ClockController.php
<?php declare(strict_types=1);

namespace App\Http\Controllers;

use App\Service\ClockInterface;
use Illuminate\Http\Request;

final class ClockController extends Controller
{
    /**
     * @param ClockInterface $clock
     */
    public function __construct(private ClockInterface $clock)
    {
    }

    /**
     * @return string
     */
    public function __invoke(): string
    {
        return $this->clock->now()->format('Y-m-d H:i:s');
    }
}
```

コントローラでコンストラクタインジェクション(依存性の注入)します。
具象(Clock)ではなく抽象(ClockInterface)に依存してることに注目してください。

実行時にはサービスプロバイダでバインドした `Clock` のインスタンスが `$clock` 入ります。
補足ですが、このコンストラクタの書き方はPHP8の新しい書き方です。([PHP 8: Constructor property promotion](https://stitcher.io/blog/constructor-promotion-in-php-8))

```php:routes/api.php
<?php declare(strict_types=1);

use App\Http\Controllers\ClockController;
use Illuminate\Support\Facades\Route;

Route::get('now', ClockController::class);
```

コントローラを作ったので、それに対応するルーティングを定義します。

```shell
$ curl http://localhost/api/now
2021-04-27 09:12:49
```

APIを叩いて現在の日付が返ってきたのでokです。
テストコードの例も書いてみます。

```php:tests/Feature/ClockControllerTest.php
<?php declare(strict_types=1);

namespace Tests\Feature;

use App\Service\ClockInterface;
use DateTimeImmutable;
use Tests\TestCase;

final class ClockControllerTest extends TestCase
{
    public function testInvoke(): void
    {
        $this->mock(ClockInterface::class, function ($mock): void {
            $mock->shouldReceive('now')
                ->andReturn(new DateTimeImmutable('2021-01-01 23:59:59'))
                ->once();
        });

        $response = $this->get('/api/now');
        $response->assertStatus(200);
        $response->assertSee('2021-01-01 23:59:59');
    }
}
```

コントローラで呼び出されている `$this->clock->now()` の部分ですが、現在時刻を取得するので当たり前ですが実行するたびに値が変わってしまいます。

動的な値をテストするのはとても大変なので固定の日付を返すようにモックと差し替えてます。
LaravelではMockeryが組み込まれており、インスタンスの差し替えが簡単に行えます。(プロダクションコードを書き換えることなくできる)

```shell
$ php artisan test

   PASS  Tests\Feature\ClockControllerTest
  ✓ invoke

  Tests:  1 passed
  Time:   0.75s
```

テストが成功しているのでokです！

## 補足: 基本のサービスプロバイダ(バインド)

```php:app/Providers/ClockServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Service\Clock;
use App\Service\ClockInterface;
use Illuminate\Support\ServiceProvider;

final class ClockServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(ClockInterface::class, Clock::class);
    }
}
```

シンプルな結合を使ってもokですが、この場合は呼び出すたびにClockインスタンスが生成されます。

```php:app/Providers/ClockServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Service\Clock;
use App\Service\ClockInterface;
use Illuminate\Support\ServiceProvider;

final class ClockServiceProvider extends ServiceProvider
{
    // シングルトンの結合
    public array $singletons = [
        ClockInterface::class => Clock::class,
    ];

    // シンプルな結合
    public array $bindings = [
        ClockInterface::class => Clock::class,
    ];
}
```

別の書き方として、 `$singletons` や `$bindings` プロパティに連想配列を定義するだけで行えます。

## 補足: singleton と bind の違い

```shell
$ php artisan tinker

>>> app(App\Service\ClockInterface::class);
=> App\Service\Clock {#3345}
```

`app(App\Service\ClockInterface::class);` を実行すると依存解決されて、`App\Service\Clock` のインスタンスが取り出されます。

[`spl_object_id`](https://www.php.net/manual/ja/function.spl-object-id.php) メソッドでインスタンスIDを取得できるので、`singleton`と`bind`の違いを確認してみます。

### singleton

```shell
$ php artisan tinker

>>> spl_object_id(app(App\Service\ClockInterface::class));
=> 3345
>>> spl_object_id(app(App\Service\ClockInterface::class));
=> 3345
>>> spl_object_id(app(App\Service\ClockInterface::class));
=> 3345
```

singletonは同じインスタンスが取り出される。

### bind

```shell
$ php artisan tinker

>>> spl_object_id(app(App\Service\ClockInterface::class));
=> 3345
>>> spl_object_id(app(App\Service\ClockInterface::class));
=> 3338
>>> spl_object_id(app(App\Service\ClockInterface::class));
=> 3345
```

bindは毎回異なるインスタンスが取り出される。

## 補足: 遅延プロバイダ

サービスプロバイダーが、コンテナ結合の登録だけであれば遅延プロバイダを利用できます。

遅延プロバイダをにすることでその結合が実際に必要になるまでコンテナへの登録を遅らせることができます。

リクエストがあるたびにファイルシステムからロードされなくなるので、アプリケーションのパフォーマンス向上が見込めます。

遅延プロバイダにするには `DeferrableProvider` を実装します。

```php:app/Providers/ClockServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Service\Clock;
use App\Service\ClockInterface;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

final class ClockServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public array $singletons = [
        ClockInterface::class => Clock::class,
    ];

    /**
     * @return string[]
     */
    public function provides(): array
    {
        return array_keys($this->singletons);
    }
}
```

## さいごに

サービスプロバイダについては基本の方法だけご紹介しました。
他にもいろんな初期設定に使われますが、必要になってからで良いと思います。

第1回 ライフサイクル編の投稿してから2ヶ月経ちましたがようやく書き上げることができました👏
なかなか続きものの記事って書くの飽きてモチベーションなくなるんですよね...
(回を重ねるごとにLGTM数が減りますからね...😇)

## 参考

- [Laravel 8.x サービスプロバイダ](https://readouble.com/laravel/8.x/ja/providers.html)
- [Laravel8.x モック](https://readouble.com/laravel/8.x/ja/mocking.html)
- [Mockery1.0 Mockery](https://readouble.com/mockery/1.0/ja/index.html)
- https://github.com/mockery/mockery