こんにちは。よっしーです(^^)
今日は、PHP-DIにおけるPHPの定義ファイルを用いたインジェクションの定義についてご紹介します。
背景
PHP-DIに触れる機会がありましたので、PHP-DIにおけるインジェクションの定義について備忘として残しました。
詳細は下記の公式サイトをご覧ください。
はじめに
オートワイヤリングと属性の機能に加えて、PHPの設定形式を使用してインジェクション(依存性の注入)を定義することができます。
その設定を配列として登録できます:
$container = new DI\Container([
// ここに定義を記述
]);
または、コンテナビルダークラスを使用して次のようにも登録できます:
// ...
$containerBuilder->addDefinitions([
// ここに定義を記述
]);
// ...
あるいは、配列を返すファイルに設定を記述し、次のようにファイルを使用して登録することもできます:
// config.php ファイルに定義を記述
return [
// ここに定義を記述
];
$containerBuilder->addDefinitions('config.php');
遅延読み込みについての注意点
PHP-DIは、記述した定義を読み込み、それらをオブジェクトを作成する手順のように使用します。
ただし、これらのオブジェクトは、コンテナから要求された場合、例えば$container->get(…)を通じて要求された場合や、別のオブジェクトにインジェクションされる必要がある場合にのみ作成されます。つまり、大量の定義を持っていても、PHP-DIは要求されるまですべてのオブジェクトを作成しません。
この動作の唯一の例外は、オブジェクトを値として定義する場合ですが、これは推奨されません(これについては「値」セクションで説明されています)。
構文
PHP-DIの定義は、PHPで記述されたDSL(特定ドメイン言語)であり、ヘルパー関数に基づいています。
このページに示されているすべての例は、PHP 7.0 互換の構文を使用しています。次の機能を使用することをお勧めします:
PHP 5.5の ::class マジック定数:
use Psr\Log\LoggerInterface;
use Monolog\Logger;
return [
LoggerInterface::class => DI\create(Logger::class)
];
PHP 5.6の関数インポート:
use function DI\create;
use function DI\get;
return [
'Foo' => create()
->constructor(get('Bar')),
];
注意: ヘルパー関数(例えば DI\create())は名前空間付きの関数であり、クラスではありません。new(例: new DI\create())を使用しないでください。そうすると致命的なエラー “Class ‘DI\create’ not found” が発生します。
定義の種類
この定義形式は、すべての中で最も強力です。定義できるエントリのいくつかの種類があります:
- 値(values)
- ファクトリ(factories)
- オブジェクト(objects)
- オートワイヤード オブジェクト(autowired objects)
- エイリアス(aliases)
- 環境変数(environment variables)
- 文字列式(string expressions)
- 配列(arrays)
- ワイルドカード(wildcards)
値(Values)
値(Symfonyではパラメータとも呼ばれます)は単純なPHPの値です。
return [
'database.host' => 'localhost',
'database.port' => 5000,
'report.recipients' => [
'bob@example.com',
'alice@example.com',
],
];
直接オブジェクトエントリを作成することで、オブジェクトエントリも定義できます:
return [
'Foo' => new Foo(),
];
ただし、これは推奨されません。このようなオブジェクトは、使用されない場合でもすべてのPHPリクエストのたびに作成されます(このセクションの先頭で説明したように、遅延読み込みは行われません)。また、これによってコンテナのコンパイルが妨げられます。
代わりに、以下で説明する方法のいずれかを使用するべきです。
ファクトリ(Factories)
ファクトリは、インスタンスを返すPHPの呼び出し可能な関数です。ファクトリを使用すると、オブジェクトを遅延的に簡単に定義できます。つまり、各オブジェクトは実際に必要な時にのみ作成されます(呼び出し可能な関数は、実際に必要な時に呼び出されるため)。
他の定義と同様に、ファクトリも1回呼び出され、ファクトリを解決するたびに同じ結果が返されます。
ファクトリはDI\factory()ヘルパーを使用して定義することもできますが、クロージャをショートカットとして使用することも可能です:
use Psr\Container\ContainerInterface;
use function DI\factory;
return [
'Foo' => function (ContainerInterface $c) {
return new Foo($c->get('db.host'));
},
// 同じ内容
'Foo' => DI\factory(function (ContainerInterface $c) {
return new Foo($c->get('db.host'));
}),
];
他のサービスは、コンテナに登録されているかオートワイヤリングが有効になっている限り、型ヒントを使用してインジェクションすることができます:
return [
'LoggerInterface' => DI\create('MyLogger'),
'Foo' => function (LoggerInterface $logger) {
return new Foo($logger);
},
];
DI\factory()ヘルパーは、自動的に型ヒントを使用してインジェクションできない値のようなエントリを指定するためのparameter()メソッドを提供しています。
return [
'Database' => DI\factory(function ($host) {...})
->parameter('host', DI\get('db.host')),
];
これは、最初の例でコンテナ自体をインジェクトすることでも実現できます。コンテナをインジェクトする場合、実装DI\ContainerではなくインターフェースPsr\Container\ContainerInterfaceに対して型ヒントを付けるべきです。
ファクトリは任意のPHP呼び出し可能なものであり、そのためクラスメソッドでもファクトリとして使用できます:
class FooFactory
{
public function create(Bar $bar)
{
return new Foo($bar);
}
}
FooFactoryを定義時に積極的に読み込むことを選択することができますが(Foo::class
の例)、この場合、ファクトリは使われない場合でもすべてのリクエストごとに新しいインスタンスが作成されます(new FooFactory)。さらに、この方法ではファクトリへの依存関係を渡すのが難しくなります。
おすすめの方法は、コンテナにファクトリを作成させることです:
return [
Foo::class => DI\factory([FooFactory::class, 'create']),
// または代替構文:
Foo::class => DI\factory('Namespace\To\FooFactory::create'),
];
上記の設定は、次のコードと同等です:
$factory = $container->get(FooFactory::class);
return $factory->create(...);
もしファクトリが静的メソッドである場合、同じように簡単にできます:
class FooFactory
{
public static function create()
{
return ...
}
}
return [
Foo::class => DI\factory([FooFactory::class, 'create']),
];
注意点:
factory([FooFactory::class, 'build'])
:もしbuild()
が静的メソッドなら、FooFactory::build()
が静的に呼び出されます(静的メソッドはオブジェクトか値を返す責任があります)。- 配列内に任意のコンテナエントリ名を設定できます。例:
DI\factory(['foo_bar_baz', 'build'])
(または代替構文:DI\factory('foo_bar_baz::build')
)。これにより、foo_bar_baz
およびその依存関係を他のオブジェクトと同じように設定できます。 - ファクトリは任意のPHP呼び出し可能なものとして使用できるため、呼び出し可能なオブジェクト(Invokable objects)も使用できます。例:
DI\factory(InvokableFooFactory::class)
(または代替構文:DI\factory('invokable_foo_factory')
、これがコンテナで定義されている場合)。 - すべてのクロージャは、ネストされた他の定義(create()、env()など)に入れ子になっていても、PHP-DIによってファクトリとして認識されます(ネストされた定義については「定義のネスト」セクションを参照)。
要求されたエントリの名前を取得する
異なるエントリを作成するために同じファクトリを再利用したい場合、現在解決中のエントリの名前を取得したいかもしれません。DI\Factory\RequestedEntryオブジェクトを型ヒントを使用してインジェクションすることで、これを行うことができます:
use DI\Factory\RequestedEntry;
return [
'Foo' => function (RequestedEntry $entry) {
// $entry->getName() に要求された名前が含まれています
$class = $entry->getName();
return new $class();
},
];
RequestedEntryは型ヒントを使用してインジェクションされるため、コンテナまたは他のサービスのインジェクションと組み合わせることができます。ファクトリ引数の順序は重要ではありません。
デコレーション
複数の定義ファイルを使用するシステムで、以前に定義されたエントリをデコレートして上書きすることができます:
return [
// 別のファイルで以前に定義されたエントリをデコレート
'WebserviceApi' => DI\decorate(function ($previous, ContainerInterface $c) {
return new CachedApi($previous, $c->get('cache'));
}),
];
この機能について詳しくは、定義のオーバーライドガイドを読んでください。
オブジェクト(Objects)
ファクトリを使用してオブジェクトを作成することは非常に強力ですが(PHPを使用して何でも行えるため)、DI\create()ヘルパーを使用すると、シンプルな場合もあります。いくつかの例を挙げます:
return [
// オブジェクトを作成するためにLoggerクラスをインスタンス化
'Logger' => DI\create(),
// インターフェースを実装クラスにマッピング
'LoggerInterface' => DI\create('MyLogger'),
// エントリに任意の名前を使用
'logger.for.backend' => DI\create('Logger'),
];
DI\create()ヘルパーを使用すると、コンストラクタパラメータを定義できます:
return [
'Logger' => DI\create()
->constructor('app.log', DI\get('log.level'), DI\get('FileWriter')),
];
また、セッター/メソッドインジェクションも定義できます:
return [
'Database' => DI\create()
->method('setLogger', DI\get('Logger')),
// メソッドを2回呼び出すことも可能
'Logger' => DI\create()
->method('addBackend', 'file')
->method('addBackend', 'syslog'),
];
さらに、プロパティインジェクションも行えます:
return [
'Foo' => DI\create()
->property('bar', DI\get('Bar')),
];
それぞれのエントリは1度解決され、同じインスタンスが使用される場所でどこでもインジェクションされます。他の定義と同様に、create()ヘルパーで定義されたオブジェクトは、必要なときにのみ作成されます(遅延読み込み)。
オートワイヤード オブジェクト(Autowired objects)
オートワイヤリングを有効にしている場合、DI\autowire()ヘルパーを使用してオブジェクトがどのようにオートワイヤリングされるかをカスタマイズできます。
DI\autowire()はDI\create()と同じように振る舞いますが、オブジェクトのビルド方法をゼロから設定する代わりに、オートワイヤリングから必要な部分のみをオーバーライドします。
return [
// オプションを指定しない場合は、設定ファイルに書く必要はありません
'MyLogger' => DI\autowire(),
// インターフェースを実装クラスにマッピング(MyLoggerクラスをオートワイヤリング)
'LoggerInterface' => DI\autowire('MyLogger'),
// 任意の名前をエントリに使用
'logger.for.backend' => DI\autowire('MyLogger'),
];
DI\create()と同様に、コンストラクタパラメータを明示的に設定できます:
return [
'Logger' => DI\autowire()
->constructor('app.log', DI\get('log.level'), DI\get('FileWriter')),
];
セッター/メソッドインジェクションも行えます:
return [
'Database' => DI\autowire()
->method('setLogger', DI\get('Logger')),
];
プロパティインジェクションも行えます:
return [
'Foo' => DI\autowire()
->property('bar', DI\get('Bar')),
];
また、特定のパラメータのみを定義することもできます。これにより、オートワイヤリングの際に型ヒントを使用して推測できなかったパラメータを定義できます。
return [
'Logger' => DI\autowire()
// $filenameパラメータを設定
->constructorParameter('filename', 'app.log')
// $handlerパラメータを設定
->methodParameter('setHandler', 'handler', DI\get('SyslogHandler')),
];
PHP 8以降では、名前付き引数も使用できます:
return [
'Logger' => DI\autowire()
// $filenameパラメータを設定
->constructor(
filename: 'app.log'
)
// $handlerパラメータを設定
->method('setHandler', handler: DI\get('SyslogHandler')),
];
エイリアス(Aliases)
DI\get()ヘルパーを使用して、エントリを別のエントリにエイリアスすることができます:
return [
'doctrine.entity_manager' => DI\get('Doctrine\ORM\EntityManager'),
];
エイリアスを使用して、クラスまたはインターフェースを要求/インジェクションする際に同じオブジェクトを取得できるようにマッピングすることができます。
return [
// インターフェースを実装クラスにマッピング(それ以外で定義されていない場合、MyLoggerクラスをオートワイヤリング)
'LoggerInterface' => DI\get('MyLogger'),
];
環境変数(Environment variables)
DI\env()ヘルパーを使用して、環境変数の値を取得できます:
return [
'db1.url' => DI\env('DATABASE_URL'),
// デフォルト値を指定
'db2.url' => DI\env('DATABASE_URL', 'postgresql://user:pass@localhost/db'),
// デフォルト値に別のエントリを指定
'db2.host' => DI\env('DATABASE_HOST', DI\get('db.host')),
];
文字列式(String expressions)
DI\string()ヘルパーを使用して、文字列エントリを連結できます:
return [
'path.tmp' => '/tmp',
'log.file' => DI\string('{path.tmp}/app.log'),
];
配列(Arrays)
エントリは、単純な値または他のエントリを含む配列にすることができます:
return [
'report.recipients' => [
'bob@example.com',
'alice@example.com',
],
'log.handlers' => [
DI\get('Monolog\Handler\StreamHandler'),
DI\get('Monolog\Handler\EmailHandler'),
],
];
複数の定義ファイルを持つ場合、配列には追加の機能があります。詳細は定義のオーバーライドドキュメントを参照してください。
ワイルドカード(Wildcards)
ワイルドカードを使用して、複数のエントリを一括して定義できます。インターフェースを実装にバインドするのに非常に便利です:
return [
'Blog\Domain\*RepositoryInterface' => DI\create('Blog\Architecture\*DoctrineRepository'),
];
この例では、ワイルドカードはBlog\Domain\UserRepositoryInterfaceに一致し、それをBlog\Architecture\UserDoctrineRepositoryにマップします。
注意:
- ワイルドカードは名前空間を跨って一致しません。
- ワイルドカードなしの完全一致(*を含まない一致)は、ワイルドカードを含む一致よりも常に選択されます(まず完全一致を探し、次にワイルドカードを検索します)。
- “競合”(ワイルドカードを使用した2つの異なる一致)の場合、最初の一致が優先されます。
定義のネスト
不要なエントリでコンテナを汚染しないように、定義を別の定義の中にネストすることができます。例えば:
return [
'Foo' => DI\create()
->constructor(DI\string('{root_directory}/test.json'), DI\create('Bar')),
];
ただし、クロージャは「ファクトリ」定義と同等です。そのため、クロージャは常にファクトリとして解釈されます。たとえば、ファクトリ以外の目的で無名関数を使用する場合は、それらをDI\value()ヘルパーでラップする必要があります:
return [
'router' => DI\create(Router::class)
->method('setErrorHandler', DI\value(function () {
...
})),
];
もちろん、これは設定ファイル内のクロージャにのみ適用されます。
コンテナに直接定義を設定する方法
配列内でエントリを定義する以外にも、以下に示すようにコンテナに直接エントリを設定することもできます。
$container->set('db.host', 'localhost');
$container->set('My\Class', \DI\create()
->constructor('some raw value'));
ただし、コンテナ内でContainer::set()を使用して定義を設定することはお勧めしません。なぜなら、Container::set()で設定されたすべてのエントリはコンパイルされないためです。
また、コンパイル済みコンテナを使用する場合、実行中にコンテナに定義を追加することはできません。この場合、上記の例で説明されているように、定義を配列またはファイルに配置してください。
これは、定義がキャッシュされるためです(値はキャッシュされません)。動的に定義を設定する場合、それがキャッシュされるため、非常に奇妙なバグが発生する可能性があります(動的な定義はもちろんキャッシュされるべきではないため、動的であるという理由から)。
この場合、この記事の前述の方法で定義を配列またはファイルに配置してください。
エラーとなる例:
$builder = new ContainerBuilder();
$builder->setDefinitionCache(new ApcCache());
$container = $builder->build();
// 動作する:値を設定できます
$container->set('foo', 'hello');
$container->set('bar', new MyClass());
// エラー:キャッシュを使用する場合、->set()を使用して定義を設定できません
$container->set('foo', DI\create('MyClass'));
このコードは、コンパイル済みコンテナを使用している場合の動作を示しています。コンパイル済みコンテナを使用する場合、コンパイル済みコンテナがキャッシュを使用するため、コンテナ内で動的に定義を設定することはできません。そのため、コンテナに定義を追加する際には、コンテナがキャッシュを使用しないように注意する必要があります。
おわりに
今日は、PHP-DIにおけるPHPの定義ファイルを用いたインジェクションの定義についてご紹介しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント