Help us understand the problem. What is going on with this article?

【PHP】依存性の自動注入を実装してみる【自動DI】

まえがき

クリーンアーキテクチャの記事に非常に感化され、
Laravel使ってない開発中のシステムに思想をぶっこみ出していたある日。

細分化したクラスやインスタンスを眺めて思いました。
案の定ちょっとしんどくなってきた…。やっぱ自動注入欲しいな。と。
怠惰な心と好奇心が疼きました。

依存性の自動注入用のクラスを作ってみたので
そのソースと実行例です。
# コンストラクタインジェクションはすぐそれっぽいのができました。
 そのあと結局あれもこれもと手は止まらず…

先に実行例

素材

class ValueObjectA
{
    public $value = "a!";
}

class ValueObjectB
{
    public $value = "b!";
}

class ValueObjectC
{
    public $value = "c!";
    private $a;

    public function __construct(\ValueObjectA $a)
    {
        $this->a = $a;
    }
}

コンストラクタインジェクション

namespace Demo\Path\To\Hoge;

class DemoConstructorInjection
{
    private $a;
    private $b;
    private $c;

    public function __construct(\ValueObjectA $a, \ValueObjectB $b, \ValueObjectC $c)
    {
        $this->a = $a;
        $this->b = $b;
        $this->c = $c;
    }
}

// $instance = new DemoConstructorInjection(new \ValueObjectA, new \ValueObjectB, new \ValueObjectC);
// の代わりに下記を使ってインスタンスを自動DIしてインスタンス化
$instance = \Core\Dependency::call(DemoConstructorInjection::class);
var_dump($instance);
object(Demo\Path\To\Hoge\DemoConstructorInjection)#7 (3) {
  ["a":"Demo\Path\To\Hoge\DemoConstructorInjection":private]=>
  object(ValueObjectA)#9 (1) {
    ["value"]=>
    string(2) "a!"
  }
  ["b":"Demo\Path\To\Hoge\DemoConstructorInjection":private]=>
  object(ValueObjectB)#10 (1) {
    ["value"]=>
    string(2) "b!"
  }
  ["c":"Demo\Path\To\Hoge\DemoConstructorInjection":private]=>
  object(ValueObjectC)#13 (2) {
    ["value"]=>
    string(2) "c!"
    ["a":"ValueObjectC":private]=>
    object(ValueObjectA)#15 (1) {
      ["value"]=>
      string(2) "a!"
    }
  }
}

その2 インターフェースの型宣言からの読み替え

interface DemoValueObjectInterface
{
}

class ValueObjectD implements DemoValueObjectInterface
{
    public $value = "d!";
}

class DemoConstructorInjection2
{
    public function __construct(\DemoValueObjectInterface $obj)
    {
        var_dump($obj->value);
    }
}

// 対応するクラスを設定
\Core\Dependency::bind(DemoValueObjectInterface::class, ValueObjectD::class);
\Core\Dependency::call(DemoConstructorInjection2::class);
string(2) "d!"

その3 引数の指定

class DemoConstructorInjection3
{
    public function __construct(\ValueObjectA $a, \ValueObjectB $b)
    {
        var_dump($a, $b);
    }
}


$a = new \ValueObjectA;
$a->value = "alter a!";
\Core\Dependency::call(DemoConstructorInjection3::class, [ $a ]);
object(ValueObjectA)#11 (1) {
  ["value"]=>
  string(8) "alter a!"
}
object(ValueObjectB)#12 (1) {
  ["value"]=>
  string(2) "b!"
}
$b = new \ValueObjectB;
$b->value = "alter b!";

// 第二引数だけ指定
\Core\Dependency::call(DemoConstructorInjection3::class, [ 1 => $b ]);
object(ValueObjectA)#14 (1) {
  ["value"]=>
  string(2) "a!"
}
object(ValueObjectB)#12 (1) {
  ["value"]=>
  string(8) "alter b!"
}

メソッドインジェクション

要領は同じ

class DemoMethodInjection
{
    public function demo(\ValueObjectA $a, \ValueObjectB $b, \ValueObjectC $c)
    {
        return [ $a, $b, $c ];
    }
}

$demoMethodInjection = \Core\Dependency::call(DemoMethodInjection::class);
$response = \Core\Dependency::callMethod($demoMethodInjection, "demo");
var_dump($response);

// 以下の形式も可能。ただしインスタンスへの変更は受け取れない
$response = \Core\Dependency::callMethod(DemoMethodInjection::class, "demo");
array(3) {
  [0]=>
  object(ValueObjectA)#16 (1) {
    ["value"]=>
    string(2) "a!"
  }
  [1]=>
  object(ValueObjectB)#17 (1) {
    ["value"]=>
    string(2) "b!"
  }
  [2]=>
  object(ValueObjectC)#20 (2) {
    ["value"]=>
    string(2) "c!"
    ["a":"ValueObjectC":private]=>
    object(ValueObjectA)#22 (1) {
      ["value"]=>
      string(2) "a!"
    }
  }
}

ファンクションインジェクション

function demoFunction (\ValueObjectA $a)
{
    var_dump($a->value);
}

\Core\Dependency::callFunction("demoFunction");
string(2) "a!"

その2 クロージャ

\Core\Dependency::callFunction(function (\ValueObjectA $a, \ValueObjectB $b) {
    var_dump($a->value, $b->value);
});
string(2) "a!"
string(2) "b!"

シングルトン

class Step1
{
    public function __construct(\ValueObjectA $a)
    {
        var_dump($a->value);
        $a->value = "alter a!";
    }
}

class Step2
{
    public function __construct(\ValueObjectA $a)
    {
        var_dump($a->value);
    }
}

\Core\Dependency::singleton(ValueObjectA::class);
\Core\Dependency::call(Step1::class);
\Core\Dependency::call(Step2::class);
string(2) "a!"
string(8) "alter a!"

その2 インターフェースの型宣言からの読み替え(bind併用)

class StepD1
{
    public function __construct(\DemoValueObjectInterface $d)
    {
        var_dump($d->value);
        $d->value = "alter d!";
    }
}

class StepD2
{
    public function __construct(\DemoValueObjectInterface $d)
    {
        var_dump($d->value);
        $d->value = "alter, alter d!";
    }
}

class StepD3
{
    public function __construct(\ValueObjectD $d)
    {
        var_dump($d->value);
    }
}

\Core\Dependency::bind(DemoValueObjectInterface::class, ValueObjectD::class);
\Core\Dependency::singleton(ValueObjectD::class);
\Core\Dependency::call(StepD1::class);
\Core\Dependency::call(StepD2::class);
\Core\Dependency::call(StepD3::class);
string(2) "d!"
string(8) "alter d!"
string(15) "alter, alter d!"

最後に つくったクラス

…という感じで使えるクラスになりました。

<?php
namespace Core;

use Core\Dependency\InterfaceToClassBindContainer;
use Core\Dependency\SingletonContainer;
use ReflectionClass;
use ReflectionMethod;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionParameter;
use Closure;

final class Dependency
{
    /**
     * 先頭にバックスラッシュが無ければ付与して返す
     * 
     * @param string $name
     * @return string
     */
    public static function addLeadingBackslash(string $name): string
    {
        return strpos($name, "\\") === 0 ? $name : "\\" . $name;
    }

    /**
     * 型宣言のインターフェース名をクラス名に読み替えるためのマップを設定
     * 
     * @param string $interfaceName 完全修飾インターフェース名
     * @param string $className     完全修飾クラス名
     */
    public static function bind(string $interfaceName, string $className): void
    {
        InterfaceToClassBindContainer::setInterfaceToClass($interfaceName, $className);
    }

    /**
     * 型宣言のクラス名とシングルトン用のインスタンスを設定
     * 
     * @param object|string $class
     *                      インスタンス
     *                      または完全修飾インターフェース/クラス名
     *                      名前で指定した場合は self::call でインスタンス化してから保持する
     *                      (インターフェース名の読み替えも実行される)
     * 
     * @param array         $specifiedArguments
     *                      数字添字配列で引数を部分的に指定可能
     *                     (キーは0から順)
     *                      $classを名前で指定した場合のみ有効
     * @return object
     *         インスタンスを返却
     */
    public static function singleton($class, array $specifiedArguments = []): object
    {
        if (is_string($class))
        {
            // 文字列の場合はインスタンス化
            $class = self::call($class, $specifiedArguments);
        }

        return SingletonContainer::setSingletonInstance($class);
    }

    /**
     * 指定されたインターフェースまたはクラスに対応するインスタンスを
     * コンストラクターの依存関係を解決した上で取得
     * 
     * @param string $fullName
     *               完全修飾インターフェース/クラス名
     * @param array  $specifiedArguments
     *               数字添字配列で引数を部分的に指定可能
     *               (キーは0から順)
     * @return object
     */
    public static function call(string $fullName, array $specifiedArguments = []): object
    {
        // 定義されたインターフェース名の場合はクラス名に置き換え
        $className = InterfaceToClassBindContainer::interfaceToClass($fullName);

        if (SingletonContainer::hasSingletonInstance($className))
        {
            // シングルトンの定義があれば返して終了
            return SingletonContainer::getSingletonInstance($className);
        }

        return self::callClass($className, $specifiedArguments);
    }

    /**
     * 指定されたクラスのインスタンスを
     * コンストラクターの依存関係を解決した上で取得
     * 
     * @param string $className
     *               完全修飾クラス名
     * @param array  $specifiedArguments
     *               数字添字配列で引数を部分的に指定可能
     *               (キーは0から順)
     * @return object
     */
    public static function callClass(string $className, array $specifiedArguments = []): object
    {
        $reflectionClass = new ReflectionClass($className);

        // コンストラクターの定義を取得
        $reflectionMethod = $reflectionClass->getConstructor();

        if (empty($reflectionMethod))
        {
            // コンストラクター定義が無い
            // 解決の必要がないのでそのままインスタンス化して返す
            return $reflectionClass->newInstance();
        }
        else
        {
            // 依存性を解決して引数の要素を得る
            // インスタンス化して返す
            return $reflectionClass->newInstanceArgs( self::resolution($reflectionMethod, $specifiedArguments) );
        }
    }

    /**
     * 指定されたクラスメソッドを
     * 引数の依存関係を解決した上で実行し、結果を返す
     * (クラスを名前で指定した場合、インスタンスは取得不可)
     * 
     * @param object|string $class
     *                      インスタンスまたは完全修飾クラス名
     * @param string        $methodName
     *                      メソッド名
     * @param array         $specifiedArguments
     *                      数字添字配列で引数を部分的に指定可能
     *                      (キーは0から順)
     * @return mixed
     */
    public static function callMethod($class, string $methodName, array $specifiedArguments = [])
    {
        if (is_string($class))
        {
            // 文字列の場合はインスタンス化
            $class = self::call($class);
        }

        // クラスメソッドの定義を取得
        $reflectionMethod = new ReflectionMethod($class, $methodName);

        // 依存性を解決して引数の要素を得る
        // メソッドを実行し、結果を返す
        return $reflectionMethod->invokeArgs($class, self::resolution($reflectionMethod, $specifiedArguments));
    }

    /**
     * 指定された関数またはクロージャを
     * 引数の依存関係を解決した上で実行し、結果を返す
     * 
     * @param string|Closure $function
     *                       関数名またはクロージャ
     * 
     * @param array          $specifiedArguments
     *                       数字添字配列で引数を部分的に指定可能
     *                       (キーは0から順)
     * 
     * @return mixed
     */
    public static function callFunction($function, array $specifiedArguments = [])
    {
        // 関数の定義を取得
        $reflectionFunction = new ReflectionFunction($function);

        // 依存性を解決して引数の要素を得る
        // 関数を実行し、結果を返す
        return $reflectionFunction->invokeArgs( self::resolution($reflectionFunction, $specifiedArguments) );
    }

    /**
     * メソッドまたは関数の引数の要素を配列にまとめて返す
     * 
     * 引数に指定があればその要素を使用し、
     * なければ型宣言をもとにインスタンスを作成して使用する
     * (各クラスに必要な引数も再帰的に解決を試みる)
     * 
     * @param ReflectionFunctionAbstract $reflectionMethod
     *                                   ReflectionMethod または ReflectionFunction
     * @param array                      $specifiedArguments
     *                                   数字添字配列で引数を部分的に指定可能
     *                                   (キーは0から順)
     * @return object[]
     */
    private static function resolution(ReflectionFunctionAbstract $reflectionMethod, array $specifiedArguments = []): array
    {
        $argumentCollection = [];

        // メソッド/関数の引数定義を取得
        // 引数定義ループ
        foreach ($reflectionMethod->getParameters() as $key => $reflectionParameter)
        {
            if (isset($specifiedArguments[ $key ]))
            {
                // あらかじめ引数の指定があればそちらを優先的に使用
                $argumentCollection[] = $specifiedArguments[ $key ];
            }
            else
            {
                // 引数の定義から引数の要素を取得
                // デフォルト値、型宣言のクラス、null の順で該当するものを返す
                $argumentCollection[] = self::getArgument($reflectionParameter);
            }
        }

        return $argumentCollection;
    }

    /**
     * 引数の定義から引数の要素を取得
     * デフォルト値、型宣言のクラス、null の順で該当するものを返す
     * 
     * @param ReflectionParameter $reflectionParameter
     * @return mixed
     */
    private static function getArgument(ReflectionParameter $reflectionParameter)
    {
        $argument = null;

        if ($reflectionParameter->isDefaultValueAvailable())
        {
            // デフォルト値の定義がある場合はデフォルト値を使用
            $argument = $reflectionParameter->getDefaultValue();
        }
        else
        {
            // 引数の指定もデフォルト値も無い場合、型宣言からの生成を試みる
            $tempReflectionClass = $reflectionParameter->getClass();

            if (!is_null($tempReflectionClass))
            {
                // デフォルト値が型宣言をもとにインスタンスを生成して使用(再帰)
                $argument = self::call($tempReflectionClass->getName());
            }
        }

        return $argument;
    }
}
<?php
namespace Core\Dependency;

use Core\Dependency;

final class InterfaceToClassBindContainer
{
    /**
     * 型宣言のインターフェース名をクラス名に読み替えるためのマップ
     * 
     * キー:完全修飾インターフェース名
     * 値  :完全修飾クラス名
     * 
     * @var array
     */
    private static $interfaceToClass = [];

    /**
     * 型宣言のインターフェース名をクラス名に読み替えるためのマップを設定
     * 
     * @param string $interfaceName 完全修飾インターフェース名
     * @param string $className     完全修飾クラス名
     */
    public static function setInterfaceToClass(string $interfaceName, string $className): void
    {
        // 先頭にバックスラッシュを付与
        $interfaceName = Dependency::addLeadingBackslash($interfaceName);
        $className     = Dependency::addLeadingBackslash($className);

        // 一応インターフェースとクラスの指定であることが確認できたものだけ
        if (interface_exists($interfaceName) && class_exists($className))
        {
            self::$interfaceToClass[ $interfaceName ] = $className;
        }
    }

    /**
     * 定義されたインターフェース名の場合はクラス名に置き換えて返却する
     * 定義されていない場合は指定された値をそのまま返却する
     * 
     * @param string $fullName
     *               完全修飾インターフェース名
     * @return string
     */
    public static function interfaceToClass(string $fullName): string
    {
        $fullName = Dependency::addLeadingBackslash($fullName);

        // 定義されたインターフェース名の場合はクラス名に置き換え
        if (empty(self::$interfaceToClass[ $fullName ]))
        {
            return $fullName;
        }
        else
        {
            return self::$interfaceToClass[ $fullName ];
        }
    }
}
<?php
namespace Core\Dependency;

use Core\Dependency;

final class SingletonContainer
{
    /**
     * 型宣言のクラス名とシングルトン用のインスタンス
     * 
     * キー:完全修飾クラス名
     * 値  :シングルトン用のインスタンス
     * 
     * @var array
     */
    private static $instanceCollectionForSingleton = [];

    /**
     * 型宣言のクラス名とシングルトン用のインスタンスを設定
     * 
     * @param object $instance
     * @return object
     *         インスタンスを返却
     */
    public static function setSingletonInstance(object $instance): object
    {
        self::$instanceCollectionForSingleton[ "\\" . get_class($instance) ] = $instance;

        return $instance;
    }

    /**
     * シングルトン用インスタンスが定義されているかどうかを返す
     * 
     * @param string $className
     * @return bool
     */
    public static function hasSingletonInstance(string $className): bool
    {
        return !empty(self::$instanceCollectionForSingleton[ Dependency::addLeadingBackslash($className) ]);
    }

    /**
     * シングルトン用インスタンスを返す
     * 
     * @param string $className
     * @return object|NULL
     */
    public static function getSingletonInstance(string $className): ?object
    {
        $className = Dependency::addLeadingBackslash($className);

        // シングルトンの定義があれば返して終了
        if (self::hasSingletonInstance($className))
        {
            return self::$instanceCollectionForSingleton[ $className ];
        }

        return null;
    }
}

ちょっと楽しかったです。

追記:クラスを分離しました。最後のクラスを最新版に更新

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away