Slim3にLeague/Containerを導入してみた話(とPSR-11)

  • 1
    Like
  • 0
    Comment
More than 1 year has passed since last update.

思ったより大変だった。

Slim3のデフォルトコンテナはPimple。非常にシンプルで機能を削ぎ落としたコンテナと言われている。ただし、Slim3ContainerInterface対応のコンテナなら使える、ということになっている。

そう言えばLeague/Containerも、ContainerInterfaceに対応している。だったら簡単に交換できるかなと思って、特にメリットが何かも考えずにやってみた。

ちなみにContainerInterfaceというは、ちょっと前からあるコンテナの使い方を統一するためのインターフェース。これをベースにPSR-11が議論されている。

Adapter作成

動かす方針としては、
League/ContainerPimpleのように動くアダプターを作成することで対応した。

ContainerInterfaceに対応

ContainerInterfaceを組み込むのは簡単。何しろgethasの2つしかメソッドがない。こんな感じ。

class Container implements ContainerInterface, \ArrayAccess {
    /**
     * @var \League\Container\Container
     */
    private $container;

    public function __construct($container) {
        $this->container = $container;
    }
    public function get($id) {
        return $this->container->get($id);
    }
    public function has($id) {
        return $this->container->has($id);
    }
}

早速使ってみる。が、

やっぱり動かなかった。

ArrayAccessに対応

原因は、Slim3内部で使われるサービスを設定するコードにあった。要するにSlim3のサービス・プロバイダーがPimple前提になっていた。

嫌な予感…

そのサービス・プロバイダーは、こんなコード。

$container['environment'] = function () {
    return new Environment($_SERVER);
};

つまり、Pimpleとして動かすには、ArrayAccessも実装する必要があったのである。

ちなみにArrayAccessというのは、PHPのオブジェクトを配列のように使えるようにするためのインターフェース。

class Container implements ContainerInterface, \ArrayAccess
{
    // 以下、ArrayAccess用のコード 

    public function offsetExists($offset) {
        return $this->container->has($offset);
    }
    public function offsetGet($offset) {
        return $this->container->get($offset);
    }
    public function offsetSet($offset, $value) {
        $this->container->add($offset, $value);
    }
    public function offsetUnset($offset) {
        throw new \BadMethodCallException;
    }
}

それでも、動かない。

動かすために

仕方ないので、コードを追いかけて、問題を探す。幾つかの挙動を変えることで対応することが出来た。

設定箇所:offsetSet

Slim\DefaultServiceProviderにあるregisterというメソッドで設定を行っている。コードとしては、こんな感じ。

$container['name'] = function($c){ /* オブジェクト返す */ };

配列として設定しようとすると、ArrayAccess::offsetSetが呼ばれる、という風にPHPは動く。

  • ポイントは「配列で設定すると「Singleton」として扱う」という前提だったこと。

確かに配列に設定するというメタファーなら、値は共有される気がする。

  • もう一つのポイントは「ファクトリ用のコーラブルの引数にコンテナが入ってくる」こと。

この2点を考慮して書き直すと、

    public function offsetSet($offset, $value)
    {
        $builder = $this->container->add($offset, $value, true);
        if ($builder && is_callable($value)) {
            $builder->withArgument($this);
        }
    }

となった。

addメソッドの第3引数にtrueを設定することでSingletonとして設定している。さらにコーラブルなら、引数に自分自身を設定している。

クラスの実体化:has

League/Containerの特徴は、コンテナ自身でクラスをnewできること。設定によっては、依存性を自動解決して注入することも可能である。

ところが、ファクトリなど設定されてないクラス名にたいしてhasで問い合わせるとfalseが返ってくる。

たしかに。設定されてないのだから、間違ってない。

    public function has($id)
    {
        if ($this->container->has($id)) {
            return true;
        }
        if (is_string($id) && class_exists($id)) {
            return true;
        }
        return false;
    }

として動かした。

Slim3用の設定:settings

まだ、動かん。

見れば、Slim3自体でもPimpleを継承してゴニョゴニョしている。その中で、デフォルト設定がある。これがないので動かなかった。ので、こんなコードを追加。

$container = new Container(new \League\Container\Container);
$container['settings'] = [
    'httpVersion'                       => '1.1',
    'responseChunkSize'                 => 4096,
    'outputBuffering'                   => 'append',
    'determineRouteBeforeAppMiddleware' => false,
    'displayErrorDetails'               => false,
];

確か、以上の修正で動かせた。

動かしてみた感想

学んだことは、コンテナ取り替えるの苦しい

コンテナを使う場合には標準がある。一方、設定する場合には標準がない。これを合わせこむのは面倒。今回のPimpleもLeague/Containerも比較的単純な方のコンテナ(だと思う)ので、複雑なコンテナ使ったら、大変だろうなと。

サービス・プロバイダー自作

League/Containerを使うと決めた時点で、サービス・プロバイダーも自作するべきだった、のだろうと思う。

ただ、Slim3が進化して必要なサービスが増えた場合、追従するのは大変。やはりSlim3謹製のサービス・プロバイダーを使うメリットは大きい。

PSR-11のServiceProvider

さて、こんな面倒な事を、PSRでは統一しようとしている。

どうやって?と思ってみてみたが、意外と簡単な方法があった。ちなみに、ここで開発(というか議論)が進んでいる様子(https://github.com/container-interop/service-provider

そのインターフェースとは…

interface ServiceProvider
{
    /**
     * @return callable[]
     */
    public function getServices();
}

これだけかい。すごい。