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

思ったより大変だった。

[`Slim3`](http://www.slimframework.com/)のデフォルトコンテナは[`Pimple`](http://pimple.sensiolabs.org/)。非常にシンプルで機能を削ぎ落としたコンテナと言われている。ただし、`Slim3`は`ContainerInterface`対応のコンテナなら使える、ということになっている。

そう言えば[`League/Container`](http://container.thephpleague.com/)も、`ContainerInterface`に対応している。だったら簡単に交換できるかなと思って、**特にメリットが何かも考えず**にやってみた。

> ちなみに[ContainerInterface](https://github.com/container-interop/container-interop)というは、ちょっと前からあるコンテナの使い方を統一するためのインターフェース。これをベースにPSR-11が議論されている。

Adapter作成
----

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

### `ContainerInterface`に対応

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

```php
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`前提になっていた。

> 嫌な予感…

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

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

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

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


```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というメソッドで設定を行っている。コードとしては、こんな感じ。

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

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

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

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

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

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

```php
    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`が返ってくる。

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

```php
    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`を継承してゴニョゴニョしている。その中で、デフォルト設定がある。これがないので動かなかった。ので、こんなコードを追加。

```php
$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](https://github.com/container-interop/service-provider)）

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

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

> これだけかい。すごい。