Edited at
PHPDay 11

PSR-11 Container interface

PSR一覧

PSR-5 / PSR-6 / PSR-11 / PSR-12 / PSR-14 / PSR-16

アレだよアレ、えーとほらなんだっけ、依存性注入とかいうやつ。

そんなわけでPSR-11 Container Interfaceが2016/12/13現在レビュー中となっているので翻訳してみます。

このURL、ステータスが変わると一緒に変わってしまって不便なんだけどどうにかならんのだろうか。

なお私は英検で3級を取る程度の程度の能力なのでプルリク待ち。


Container interface

このドキュメントは、依存性注入コンテナのインターフェイスについて解説するよ。

ContainerInterfaceの目的は、フレームワークやライブラリがコンテナから中身のデータ(以降エントリと呼ぶ)をどのように取得するかの扱いを標準化することだよ。

"MUST"とか"MAY"とかの意味合いについてはRFC2119を参照。

このドキュメントにおいて"implementor"はライブラリやフレームワークでContainerInterfaceを実装する人物のことだよ。

"user"はコンテナを使う人だよ。


1. Specification


1.1 Basics

Psr\Container\ContainerInterfaceはget()とhas()の2メソッドを持ってるよ。

get()はエントリ識別子を表す引数がひとつ必須だよ。エントリ識別子はString型でなければならない(MUST)よ。

返り値はmix型で、もし識別子が存在しなければ例外を出すよ。

get()に同じ識別子の呼び出しが来たら、同じ値を返すべき(SHOULD)だよ。

ただし実装者の都合やユーザ設定によって別の値が返る可能性もあるから、ユーザは同じ値が返るという前提に頼ってはいけない(SHOULD NOT)よ。

ContainerInterface::get()の引数はひとつだけだけど、実装では追加の引数を取ってもいい(MAY)よ。

has()はエントリ識別子を表す引数がひとつ必須だよ。

返り値はbool型でなくてはならず(MUST)、引数のコンテナが存在すればtrueを、無ければfalseを返すよ。

has($id)===trueだったからといってget($id)が例外を出さないとは限らないけど、少なくともNotFoundExceptionは出さないよ。


1.2 Exceptions

コンテナがスローする例外はPsr\Container\Exception\ContainerExceptionInterfaceをimplementsしなければならない(MUST)よ。

存在しないIDでget($id)が呼ばれた場合はPsr\Container\Exception\NotFoundExceptionInterfaceを出すべき(SHOULD)だよ。


1.3 Recommended usage

ユーザは、コンテナ自体をオブジェクトに渡してはいけない(SHOULD NOT)よ。

それはサービスロケータであって、一般的には推奨されてないよ。

詳細はメタ文書のセクション4を参照してね。


1.4 Additional feature: Delegate lookup

ここでは、コンテナに追加してもいい(MAY)デリゲートルックアップの追加機能を解説するよ。

デリゲートルックアップとは、複数のコンテナがエントリを共有できるようにする機能だよ。

この機能を実装したコンテナは、他のコンテナからもエントリを探したりできるようになるよ。

この機能を実装したコンテナは相互運用性が高まるから、実装を推奨するよ(RECOMMENDED)。

この機能を実装するコンテナは、

・ContainerInterfaceをimplementsしないといけない(MUST)。

・コンストラクタなりsetterなりでデリゲートコンテナにコンテナを登録する手段を作らないといけない(MUST)。

・デリゲートコンテナもContainerInterfaceをimplementsしないといけない(MUST)。

コンテナがデリゲートコンテナのデリゲートルックアップ機能を使う用にした場合、

・get()は、エントリがデリゲートコンテナ内のどこかにある場合にエントリを返し、無ければ例外を投げないといけない(MUST)。

・has()は、エントリがデリゲートコンテナ内のどこかにある場合にtrueを返さないといけない(MUST)。

・get()したエントリが別のエントリを参照している場合、エントリの参照はコンテナではなくデリゲートコンテナが行う。

重要! getしようとしてるエントリが他のエントリを参照している場合、中身の取得はコンテナ自身ではなくデリゲートコンテナが行うべき(SHOULD)だよ。

ただし例外として、特殊なエントリや、同じコンテナにあるエントリはデリゲートコンテナを使わず直接参照する場合もあるよ。


2. Package

各Interface、Class、Exceptionはpackagistで公開されてるよ(作成されているとは言ってない)。


2.1. Psr\Container\ContainerInterface

namespace Psr\Container;

use Psr\Container\Exception\ContainerExceptionInterface;
use Psr\Container\Exception\NotFoundExceptionInterface;

/**
* コンテナのinterface
*/

interface ContainerInterface
{
/**
* 引数の識別子のエントリが存在すれば、それを返す。
*
* @param string $id エントリ識別子
*
* @throws NotFoundExceptionInterface 指定のエントリがない
* @throws ContainerExceptionInterface エントリ内部で発生した例外
*
* @return mixed エントリ
*/

public function get($id);

/**
* 引数の識別子のエントリが存在すれば、trueを返す。
* has($id)===trueだったからといってget($id)が例外を出さないとは限らないが、少なくともNotFoundExceptionInterfaceは出さない。
*
* @param string $id エントリ識別子
*
* @return boolean エントリが存在すればtrue
*/

public function has($id);
}


2.2. Psr\Container\Exception\ContainerExceptionInterface

namespace Psr\Container\Exception;

/**
* コンテナ内部で発生する例外はこれをimplementsする。
*/

interface ContainerExceptionInterface
{
}


2.3. Psr\Container\Exception\NotFoundExceptionInterface

namespace Psr\Container\Exception;

/**
* エントリがなかったときに出す。
* これ普通にclassでええんちゃう?
*/

interface NotFoundExceptionInterface extends ContainerExceptionInterface
{
}


Container Meta Document


1. Introduction

このドキュメントは、コンテナPSRを作るに至った経緯と理由を解説するよ。


2. Why bother?

既に多種多様なDIコンテナが存在しているけど、それぞれエントリを格納する方法が全然違うよ。

・コールバック(Pimple, Laravel, ...)

・配列やYAMLやXMLなどの設定ファイル(Symfony, ZF, ...)

・ファクトリメソッド

・PHP API(PHP-DI, ZF, Symfony, Mouf...)

・自動作成(Laravel, PHP-DI, ...)

・アノテーション(PHP-DI, JMS Bundle...)

・GUI (Mouf...)

・設定ファイルからPHPファイルに変換(Symfony, ZF...)

・エイリアス

・プロキシを使ったLazy Load

DIの実装はたくさんあるけど、どのDIコンテナもやってることは一緒で、設定されたオブジェクト(主にサービス)を拾ってきてるだけだよ。

エントリをコンテナから取得してくる方法を標準化することで、コンテナPSRを使用するフレームワークとライブラリは互換するようになるよ。

これによって、ユーザは自分の好みのコンテナを選んで使えるようになるよ。

また一部フレームワークは、そのコンテナ固有の機能に依存しているよ。

そのような場合、コンテナPSRは、ひとつのコンテナが別のコンテナからもエントリを取得できるようにするデリゲートルックアップ機能を実装するよ。


3. Scope


3.1. Goals

コンテナPSRは、フレームワークとライブラリがコンテナからどうやって取得するかの部分を標準化するよ。

コンテナには「エントリを登録する」「エントリを取得する」ふたつの機能が必要だけど、エントリを登録するのは主にユーザで、フレームワークは主にエントリを取得するほうだよ。

従って、コンテナPSRでは、コンテナからエントリを取得する方だけを規定するよ。


3.2. Non-goals

コンテナにエントリを登録する方は、このPSRの対象外だよ。

コンテナの登録自体はAutoWiringでもいいしコールバックでもいいし構成ファイルでもなんでもいいよ。

エントリの命名規則もPSR対象外だよ。

命名規則は主に2種類があるけど、どちらにも長所や短所があるし、どちらかに統一しないといけないということはないよ。

そのかわりエイリアスを使ってコンテナ間の差異を埋めることはできるよ。

・識別子はクラス名やインターフェイス名。主にAutoWiringなFWで使われる。

・識別子はコモンネーム。変数名的な。主に設定ファイル型のFWで使われる。


4. Recommended usage: Container PSR and the Service Locator

『ユーザは、コンテナ自体をオブジェクトに渡してはいけないよ(SHOULD NOT)』ってのはどういうことかというと、

// これはサービスロケータだからよくない

class BadExample
{
public function __construct(ContainerInterface $container)
{
$this->db = $container->get('db');
}
}

// 直接エントリを渡せばいいよ
class GoodExample
{
public function __construct($db)
{
$this->db = $db;
}
}

BadExampleみたいにコンテナ自体を突っ込むのはやめよう。

・コードの互換性が悪化するよ。コンテナPSRと互換性のあるコンテナを使わないといけなくなるよ。エントリ自体を突っ込むようにすれば、コンテナ自体はなんでも使えるよ。

・開発者にエントリ名"db"を強制してるから、他のパッケージなどと競合する可能性があるよ。

・テストが難しくなっちゃう。

・BadExampleにはエントリ"db"が必要であることが明示されておらず、依存関係が隠されてしまう。

ContainerInterfaceは主にフレームワーク開発者が使うものだから、フレームワークを使うユーザは使う必要なくて、直接コンテナを使えばいいよ。

Whether using the Container PSR into your code is considered a good practice or not boils down to knowing if the objects you are retrieving are dependencies of the object referencing the container or not.

class RouterExample

{

/**
* コンストラクタ
* @param ContainerInterface コンテナ
*/

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* ルーティングする
* @param $_REQUEST
*/

public function getRoute($request)
{
// コントローラ名をURLから取得
$controllerName = $this->getContainerEntry($request->getUrl());
// コントローラエントリを取得
// コントローラはルータに依存していないので正しい実装
$controller = $this->container->get($controllerName);
// ...
}
}

この例では、ルータはURLからコントローラ名を取り出し、コンテナからコントローラエントリを取得してるよ。

コントローラはルータに依存してないから正当な使い方だよ。

大まかには、エントリ名を計算で求めていて、外から変更させられるような場合は、その使い方は正しいよ。

例外として、インスタンスの作成だけを目的としたサービスロケータとして使うのはOKだよ。

各ファクトリークラスはファクトリ用のinterfaceをimplementsしないといけないよ。

// この使い方はOKらしい

interface FactoryInterface
{
public function newInstance();
}

class ExampleFactory implements FactoryInterface
{
protected $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

public function newInstance()
{
return new Example($this->container->get('db'));
}
}


5. History

コンテナPSRがPHP-FIGに提出される前に、コンテナPSRへの足がかりとしてcontainer-interopというContainerInterfaceが存在したよ。

以下、時々container-interopへの言及が出てくるよ。


6. Interface name

名前空間は他のPSRと揃えたけど、インターフェイス名ContainerInterfaceはcontainer-interop時代から変わってないよ。

この名前については徹底的に議論したうえで投票で決定したよ。

ContainerInterface: +8

ProviderInterface: +2

LocatorInterface: 0

ReadableContainerInterface: -5

ServiceLocatorInterface: -6

ObjectFactory: -6

ObjectStore: -8

ConsumerInterface: -9


7. Interface methods

既存の各コンテナを統計的分析した後で、インターフェイスに載せるメソッドを選択したよ。

分析結果は以下のとおり。

・全てのコンテナは、IDでエントリを取得するメソッドを持っていた。

・ほとんどはそのメソッド名がget()だった。

・全てのget()メソッドで、第一引数はString型だった。

・いくつかのコンテナはオプションの第二引数を持っているが、コンテナによって中身が異なっている。

・ほとんどのコンテナは、そのIDのエントリが存在するかをチェックするメソッドを持っている。

・そのメソッド名でいちばん多かったのはhas()。

・全てのhas()は、引数がコンテナIDひとつだけだった。

・get()でエントリが見つからない場合、ほとんどのコンテナはnullを返すのではなく例外を出した。

・ほとんどのコンテナはArrayAccessを実装していない。

エントリを登録するメソッドを含めるかについては最初に議論したけど、このようなメソッドはPSRの対象外なのでContainerInterfaceには含めないことにしたよ。

結果として、ContainerInterfaceには以下の2メソッドだけが残ったよ。

・get():引数のエントリを返す。エントリが見つからないときは例外をスローする。

・has():引数のエントリが存在するか否かを返す。


7.1. Number of parameters in get() method

ContainerInterface::get()は引数が一つしかないよ。

第二引数のある既存コンテナとは一見互換性がないけど、第二引数はオプションにしておくと普通に互換するから問題ないよ。

class HOGE implements ContainerInterface{

public function get($id, array $options = []){
// エラーにはならない
}
}


7.2. Type of the $id parameter

get()とhas()の引数$idの型は、調査した全てのコンテナで文字列型だったよ。

しかし、Objectなり他の型を使うようにすれば、コンテナはより高度なAPIを提供できるんじゃないかという可能性が議論されたよ。

例としては、コンテナをBuilderパターンとして使うとか。

この場合引数$idは、インスタンスの作成方法を記述するBuilderオブジェクトになるよ。

でも議論の結果、これらはコンテナの仕事の範囲を超えているから却下となったよ。


8. Delegate lookup feature


8.1. Purpose of the delegate lookup feature

ひとつのアプリケーションで複数のコンテナを並べて使用する場合はContainerInterfaceで十分だよ。

例えばAcclimateCompositeContainerは以下のようになってるよ。

しかしこれでは、Container1はContainer2の存在を認識できないよ。

Container1のエントリがContainer2のエントリを参照したり、その逆ができたりすると便利だよね。

下記画像では、Container1のエントリ1がContainer2のエントリ3を参照しているよ。


8.2. Chosen Approach

この機能はデリゲートルックアップと呼ばれ、この機能を実装する外側のコンテナはデリゲートコンテナと呼ばれるよ。

・中のコンテナはContainerInterfaceをimplementsする必要があるよ。

・デリゲートコンテナもContainerInterfaceをimplementsする必要があるよ。

・コンストラクタなりsetterなりでデリゲートコンテナにコンテナを登録する方法を提供する必要があるよ。

デリゲートコンテナは、

・get()は中のコンテナにエントリが存在する場合にエントリを返す。無ければNotFoundExceptionInterfaceを出す。

・has()は中のコンテナにエントリが存在する場合にtrueを返す。無ければfalseを返す。

重要! getしようとしてるエントリが他のエントリを参照している場合、そのエントリの取得はコンテナ自身ではなくデリゲートコンテナが行うべき(SHOULD)だよ。

ただし例外として、特殊なエントリや、同じコンテナにあるエントリはデリゲートコンテナを通さず直接参照してもいいよ。


8.3. Typical usage

デリゲートコンテナは、普通はコンポジットコンテナになるよ。

コンポジットコンテナというのは、中にコンテナが複数入っているコンテナのことだよ。

コンポジットコンテナにget()すると、中にあるコンテナ全てから順繰りにエントリを探し出して返すよ。

返したエントリが別のエントリを参照している場合は、コンポジットコンテナ内の他のコンテナにも探しに行くよ。

コンポジットコンテナにコンテナを登録する順序は重要だよ。

コンポジットコンテナに先に登録したコンテナは、後から登録したコンテナのエントリを上書きしてしまうよ。

上記例では、コンテナ1にはエントリ「entityManager」が、コンテナ2にはエントリ「myController」と「entityManager」が登録されてるよ。

デリゲートルックアップ機能がなかった場合、「myController」エントリを要求すると、普通にコンテナ2のエントリ「myController」と「entityManager」が返ってくるよ。

しかしデリゲートルックアップ機能を使うと、「myController」エントリを要求した場合の挙動は以下のようになるよ。

・コンポジットコンテナはコンテナ1に「myController」が存在するか確認する。答えはNo。

・コンポジットコンテナはコンテナ2に「myController」が存在するか確認する。答えはYes。

・コンポジットコンテナはコンテナ2から「myController」をget()する。

・コンテナ2は「myController」が「entityManager」に依存してることに気付く。

・コンテナ2は「entityManager」の取得をコンポジットコンテナに依頼する。

・コンポジットコンテナはコンテナ1に「entityManager」が存在するか確認する。答えはYes。

・コンポジットコンテナはコンテナ1から「entityManager」をget()する。

最終的に、コンテナ2のエントリ「myController」とコンテナ1のエントリ「entityManager」が取得されるよ。


8.4. Alternative: the fallback strategy

この問題の代替案のひとつは、コンテナはまず自分の中でエントリを探し、それで存在しなかったときに初めてコンポジットコンテナに捜索を依頼する、という方法だよ。

この方針ではデリゲートコンテナはフォールバック用途として扱われるよ。

これは@moufmoufのブログで提案され、こことかここで議論されてるよ。

この戦略の問題点は、

・無限ループになっちゃう可能性があるよ。

・デリゲートコンテナのエントリでローカルコンテナのエントリを上書きできないよ。


8.5. Alternative: force implementing an interface

次の代替案として、ParentAwareContainerInterfaceインターフェイスという提案が行われたよ。

setParentContainer()でデリゲートルックアップ用のデリゲートコンテナを指定するという方法だよ。

interface ParentAwareContainerInterface extends ReadableContainerInterface {

/**
* 親コンテナを設定する。
* このコンテナは依存解消に親コンテナを使うようになる。
*
* @param ContainerInterface $container
*/

public function setParentContainer(ContainerInterface $container);
}

@Ocramius問題点を指摘したよ。

setterは実装にまで踏み込んでしまっているから、コンテナのやるべき仕事じゃない。

次に@mnapoli発言し、参加者全員がこの提案を放棄するまで議論は続いたよ。

長所としては、デリゲートコンテナ/コンポジットコンテナの登録を自身に任せることができるよ。

$containerA = new ContainerA();

$containerB = new ContainerB();
$compositeContainer = new CompositeContainer([$containerA, $containerB]);

// setParentContainerの呼び出しをCompositeContainerに任せられるから、使う側は気にする必要がない
class CompositeContainer {
public function __construct($containers) {
foreach ($containers as $container) {
if ($container instanceof ParentAwareContainerInterface) {
$container->setParentContainer($this);
}
}
}
}

短所はここらへんで色々議論されてるよ。

interfaceにsetterを強制するのは、コンストラクタ引数の指定と同様に、基本的に悪い考えだよ。

デリゲートコンテナを各コンテナにどのように登録するかは実装に任せるべきだよ。


8.6 Alternative: no exception case for delegate lookups

元々デリゲートルックアップの項目は


重要! getしようとしてるエントリが他のエントリを参照している場合、そのエントリの取得はコンテナ自体ではなくデリゲートコンテナが行わなけれなならない(MUST)。


と書かれていたけど、後で書き換えられたよ。


重要! getしようとしてるエントリが他のエントリを参照している場合、そのエントリの取得はコンテナ自身ではなくデリゲートコンテナが行うべき(SHOULD)だよ。

ただし例外として、特殊なエントリや、同じコンテナにあるエントリはデリゲートコンテナを通さず直接参照してもいいよ。


互いに依存しているようなサービスがある場合に、取り出すエントリ間の関係を壊さないように、例外を許可するようにしたよ。


8.7. Alternative: having one of the containers act as the composite container

現実的なシナリオとしては、既にSymfony2やZF2などの大きなフレームワークが既に動いており、そこのコンテナに別のDIコンテナを追加したいって場合がほとんどだと思うよ。

既存のフレームワークは大抵、独自のDIコンテナを使ってコントローラのインスタンスを作ってるよ。

コンテナPSRが早くACCEPTされて普及しないと、独自コンテナの代わりにコンポジットコンテナを使ってもフレームワークはそれを認識してくれないよ。

現実路線として、@mnapoli@moufmoufSymfony2Silexの既存のDIコンテナを拡張してコンポジットコンテナにしたよ。

これはコンテナPSRをサポートしていないフレームワークでDIコンテナを動作させるための一時的なものであって、コンテナをコンポジットコンテナとして動かすことはデリゲートルックアップの機能ではないよ。


9. Implementations

以下のプロジェクトでは既にcontainer-interopが実装されてるから、ACCEPTされ次第コンテナPSRに切り替わる予定だよ。


Projects implementing ContainerInterface

Acclimate

Aura.DI

dcp-di

Mouf

Njasm Container

PHP-DI

PimpleInterop

XStatic


Projects implementing the delegate lookup feature

Aura.DI

Mouf

PHP-DI

PimpleInterop


Middlewares implementing ContainerInterface

Alias-Container

Prefixer-Container


Projects using ContainerInterface

Slim Framework

interop.silex.di

Woohoo Labs. API Framework

Invoker


10. People


10.1 Editors

Matthieu Napoli

David Négrier


10.2 Sponsors

Paul M. Jones (Coordinator)

Jeremy Lindblom


10.3 Contributors

Alexandru Pătrănescu

Amy Stephen

Ben Peachey

David Négrier

Don Gilbert

Jason Judge

Jeremy Lindblom

Marco Pivetta

Matthieu Napoli

Nelson J Morais

Paul M. Jones

Phil Sturgeon

Stephan Hochdörfer

Taylor Otwell


11. Relevant links

GoogleグループのコンテナPSRスレッド

Container-interop

ディスカッション一覧

名称の投票

デリゲートルックアップのオリジナル記事


感想

もふもふ

これだけ長々と色々書かれているわりに、実際の中身はget()とhas()だけだった。

あとデリゲートルックアップ|デリゲートコンテナのあたり、ソースの意味は判らないこともないんだけど英語の意味がわからなかった。