Study & Practice

北海道札幌市のプログラマによる技術とか雑記のブログ

サービスコンテナ入門(Laravel)

5月から配属されたチームがLaravelを使ったチームなので、ちゃんと勉強しとこうと思ってLaravelのドキュメントを読み始めました。Laravelを使う上で避けて通れないのが「サービスコンテナ」ということで、本ポストを学んだことの事項とします。

サービスコンテナとは

公式ドキュメントでは「サービスコンテナはクラスの依存関係を管理し依存性の注入(Dependency Injection)を実行するための強力なツールです。」と記載があります。

laravel.com

依存性の注入(Dependency Injection)ってなに?なんのためにあるのっていう方にはこちらの記事がおすすめです。

qiita.com

それでは早速Laravelのコードを書いて学んでいきましょう。

準備

まずはLaravelプロジェクトを作ります。

composer create-project laravel/laravel service-container-example

※最新の公式ドキュメントではLaravel Sailの利用が推奨されているんですが、今開発用に使っているM1 MacBook Airだとエラーが解決できなかったのでcompoerを使ってます。

プロジェクトディレクトリに入ってPHP用のテストサーバを起動します。

cd service-container-example
php artisan serve

ブラウザからhttp://localhost:8000にアクセスしてLaravelのWelcomeページが表示されれば準備完了です。

サービスコンテナの使い方

サービスコンテナを使うためのAPIを作っていきます。まずはコントローラーからです。プロジェクトルートディレクトリで以下のコマンドを実行します。

php artisan make:controller HelloController

./app/Http/Controllers/HelloController.phpができるので、indexメソッドを作成します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HelloController extends Controller
{
    // 追加
    public function index()
    {
        return "Hello.";
    }
}

./routes/web.phpにHelloController@index用のルーティングを設定します。

<?php

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

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

// 追加
Route::get('/hello', [HelloController::class, "index"]);

これでhttp://localhost:8000/helloにアクセスしたら「Hello.」と表示されます。

それではこの機能を単純なサービスコンテナを使って実装していきます。

./app/Models/HelloMaker.phpを以下の内容で作成します。

<?php

namespace App\Models;

class HelloMaker
{
    public function text()
    {
        return "Hello from Hello Maker.";
    }
}

以下のコマンドを実行してHelloMakerProvider.phpを作成します。

php artisan make:provider HelloMakerProvider

./app/Providers/HelloMakerProvider.phpが作成されるのでregisterメソッドにサービスコンテナを利用するための処理を追加します。

<?php
namespace App\Providers;

use App\Models\HelloMaker;
use Illuminate\Support\ServiceProvider;

class HelloMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 追加
        $this->app->bind(HelloMaker::class, function() {
            return new HelloMaker();
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

そして./config/app.phpで定義されている連想配列のprovidersの項目に「App\Providers\HelloMakerProvider::class」を追加します。
ファイルは結構大きいので全ては載せませんが追加後のprovidersは私の環境では以下のようになっています。

    '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,

        // 追加
        App\Providers\HelloMakerProvider::class,
    ],


最後にHelloControllerの内容を以下のように編集します。

<?php

namespace App\Http\Controllers;

use App\Models\HelloMaker;
use Illuminate\Http\Request;

class HelloController extends Controller
{
    private $hello_maker;

    // 追加
    public function __construct(HelloMaker $hello_maker)
    {
        $this->hello_maker = $hello_maker;
    }

    public function index()
    {
        return $this->hello_maker->text();
    }
}

HelloMakerProvider.phpのregisterメソッドの処理を記載することによって、HelloControllerのコンストラクタでHelloMakerが呼ばれたときに自動的に新しいインスタンスが生成されるようになります。

これでhttp://localhost:8000/helloにアクセスしたときに「Hello from Hello Maker.」と表示されます。

ただこれはサービスコンテナを使ってはいますがHelloMakerがそのまま出てきているので依存性の注入の利点が活かせていません。ということで依存性の注入を活かすためにインターフェースを利用したバージョンにしてみましょう。

インターフェースを使ったバージョン

まずは./app/Models/TextMaker.phpをinterfaceとして定義します。

<?php

namespace App\Models;

interface TextMaker
{
    public function text();
}

HelloMakerをTextMakerの実装にします。

<?php

namespace App\Models;

// implements TextMakerを追加
class HelloMaker implements TextMaker
{
    public function text()
    {
        return "Hello from Hello Maker.";
    }
}

./app/Providers/HelloMakerProvider.phpを以下のように編集します。

<?php

namespace App\Providers;

use App\Models\HelloMaker;
use App\Models\TextMaker;
use Illuminate\Support\ServiceProvider;

class HelloMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 変更
        $this->app->bind(TextMaker::class, HelloMaker::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

./app/Http/Controllers/HelloController.phpの内容を以下に編集します。

<?php

namespace App\Http\Controllers;

use App\Models\TextMaker;

class HelloController extends Controller
{
    // $hello_maker から$text_makerに変更
    private $text_maker;

    public function __construct(TextMaker $text_maker)
    {
        $this->text_maker = $text_maker;
    }

    public function index()
    {
        return $this->text_maker->text();
    }
}

これでインターフェースを使ったバージョンの実装ができました。動作は変わっていませんが、HelloControllerではTextMakerしか参照していないのにhttp://localhost:8000/helloにアクセスすると「Hello from Hello Maker.」と表示されるはずです。

ついでにインターフェースの実装を呼び分けるパターンも試してみましょう

実装を呼び分けるパターン

以下のコマンドで./app/Http/Controllers/GoodbyeController.phpを作成します。

php artisan make:controller GoodbyeController

内容は以下のようにします。

<?php

namespace App\Http\Controllers;

use App\Models\TextMaker;
use Illuminate\Http\Request;

class GoodbyeController extends Controller
{
    private $text_maker;

    public function __construct(TextMaker $text_maker)
    {
        $this->text_maker = $text_maker;
    }

    public function index()
    {
        return $this->text_maker->text();
    }
}

./routes/web.phpにGoodbyeController用のルーティング設定を追加します。

use App\Http\Controllers\GoodbyeController;
use App\Http\Controllers\HelloController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('/hello', [HelloController::class, "index"]);

// 追加
Route::get('/goodbye', [GoodbyeController::class, "index"]);

./app/Models/GoodbyeMaker.phpを作成し、内容を以下のようにします。

<?php

namespace App\Models;

class GoodbyeMaker implements TextMaker
{
    public function text()
    {
        return "Goodbye from Goodbye Maker.";
    }
}

以下のコマンドで./app/Providers/TextMakerProvider.phpを作成します。

php artisan make:provider TextMakerProvider

内容を以下のようにします。

<?php

namespace App\Providers;

use App\Http\Controllers\GoodbyeController;
use App\Http\Controllers\HelloController;
use App\Models\GoodbyeMaker;
use App\Models\HelloMaker;
use App\Models\TextMaker;
use Illuminate\Support\ServiceProvider;

class TextMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->when(HelloController::class)
            ->needs(TextMaker::class)
            ->give(HelloMaker::class);
        
        $this->app->when(GoodbyeController::class)
            ->needs(TextMaker::class)
            ->give(GoodbyeMaker::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

./app/Providers/HelloMakerProvider.phpは不要になるので削除します。

最後に./config/app.phpのprovidersからApp\Providers\HelloMakerProvider::classをApp\Providers\TextMakerProvider::classを変更します。

これでhttp://localhost:8000/helloにアクセスすると「Hello from Hello Maker.」と表示され、http://localhost:8000/goodbyeにアクセスすると「Goodbye from Goodbye Maker.」と表示されます。

まとめ

以上がLaravelのサービスコンテナの使い方になります。今回のようなテキストを返すだけのAPIに対してサービスコンテナを使ってもあまりメリットは感じられません。しかし、コントローラごとにDBの向き先を変えたい場合や、テストでモックを使いたい場合などではインターフェースを利用した依存性の注入は非常に強力です。疎結合な設計をする際にも重要な技術になってくるので使い方を覚えておいて損はないはずです。みなさんも是非使ってみてください。