60
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-04-26

はじめに

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

シリーズ

環境

  • PHP 8.0.3
  • Laravel 8.38.0

参考

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

用語

Laravel サービスコンテナ

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

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

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

Laravel サービスプロバイダ

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

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


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

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

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

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,

    ],

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

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

$ php artisan make:provider ClockServiceProvider

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

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

        App\Providers\ClockServiceProvider::class,
    ],
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関連のファイル

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

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 を参考に ClockInterface を定義します。システムの現在時刻を定義するインターフェースです。

DateTimeImmutable はクラスの挙動は DateTime とほぼ同じ振る舞いをします。ただし、自分自身は変更せずに新しいオブジェクトを返すという点だけが異なります。

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 クラスです。

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)

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

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

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

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

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

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

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が組み込まれており、インスタンスの差し替えが簡単に行えます。(プロダクションコードを書き換えることなくできる)

$ php artisan test

   PASS  Tests\Feature\ClockControllerTest
  ✓ invoke

  Tests:  1 passed
  Time:   0.75s

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

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

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インスタンスが生成されます。

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 の違い

$ php artisan tinker

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

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

spl_object_id メソッドでインスタンスIDを取得できるので、singletonbindの違いを確認してみます。

singleton

$ 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

$ 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 を実装します。

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数が減りますからね...😇)

参考

60
55
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?