DIコンテナ使ってみて思った雑文

アプリケーションの流れとして、

  1. コンテナを構築
  2. アプリを実行するなにか(パイプラインとかディスパッチャとか)をコンテナから取り出す
  3. リクエストオブジェクトを作成してアプリを実行

みたいな流れかなと思う。順番的にDIコンテナが先に作られるのでリクエストオブジェクトやリクエストに含まれる情報はDIコンテナには入らない。そもそもDIコンテナはグローバルなコンテキストのものなので、リクエストのコンテキストに属するオブジェクトを入れるべきではない。

(PHPでは出来ないけれども)もし可能なのならDIコンテナはリクエストをまたがって共有されても良い。なのでコンテナに入るオブジェクトはステートレスでなければならないし、再入可能でスレッドセーフであるべき(PHPでは関係ないけれども)。

・・・みたいなイメージ。


例えば Logger にリクエストに基づく情報が自動的に付加されるようにしてみる(IPアドレスとか)。

$logger = $logger->withContext(['ipaddr' => $ipaddr]);

// $context に 'ipaddr' が追加される
$logger->info('ほげほげ');

こうして作られた Logger はリクエストのコンテキストに属するので、グローバルなコンテキストであるところのコンテナには入らない。

サービスクラスがコンテナに入っているのだとすると、サービスクラスのコンストラクタにインジェクションできるものはコンテナに入っているものだけなので、↑のよう部分的に適用された Logger をインジェクションすることはできない。

大本の Logger がインジェクションされて、そしてサービスのメソッドにリクエストオブジェクトが渡されるようにする?(コードはイメージです)

class HogeService
{
    public function __construct($logger)
    {
        $this->logger = $logger
    }

    public function func($request)
    {
        $logger = $this->logger->withContext(['ipaddr' => $request->getRemoteAddr()]);

        // $logger を使ってログを出す
    }
}

ちがう、サービスがリクエストに依存しているわけではないので ipaddr が適用された Logger を渡すべき。

class HogeService
{
    public function func($logger)
    {
        // $logger には 'ipaddr' が適用されているけどサービスはそんなこと知らねえ
    }
}

ただ $logger をずっと引数として持ち回さなければならない。引数が Logger しかないなら良いけれども、他にもリクエストのコンテキストに属するオブジェクトをたくさん持ち回したくなったときにツラミが増してくる(Logger 以外なにがあるかぱっと思いつかないけど)。

ので、それを見越して、リクエストのコンテキスト、というオブジェクトが渡されるようにしてみる。

class HogeService
{
    public function func($requestContext)
    {
        $logger = $requestContext->getLogger();
    }
}

うーん悪手? サービスが RequestContext というよくわからないものに依存している。


Logger がコンストラクタからシュッと入ってきてそれをそのまま使えられれば良いのだけど、コンテナにリクエストに基づくモノが入っていないとすると引数で持ち回すしか無い?

あるいはコンテナにリクエストオブジェクトをぶっこんでも良いことにする?

// bootstrap.php っぽいなにか
$builder = new ContainerBuilder();
$builder->addDefinitions($definitions + [
    ServerRequestInterface::class => ServerRequestFactory::fromGlobals(),
]);

$container = $builder->build();

// コンテナにリクエストオブジェクトが入っているので logger に ipaddr が適用されている
$logger = $container->get(Logger::class);

でも PSR-7 だとミドルウェアパイプラインの中でリクエストオブジェクトがクローンされて次々に生成されるわけで、↑だとコンテナに入ったリクエストオブジェクトとコントローラーのアクションに渡ってくるリクエストオブジェクトが異なるものになってしまって違和感。

例えば X-Forwarded-For を見てリクエストが持つ RemoteAddr を本来のものに書き換えるようなミドルウェアを考えると問題がわかりやすい。

それか Logger をミュータブルにしてそういうミドルウェアが Logger に IP アドレスをセットする?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $this->logger->addContext(['ipaddr' => $request->getRemoteAddr()]);
        return $delegate->process($request);
    }

うーん、どんどんゆるふわになっていく・・


コントローラーをディスパッチするときに元のコンテナを拡張した新たなコンテナを作成して、コントローラーの依存はその新たなコンテナによって解決する?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $container = $this->extend($this->container, [
            LoggerInterface::class => function () use ($request) {
                return (new Logger())->withContext([
                    'ipaddr' => $request->getRemoteAddr(),
                ]);
            }
        ]);

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

リクエストのアトリビュートを全部ぶっこむとかでも。

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $request = $request->withAttribute(
            LoggerInterface::class, (new Logger())->withContext([
                'ipaddr' => $request->getRemoteAddr(),
            ])
        );

        $container = $this->extend($this->container, $request->getAttributes());

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

とくに結論はない。