RitoLabo

Laravelのサービスコンテナで依存性注入(DI)を行う

  • 公開:
  • 更新:
  • カテゴリ: PHP Laravel
  • タグ: PHP,Laravel,ServiceContainer,DI,Repository

PHPなどでオブジェクト指向にてアプリケーション開発を行っているならば、出来るだけ結合度を低く保った状態でそれぞれの機能開発を進めていきたいところです。

Laravelでは、DIをスムーズに実現できる仕組みが提供されています。今回はLaravelのサービスコンテナを用いた依存性注入について見ていきます。

アジェンダ
  1. 開発環境
  2. 依存性注入
  3. 入門編
    1. コンポーネント
    2. サービスプロパイダ
    3. 依存性注入を行う
  4. 実践編

開発環境

今回の開発環境は以下の通りです。

  • Linux CentOS 7
  • Apache 2.4
  • PHP 7.3
  • Laravel 5.8

Laravelのルートディレクトリを「laravel/」とします。

依存性注入

DI(Dependency Injection=依存性注入)とは、依存関係にあるオブジェクトを自身の中ではなく外から設定してあげる事で、よりスムーズなソース管理や処理の流れを実現するデザインパターンの事です。

Laravelでは、これを簡単に導入できる仕組みが提供されています。

まずは何も考えずにクラス定義を行い、以下のようなコードがあったとします。

class SampleController extends Controller
{
protected $right;
protected $left;

public function __construct()
{
$this->right = new Right();
$this->left = new Left();
}

SampleクラスでRightクラスとLeftクラスをインスタンス化していますが、このSampleクラスは、RightクラスとLeftクラスがあってこそ全ての処理が実装できる事を意味しています。つまり「SampleクラスはRightクラスとLeftクラスに依存している」という状態になります。

こういったメインのオブジェクトが依存するオブジェクトを自身の中で具象化するのではなく抽象化を行い、それらを外から入れてあげる事で、オブジェクト同士がより疎の関係となり、保守性の向上につながる。という事になります。

入門編

まずは抽象化などを一旦無視して、基本的なDI実装の流れだけを見ていきます。

今回は定番の計算機能を実装します。冒頭の例に例えれば、DI前は以下のような関係性です。

<?php

namespace App\Http\Controllers;

use App\Components\Calculation;

class SampleController extends Controller
{
public function index()
{
$calculation = new Calculation();
}

コンポーネント

まずは注入する計算用のコンポーネントを作成します。laravel/app 配下に Components ディレクトリを作成し、その配下に Calculationクラスを作成します。

laravel
├─ app
│   ├─ Components
│   │   └─
Calculation.php
laravel/app/Component/Calculation.php
<?php
declare(strict_types=1);

namespace App\Components;

class Calculation
{
// 加算
public function add(int $arg1, int $arg2): int
{
return $arg1 + $arg2;
}

// 減算
public function sub(int $arg1, int $arg2): int
{
return $arg1 - $arg2;
}

// 乗算
public function mul(int $arg1, int $arg2): int
{
return $arg1 * $arg2;
}

// 除算
public function div(int $arg1, int $arg2): float
{
return $arg1 / $arg2;
}
}

シンプルな四則演算のメソッドを実装しました。

サービスプロパイダ

コンポーネントの実装が完了したので、サービスコンテナから使用できるようにします。

まずはサービスプロパイダを作成し、定義します。Laravelのルートディレクトリへ移動し、以下のartisanコマンドを叩いてサービスプロパイダを生成します。

# Laravelルートディレクトリへ移動
cd /path/to/laravel

# サービスプロパイダ作成
php artisan make:provider CalculationServiceProvider

# 実行結果
[demo@localhost laravel]$ php artisan make:provider CalculationServiceProvider
Provider created successfully.

laravel/app/Providers配下にCalculationServiceProvider.phpが生成されるので、register()メソッドを以下に定義します。

laravel/app/Providers/CalculationServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Components\Calculation;

class CalculationServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->bind('Calculation', Calculation::class);
}

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

最後にサービスプロバイダを登録します。

laravel/config/app.php
'providers' => [
/*
* 省略...
*/
App\Providers\CalculationServiceProvider::class,

依存性注入を行う

これで一連のパターンは実装できました。それでは実際にコントローラに依存性注入を行ってみます。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Components\Calculation;

class CalculationController extends Controller
{
protected $Calculation;

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

public function index()
{
$result = [
'add' => $this->Calculation->add(1, 1),
'sub' => $this->Calculation->sub(2, 1),
'mul' => $this->Calculation->mul(3, 2),
'div' => $this->Calculation->div(4, 2),
];

print_r($result);
// => Array
// (
// [add] => 2
// [sub] => 1
// [mul] => 6
// [div] => 2
// )
}
}

依存性注入が行われ、Calculation機能を使えるようになりました。コンストラクタにてタイプヒンティング付きで渡す事で自動で依存解決される(コンストラクタインジェクションといいます)ので、この中でインスタンス化しなくても済んでいる事がわかります。

実践編

ここまでで簡単なDIの流れを見てきました。今度は抽象化も行って、もう少し実践的に使ってみます。

まずはDIを行う為のサンプルで適当なコンポーネントを2つ作成しますが、それらの為のインターフェースを実装する形で作成します。

laravel/app/Services/SampleInterface.php
<?php
declare(strict_types=1);

namespace App\Services;

interface SampleInterface
{
public function execute(): string;
}
laravel/app/Components/SampleComponentAAA.php
<?php
declare(strict_types=1);

namespace App\Components;

use App\Services\SampleInterface;

class SampleComponentAAA implements SampleInterface
{
public function execute(): string
{
return 'AAA';
}
}
laravel/app/Components/SampleComponentBBB.php
<?php
declare(strict_types=1);

namespace App\Components;

use App\Services\SampleInterface;

class SampleComponentBBB implements SampleInterface
{
public function execute(): string
{
return 'BBB';
}
}

ちょっと順番が前後しますが、これらのコンポーネントをサービスクラス経由で利用するとして、そのサービスクラスをコントローラから呼んだ場合は以下の実装になるでしょう。

laravel/app/Services/SampleService.php
<?php
declare(strict_types=1);

namespace App\Services;

class SampleService
{
protected $SampleComponent;

public function __construct(SampleInterface $SampleComponent)
{
$this->SampleComponent = $SampleComponent;
}

public function index()
{
return $this->SampleComponent->execute();
}
}
laravel/app/Http/SampleController.php
<?php
declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\SampleService;

class SampleController extends Controller
{
protected $SampleService;

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

public function index()
{
echo $this->SampleService->index();
}
}

そしてこの場合に、サービスプロバイダの記述は以下になります。(AppServiceProviderに記述しますが専用に作成してもOK)

laravel/app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Services\SampleInterface;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind('SampleService', function($app) {
return new \App\Services\SampleService($app->make(SampleInterface::class));
});

$this->app->bind(SampleInterface::class, function($app) {
return new \App\Components\SampleComponentAAA();
// OR
//return new \App\Components\SampleComponentBBB();
});
}

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

抽象化を行いそれをコンテナ側で解決しているので、サービスクラスではどの具象クラスを使用するかについて言及しなくても良い流れになっています。また、利用するコンポーネントを変更する際にもサービスクラスには変更が不要になります。

コントローラ側では、サービスクラスをセットする部分でもサービスクラス側の依存が自動的に注入されているので、コンポーネントの部分をあれこれ取り回さなくて済んでいます。

もしDIを使わなかった場合、利用するコンポーネントを変更したら、サービスクラスとコントローラの両方で変更が必要になります。こういった部分をシンプルに出来るのもDIの利点の1つだと思います。

まとめ

以上で作業は完了です。オブジェクト同士の関係をシンプルに保つ事で、拡張性や保守性も向上するので是非試してみてください。

サンプルソース