Edited at

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

More than 3 years have 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();
}


これだけかい。すごい。