Practical Symfony #24: ダイナミックなコンフィギュレーショングラマー
Symfonyフレームワークの動作を設定するコンフィギュレーションとその背後の仕組みは、Symfonyのアーキテクチャを支える強力な屋台骨となっています。この仕組みの応用例の1つとして、コンフィギュレーションエントリをダイナミックに定義する仕掛けを見てみます。
通常のコンフィギュレーション
通常の静的なコンフィギュレーションは、バンドル内のDependencyInjectionディレクトリ以下にConfigurationクラスを用意し、そこで定義されたツリー構造に従って読み込まれます。コンフィギュレーションクラスの例は設定の仕様とは等を参照してください。この場合、あらかじめ固定のコンフィギュレーショングラマーがあり、それにもとづいてコンフィギュレーションファイルに設定を記述し、そのファイルの設定を読み込んでサービスコンテナが動作します。
アプリケーション開発の多くの場面ではこのような静的な定義で十分ですが、コンフィギュレーションで記述したい内容が、必ずしも1つのバンドルの内容だけで決まるとは限りません。他のバンドルと疎結合な状態を保ちつつ、協調して機能するようなコンフィギュレーションはどうやって定義するのでしょうか。
Securityバンドルのエンティティプロバイダの例
他のバンドルと協調動作するコンフィギュレーションの例として、Securityバンドルのエンティティプロバイダの例を紹介します。Symfonyで認証ユーザーのプロバイダーとしてDoctrineのエンティティクラスを使う場合、次のようにsecurity.ymlのprovidersに定義します。
providers: my_entity_provider: entity: class: SecurityBundle:User property: username manager_name: ~
この定義でentityというのがプロバイダの種類を表しており、他にはmemoryがあります。entityの場合はSecurityバンドルの機能とDoctrineの機能が連携して動作しますが、そういったDoctrineを前提とした機能をSecurityバンドルが抱え込んでいるのでしょうか? ちなみに、EntityUserProviderクラスはDoctrineBridgeという連携パッケージにあります。
このダイナミックな定義の流れは、次の図のようになっています。
Securityバンドルにおけるエンティティプロバイダのグラマー定義は、Securityエクステンションが保持しているプロバイダーファクトリーによって決まるようになっています。Securityバンドルには、このファクトリーのためのインターフェイスが定義されています。
SecurityBundle / DependencyInjection / Security / UserProvider / UserProviderFactoryInterface.php
interface UserProviderFactoryInterface { public function create(ContainerBuilder $container, $id, $config); public function getKey(); public function addConfiguration(NodeDefinition $builder); }
エクステンションは、コンフィギュレーションの仕組みが動作する前に読み込まれています。Doctrineバンドルのbuild()メソッドで、Securityエクステンションがロードされている場合は、Securityエクステンションへエンティティプロバイダーファクトリーを登録しています。
DoctrineBundle / DoctrineBundle.php
public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new RegisterEventListenersAndSubscribersPass('doctrine.connections', 'doctrine.dbal.%s_connection.event_manager', 'doctrine'), PassConfig::TYPE_BEFORE_OPTIMIZATION); if ($container->hasExtension('security')) { $container->getExtension('security')->addUserProviderFactory(new EntityFactory('entity', 'doctrine.orm.security.user.provider')); } $container->addCompilerPass(new DoctrineValidationPass('orm')); }
プロバイダーファクトリーとして登録されるEntityFactoryは、コンフィギュレーションノードを追加するメソッド(addConfiguration())を持っています。
Bridge / Doctrine / DependencyInjection / Security / UserProvider / EntityFactory.php
class EntityFactory implements UserProviderFactoryInterface { ... public function addConfiguration(NodeDefinition $node) { $node ->children() ->scalarNode('class')->isRequired()->cannotBeEmpty()->end() ->scalarNode('property')->defaultNull()->end() ->scalarNode('manager_name')->defaultNull()->end() ->end() ; } }
Securityバンドルのグラマー準備段階で、登録されたプロバイダーファクトリーのノード追加メソッドを呼び出しています($factory->addConfiguration())。
SecurityBundle / DependencyInjection / MainConfiguration.php
foreach ($this->userProviderFactories as $factory) { $name = str_replace('-', '_', $factory->getKey()); $factoryNode = $providerNodeBuilder->children()->arrayNode($name)->canBeUnset(); $factory->addConfiguration($factoryNode); }
これにより、security.ymlを読み込む段階では、グラマー定義としてentityという項目がすでに有効になっている状態になります。コンフィギュレーションファイルが読み込まれ、エンティティプロバイダーが有効になります($factory->create())。
SecurityBundle / DependencyInjection / SecurityExtension.php
private function createUserDaoProvider($name, $provider, ContainerBuilder $container, $master = true) { $name = $this->getUserProviderId(strtolower($name)); foreach ($this->userProviderFactories as $factory) { $key = str_replace('-', '_', $factory->getKey()); if (!empty($provider[$key])) { $factory->create($container, $name, $provider[$key]); return $name; } }
(しかし、ここはサービスコンテナのコンパイル段階であって、有効になるといってもエンティティプロバイダーのサービス定義が追加されるのみです。アプリケーションのランタイムまで、実際のオブジェクトはインスタンス化されません)
Bridge / Doctrine / DependencyInjection / Security / UserProvider / EntityFactory.php
class EntityFactory implements UserProviderFactoryInterface { public function create(ContainerBuilder $container, $id, $config) { $container ->setDefinition($id, new DefinitionDecorator($this->providerId)) ->addArgument($config['class']) ->addArgument($config['property']) ->addArgument($config['manager_name']) ; }
まとめ
サービスコンテナと、コンフィギュレーションとエクステンションの動きが見えてくると、Symfonyがどのように作られ動いているのか、把握しやすくなります。Securityバンドルはこの仕組みを最も高度に利用しているバンドルの1つです。ソースを読んでみるとさまざまな発見があります。