64
41

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 3 years have passed since last update.

PHP-DIイントロダクション

Last updated at Posted at 2018-12-30

この記事について

最近はDIライブラリが標準で付いているPHPフレームワークも多いと思いますが、フレームワークの機能に極力依存せずにDIをしたかったので、外部ライブラリーによるDIを試してみました。

PHP用のDIいくつかあったのですが今回はPHP-DIについて調べたのでその基本的な使い方を記載したいと思います。
自分が知りたかった箇所のみ記載してますので、詳しい使用方法は公式ページで確認してみてください。

環境

今回はphp と composer のみの最小構成の環境で試してます。

$ php -v
PHP 7.3.0 (cli) (built: Dec  6 2018 02:17:00) ( ZTS MSVC15 (Visual C++ 2017) x86 )

$ composer -v
Composer version 1.8.0 2018-12-03 10:31:16

導入

まずはcomposerを使用してPHP-DIをインストールします

composer require php-di/php-di

インストール後の composer.jsonはこんな感じの行が追加されています

{
    "name": "hirodragon/testapp",
    // ...
    "require": {
        "php-di/php-di": "^6.0" // ←追加されている
    }
}

また、[project]/vendor/composer/autoload_psr4.php にautoload設定も追加されています。

<?php
return array(
    // ...省略
    'DI\\' => array($vendorDir . '/php-di/php-di/src'),
);

インストールしたら、autoloadの設定等も済んでいるので、すぐにDIコンテナインスタンスをphpのソース上で使用できるようになります。

試しに index.phpを作成して下記のコードを実行してみます。

index.php
<?php
require_once 'vendor/autoload.php';

$container = new DI\Container();
var_dump($container);

実行結果

$ php index.php
object(DI\Container)#2 (8) {
    # ...省略
} 

試してみる

まずは設定を使わずに使用してみて動作を試してみようと思います。

構成

今回用にsampleというパッケージを作成してみました。クリーンアーキテクチャのソフトウェアを想定して配下に4つディレクトリを作成しています。
ここに色々ファイルを追加して試してみようと思います。

(以下この時点でのディレクトリ構成)

project
│   composer.json
│   composer.lock
│   index.php
│───package
│   └───sample # このsampleパッケージに色々追加していきます
│       ├───app
│       ├───usecase
│       ├───domain
│       └───infra
└───vendor

まずは追加したディレクトリをautoloadに追加しておきましょう。

composer.json
{
    ...
    "autoload": {
        "psr-4": {
            "package\\" : "package/" // 追加
        }
    },
    "require": {
        "php-di/php-di": "^6.0"
    }
}

autoload更新

$ composer dumpautoload

Sampleパッケージ

次にSampleController.php, SampleUseCase.phpを作成して、先程作成したindex.phpを修正します。
index.php -> SampleController.php -> SampleUseCase.php の順に呼び出しされるような想定です。

SampleController.php
<?php
namespace package\sample\app;

use package\sample\usecase\SampleUseCase;

class SampleController
{
    /**
     * @var SampleUseCase
     */
    private $usecase;

    public function __construct(SampleUseCase $usecase)
    {
        $this->usecase = $usecase;
        var_dump(get_class($this->usecase));
    }
}
SampleUseCase.php
<?php
namespace package\sample\usecase;

class SampleUseCase
{
    public function __construct()
    {
    }
}
index.php
<?php
require_once 'vendor/autoload.php';
use package\sample\app\SampleController;
use package\sample\usecase\SampleUseCase;

$container = new DI\Container();

$controller = new SampleController(new SampleUseCase());

実行結果

$ php index.php
string(36) "package\sample\usecase\SampleUseCase"

このSampleControlleを生成する際に引数として渡している SampleUseCase が今回のDIの対象です。

まずは試しに $container->set() にて依存関係を設定して使用してみます。
※set()の公式ドキュメントには定義ファイルを使用することをお勧めしますと書かれていますのでご注意を

index.phpを書き換えてset()にて定義してみます。

index.php
<?php
require_once 'vendor/autoload.php';
use package\sample\app\SampleController;
use package\sample\usecase\SampleUseCase;

$container = new DI\Container();
$container->set('SampleUseCase', new SampleUseCase());

$controller = new SampleController($container->get('SampleUseCase'));

実行結果

$ php index.php
string(36) "package\sample\usecase\SampleUseCase"

変更前と同様に動く事が確認できました。
ただ、この使い方ではいたる所が$containerに対する記述だらけになってしまうので、やはりちゃんと定義を使用した方が良さそうです。

定義

公式サイト : Definitions

DIする為の定義をする方法は3つあるようです。

  • auto wiring
  • annotations
  • PHP definitions

これらは併用する事もでき、併用した際は下記の優先順位で適用されます。

  1. 明示的定義したコンテナ 1
  2. PHPファイルによる定義(PHP configuration 複数ファイルに設定した場合最後の設定)
  3. アノテーションによる定義(Annotation)
  4. 型宣言よる定義(Auto wiring)

とりあえずこの3つの基本的な使い方を見てみます。

Auto wiring

auto wiring はデフォルトで on になっている為そのまま使用できます。
型宣言と同一の型を自動で検出して生成してインジェクトしてくれるようです。
index.phpを修正して実際に動かしてみます。

index.php
<?php
require_once 'vendor/autoload.php';

$container = new DI\Container();
$controller = $container->get('package\sample\app\SampleController');
var_dump(get_class($controller));

実行結果

$ php index.php
string(36) "package\sample\usecase\SampleUseCase"
string(35) "package\sample\app\SampleController"

ちゃんと SmapleUseCaseオブジェクトがSampleControllerに注入されたようです。

また、このコンストラクタによるAuto wiring機能は再起的にインジェクトしてくれるので、最初の生成にてその配下のオブジェクトにもインジェクトしてくれます。

infra層にDB用のクラスを作成して、UseCaseにインジェクトする用にコードを修正して試してみたいと思います。

MySqlDB.php

<?php
namespace package\sample\infra;

class MySqlDB
{
    public function save(int $number): void
    {
        // なんか保存処理
    }
}
SampleUseCase.php
<?php
namespace package\sample\usecase;

use package\sample\infra\MySqlDB;

class SampleUseCase
{
    public function __construct(MySqlDB $db) // 作成したMySqlDBクラスを受け取る用に修正
    {
        var_dump(get_class($db));  // 
    }
}

実行結果

$ php index.php
string(28) "package\sample\infra\MySqlDB"
string(36) "package\sample\usecase\SampleUseCase"
string(35) "package\sample\app\SampleController"

Controllerをコンテナより生成しただけで、その先のUseCaseクラスに必要なクラスもインジェクトしてくれている事が確認できました。

ただ、Auto wiring は型宣言を頼りに注入するオブジェクトを判断している為、下記のような場合は解決できません。
実際にはDIしたい時はほぼinterfaceに対してだと思うので、他の方法を使う必要があるそうです。


class Database
{
    public function __construct($dbHost, $dbPort) // 型宣言がない
    {
        // ...
    }

    public function setLogger(LoggerInterface $logger) // インターフェース
    {
        // ...
    }
}

こういったケースでは、PHPファイルによる定義でDI\autowire()に何を注入するかを明示的に宣言をする必要があります。

PHP definitions

続いてはPHPに定義を記述してDIの依存関係の定義をする方法を試してみようと思います。

定義の登録方法は下記の2種類あるようです。

// addDefinitions()の引数に配列を渡す
$containerBuilder->addDefinitions([
    // place your definitions here
]);
// addDefinitions()の引数に定義ファイル名を渡す
$containerBuilder->addDefinitions('config.php');

こちらも基本的な使い方のみさっそく試してみたいと思います。

Sample2

先ほどとは別にSample2パッケージを作成して上記2種類の方法を試してみます(中身は先程のsampleパッケージと同じですがUseCaseクラスのコンストラクタはいったん無しとしています)

addDefinitionsに連想配列として定義
index.php
<?php
require_once 'vendor/autoload.php';

$builder = new DI\ContainerBuilder();
$builder->addDefinitions([
    'SampleUseCase' => function($c){
        return $c->get('package\sample2\usecase\SampleUseCase');
    },
    'SampleController' => DI\create('package\sample2\app\SampleController')
        ->constructor(DI\get('SampleUseCase')),
]);
$container = $builder->build();

$controller = $container->get('SampleController');
var_dump(get_class($controller));

// 実行結果
$ php index.php
string(37) "package\sample2\usecase\SampleUseCase"
string(36) "package\sample2\app\SampleController"
定義ファイルを作成して定義

(index.phpと同階層にdiconfig.phpを設置)

diconfig.php
<?php

use Psr\Container\ContainerInterface;
use function DI\factory;
use package\sample2\usecase\SampleUseCase;
use package\sample2\app\SampleController;

return [
    'SampleUseCase' => DI\factory(function (ContainerInterface $c) {
        return new SampleUseCase();
    }),
    'SampleController' => DI\factory(function (ContainerInterface $c) {
        return new SampleController($c->get('SampleUseCase'));
    }),
];
index.php
<?php
require_once 'vendor/autoload.php';

$builder = new DI\ContainerBuilder();
$builder->addDefinitions('diconfig.php');
$container = $builder->build();


$controller = $container->get('SampleController');
var_dump(get_class($controller));

// 実行結果
$ php index.php
string(37) "package\sample2\usecase\SampleUseCase"
string(36) "package\sample2\app\SampleController"

DI\factory() は型宣言から推測できない場合等に使用する事ができるようです。
詳しくは公式ドキュメントを参照してみてください。
この設定ファイルを使用すればinterfaceに対するDI設定も記述できるようですが、いったん最後のアノテーションによる定義も先に見てみようと思います。

Annotations

アノテーションを使用した定義方法を見てみます。

デフォルトではこの方法は無効となっているので、まずは有効化します。

# Annotationsライブラリをインストール
composer require doctrine/annotations

また、使用するさいは有効化する記述が必要です

$containerBuilder->useAnnotations(true);

定義方法としてはphpdoc に @injectアノテーションを記載する事でPHP-DIにインジェクト対象とインジェクト内容を定義する事ができます。

Sample3パッケージ

先ほどと同様に同じ内容でsample3というパッケージを切って試して見ました。
内容はほぼ同じですが以下のようになっています。

index.php
<?php
require_once 'vendor/autoload.php';

$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->useAnnotations(true);
$container = $containerBuilder->build();

$controller = $container->get('package\sample3\app\SampleController');
var_dump(get_class($controller));
SampleController.php
<?php
namespace package\sample3\app;

use package\sample3\usecase\SampleUseCase;

class SampleController
{
    /**
     * @inject
     * @var SampleUseCase
     */
    private $usecase;

    public function __construct(SampleUseCase $usecase)
    {
        $this->usecase = $usecase;
        var_dump(get_class($this->usecase));
    }
}
# 実行結果
$ php index.php
string(37) "package\sample3\usecase\SampleUseCase"
string(36) "package\sample3\app\SampleController"

@injectアノテーションを付けただけで本当にインジェクトしてくれました。

とりあえずこれで3種類の方法で一番シンプルな形でDIする事は試せました。

これら3つの方法を組み合わせる事で柔軟にDIをする事ができそうです。

本題

基本的な使用方法に目を通したのでそれらを併用してもう少し実用的に使用してみようと思います。
ここまでは実クラスにのみの定義でしたが、一番使用頻度の高いと思われるインターフェースへのDIを織り交ぜた形で試します。

Sample4パッケージ

新たにパッケージをコピーしてinterfaceを使用する形に追加修正しました。
各ファイルは以下のようになっています。

SampleController.php
<?php
namespace package\sample4\app;

use package\sample4\usecase\SampleUseCase;

class SampleController
{
    /**
     * @var SampleUseCase
     */
    private $usecase;

    public function __construct(SampleUseCase $usecase)
    {
        $this->usecase = $usecase;
    }
}
SampleUseCase.php
<?php
namespace package\sample4\usecase;

use package\sample4\domain\SampleDB;

class SampleUseCase
{
    /**
     * @var SampleDB
     */
    private $db;

    public function __construct(SampleDB $db) // interfaceによる型宣言
    {
        $this->db = $db;
    }
}
SampleDB.php
<?php
namespace package\sample4\domain;

interface SampleDB
{
    public function save(int $number): void;
}
InMemoryDB.php
<?php
namespace package\sample4\infra;

use package\sample4\domain\SampleDB;

/**
 * SampleDBインターフェースの具象クラス1
 */
class InMemoryDB implements SampleDB
{
    private $data = [];

    public function save(int $number): void
    {
        $this->data[] = $number;
    }
}
MySqlDB.php
<?php
namespace package\sample4\infra;

use package\sample4\domain\SampleDB;

/**
 * SampleDBインターフェースの具象クラス2
 */
class MySqlDB implements SampleDB
{
    public function save(int $number): void
    {
        // なんか保存処理
    }
}

クライアントコード

index.php
<?php
require_once 'vendor/autoload.php';

use package\sample4\app\SampleController;
use package\sample4\infra\InMemoryDB;
use package\sample4\infra\MySqlDB;
use package\sample4\usecase\SampleUseCase;

// 以下のコードをPHP-DIを使用して自動化したい
$db = new InMemoryDB();
$usecase = new SampleUseCase($db);
$controller = new SampleController($usecase);
var_dump($controller);
# 実行結果
$ php index.php
object(package\sample4\app\SampleController)#12 (1) {
  ["usecase":"package\sample4\app\SampleController":private]=>
  object(package\sample4\usecase\SampleUseCase)#11 (1) {
    ["db":"package\sample4\usecase\SampleUseCase":private]=>
    object(package\sample4\infra\InMemoryDB)#10 (1) {
      ["data":"package\sample4\infra\InMemoryDB":private]=>
      array(0) {
      }
    }
  }
}

今回はinterfaceが使用されているので、先程試した中のPHPファイルによる定義によりその依存関係を定義し、それ以外の依存関係はアノテーションにて定義するようにしたいと思います。

interfaceの依存関係を定義

設定ファイルに以下のようにマッピングを定義します

diconfig.php
<?php

use package\sample4\domain\SampleDB;
use package\sample4\infra\InMemoryDB;

return [
    SampleDB::class => DI\autowire(InMemoryDB::class),
];

index.phpを書き換えて、まずはこの部分だけを試してみます。

index.php
<?php
require_once 'vendor/autoload.php';

$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->addDefinitions('diconfig.php');
$container = $containerBuilder->build();

use package\sample4\usecase\SampleUseCase;

$usecase = $container->get(SampleUseCase::class);
var_dump($usecase);
# 実行結果
$ php index.php
object(package\sample4\usecase\SampleUseCase)#20 (1) {
  ["db":"package\sample4\usecase\SampleUseCase":private]=>
  object(package\sample4\infra\InMemoryDB)#23 (1) {
    ["data":"package\sample4\infra\InMemoryDB":private]=>
    array(0) {
    }
  }
}

SampleUseCaseクラスのコンストラクタに指定されているSampleDBインターフェースの具象クラスInMemoryDBクラスが差し込まれました。
後はSampleControllerに対するインジェクションはSampleUseCase具象クラスなので、アノテーションにて設定したいと思います。

SampleController.php
<?php
namespace package\sample4\app;

use package\sample4\usecase\SampleUseCase;

class SampleController
{
    /**
     * @inject  // アノテーションを追加
     * @var SampleUseCase
     */
    private $usecase;

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

クライアントコード

index.php
<?php
require_once 'vendor/autoload.php';

use package\sample4\app\SampleController;

$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->useAnnotations(true);
$containerBuilder->addDefinitions('diconfig.php');
$container = $containerBuilder->build();

$controller = $container->get(SampleController::class);
var_dump($controller);
# 実行結果
$ php index.php
object(package\sample4\app\SampleController)#29 (1) {
  ["usecase":"package\sample4\app\SampleController":private]=>
  object(package\sample4\usecase\SampleUseCase)#33 (1) {
    ["db":"package\sample4\usecase\SampleUseCase":private]=>
    object(package\sample4\infra\InMemoryDB)#38 (1) {
      ["data":"package\sample4\infra\InMemoryDB":private]=>
      array(0) {
      }
    }
  }
}

クライアントコードでコンテナからコントローラーを作成する事により、その後必要なクラスにわたって全ての依存関係が解決された上でインジェクションがされている事が確認できました。

おしまい

今回記載した内容は基本操作となりますが、ここまで来れば後はドキュメントを参照して適宜用途にあった設定をしていくのみで使用できそうです。

本文では書けなかったですが、フレームワークを使用している際はControllerは既にフレームワーク側で生成されていると思いますので、その場合は injectOn()を使用すれば生成済のオブジェクトに対してもDIできます。
もちろん生成後なのでコンストラクタインジェクションはできないので、プロパティインジェクションにてDIする事になります。
生成されたコントローラーにはinjectOn()を使用し、それ以降の依存解消には上記の設定等を使用するのが良さそうです。
詳細はこちらに記載されています。

  1. 最初に試した$container->set()を使用する方法です

64
41
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
64
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?