Laravel風DIコンテナ

経緯

LaravelのDIコンテナが使いやすいので、自作ライブラリや軽量フレームワークでも似たような構造のものを利用したい。

実装

とりあえず以下のようにしてみた。

DI/Container

namespace DI;

class Container
{
    /**
     * バインドしたクラス管理
     *
     * @var array
     */
    protected $bindings = [];

    /**
     * クラスをバインドする
     *
     * @param string $fromClass
     * @param string|callable $toClass
     * @return void
     */
    public function bind($fromClass, $toClass)
    {
        $this->bindings[$fromClass] = new Binding($fromClass, $toClass, false, $this);
    }

    /**
     * クラスをシングルトンとしてバインドする
     *
     * @param string $fromClass
     * @param string|callable $toClass
     * @return void
     */
    public function singleton($fromClass, $toClass)
    {
        $this->bindings[$fromClass] = new Binding($fromClass, $toClass, true, $this);
    }

    /**
     * クラスインスタンスを作成する
     *
     * @param string $fromClass
     * @return object
     */
    public function make($fromClass)
    {
        if (isset($this->bindings[$fromClass])) {
            return $this->bindings[$fromClass]->getInstance();
        } else {
            return (new Binding($fromClass, $fromClass, false, $this))->getInstance();
        }
    }
}

DI/Binding.php

namespace DI;

class Binding
{
    /**
     * クラスインスタンス作成用コールバック
     *
     * @var callable
     */
    private $callback;

    /**
     * シングルトン用インスタンス
     * 
     * @var object
     */
    private $instance;

    /**
     * 返却するインスタンスの実体クラス名
     *
     * @var string
     */
    private $toClass;

    public function __construct(
        private string $fromClass,
        $toClass,
        private bool $needsSingleton,
        private Container $container
    )
    {
        if (is_callable($toClass)) {
            $this->callback = $toClass;
        } else {
            $this->toClass = $toClass;
        }
    }

    /**
     * インスタンスを作成して返す
     *
     * @return object
     */
    public function getInstance()
    {
        if (isset($this->instance)) {
            return $this->instance;
        }

        if ($this->callback !== null) {
            $instance = $this->createInstanceFromCallback($this->callback);
        } else {
            $instance = $this->createInstanceFromClassName($this->toClass);
        }

        if (!($instance instanceof $this->fromClass)) {
            throw new InvalidClassException();
        }

        if ($this->needsSingleton) {
            $this->instance = $instance;
        }

        return $instance;
    }

    /**
     * コールバックを利用してインスタンスを作成する
     *
     * @param callable $callback
     * @return object
     */
    protected function createInstanceFromCallback($callback)
    {
        return $callback($this->container);
    }

    /**
     * 実体クラス名からインスタンスを作成する
     *
     * @param string $class
     * @return object
     */
    protected function createInstanceFromClassName($class)
    {
        $ref = new \ReflectionClass($class);

        $constructor = $ref->getConstructor();

        if ($constructor === null) {
            return $ref->newInstance();
        }

        $params = $constructor->getParameters();

        $arguments = [];
        foreach ($params as $param) {
            $type = $param->getType();
            if (!$type) {
                throw new InvalidParameterException();
            }

            $strClass = (string) $type;
            if (class_exists($strClass) || interface_exists($strClass)) {
                $arguments[] = $this->container->make($strClass);
            } else {
                throw new InvalidParameterException();
            }
        }

        return $ref->newInstanceArgs($arguments);
    }
}

DI/InvalidClassException

namespace DI;

use Exception;

class InvalidClassException extends Exception
{
}

DI/InvalidParameterException

namespace DI;

use Exception;

class InvalidParameterException extends Exception
{
}

使い方

本コンテナはコンストラクタインジェクションにのみ対応している。


// 何らかのメッセージを返すクラスのインターフェース
interface MessageInterface
{
    public function say();
}

// それを実装した"hello"を返すクラス
class MessageHello implements MessageInterface
{
    public function say()
    {
        return 'hello';
    }
}

// また、それを実装した"bye"を返すクラス
class MessageBye implements MessageInterface
{
    public function say()
    {
        return 'bye';
    }
}

// さらに、外部で設定した文字列を返すクラス
class MessageSettable implements MessageInterface
{
    private $message;
    public function say()
    {
        return $this->message;
    }

    public function setMessage($message)
    {
        $this->message = $message;
    }
}

// MessageInterfaceに依存しているサービスクラス
class MessageService
{
    public function __construct(private MessageInterface $message)
    {
    }

    public function execute()
    {
        return $this->message->say();
    }
}

上記のようなインターフェースと、それに依存しているサービスクラスが存在する場合

// インスタンス作成
$container = new DI\Container();

// 依存するインターフェース
$container->bind(MessageInterface::class, MessageHello::class);

// サービスクラスのインスタンスを作成する
$service = $container->make(MessageService::class);

echo $service->execute(); // "hello"が返ってくる

また、クロージャを利用して返すこともできる

$container->bind(MessageInterface::class, function ($container) {
    $message = $container->make(MessageSettable::class);
    // または、普通にインスタンス作成してもよい
    // $message = new MessageSettable();
    $message->setMessage('good morning');

    return $message;
});

$service = $container->make(MessageService::class);

echo $service->execute(); // "good morning"が返ってくる

singletonメソッドを使うことで単一インスタンスを返すこともできる

$container->singleton(MessageSettable::class, MessageSettable::class);

$message = $container->make(MessageSettable::class);
$message->setMessage('単一インスタンス');

echo $message->say(); // 当然"単一インスタンス"が返る

// 再度インスタンスを作成する
$message2 = $container->make(MessageSettable::class);

echo $message->say(); // setMessageしていなくても"単一インスタンス"が返る

最後に

以上、車輪の再開発でした。

ファイルは、こちら