6
4

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.

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

Last updated at Posted at 2019-03-22

まえがき

クリーンアーキテクチャの記事に非常に感化され、
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;

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 = []): mixed
    {
        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 = []): mixed
    {
        // 関数の定義を取得
        $reflectionFunction = new \ReflectionFunction($function);

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

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

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

        return $argumentCollection;
    }

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

        if ($reflectionParameter->isDefaultValueAvailable())
        {
            // デフォルト値の定義がある場合はデフォルト値を使用
            $argument = $reflectionParameter->getDefaultValue();
        }
        else
        {
            // 引数の指定もデフォルト値も無い場合、型宣言からの生成を試みる
            $type = $reflectionParameter->getType();
            $tempReflectionClass = $type !== null && !$type->isBuiltin() 
                ? new \ReflectionClass($type->getName())
                : null
            ;

            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 array $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 array $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
     */
    public static function getSingletonInstance(string $className): ?object
    {
        $className = Dependency::addLeadingBackslash($className);

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

        return null;
    }
}

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

追記:
・クラスを分離しました。最後のクラスを最新版に更新
・PHP8に対応(ReflectionParameter::getClassが使えなくなった&名前付き引数に対応)

6
4
0

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
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?