思ったより大変だった。
Slim3
のデフォルトコンテナはPimple
。非常にシンプルで機能を削ぎ落としたコンテナと言われている。ただし、Slim3
はContainerInterface
対応のコンテナなら使える、ということになっている。
そう言えばLeague/Container
も、ContainerInterface
に対応している。だったら簡単に交換できるかなと思って、特にメリットが何かも考えずにやってみた。
ちなみにContainerInterfaceというは、ちょっと前からあるコンテナの使い方を統一するためのインターフェース。これをベースにPSR-11が議論されている。
Adapter作成
動かす方針としては、
League/Container
がPimple
のように動くアダプターを作成することで対応した。
ContainerInterface
に対応
ContainerInterface
を組み込むのは簡単。何しろget
とhas
の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();
}
これだけかい。すごい。