symfonyコンポーネントによる真の依存性注入
公開: 2022-03-11高性能PHPフレームワークであるSymfony2は、コンポーネントがDIコンテナーに依存性注入インターフェースを提供する依存性注入コンテナーパターンを使用します。 これにより、各コンポーネントは他の依存関係を気にする必要がなくなります。 'Kernel'クラスは、DIコンテナーを初期化し、それをさまざまなコンポーネントに挿入します。 ただし、これは、DIコンテナをサービスロケータとして使用できることを意味します。
Symfony2にはそのための'ContainerAware'クラスさえあります。 多くの人が、ServiceLocatorはSymfony2のアンチパターンであるという意見を持っています。 個人的には同意しません。 DIに比べて単純なパターンであり、単純なプロジェクトに適しています。 しかし、単一のプロジェクトに組み合わされたServiceLocatorパターンとDI-containerパターンは間違いなくアンチパターンです。
この記事では、ServiceLocatorパターンを実装せずにSymfony2アプリケーションを構築しようとします。 DIコンテナについて知ることができるのは、DIコンテナビルダーだけです。
DIコンテナ
依存性注入パターンでは、DIコンテナーはサービスの依存性を定義し、サービスは注入用のインターフェースのみを提供できます。 依存性注入に関する記事はたくさんありますが、おそらくそれらすべてを読んだことがあるでしょう。 それでは、理論に焦点を当てるのではなく、基本的な考え方を見てみましょう。 DIには次の3つのタイプがあります。
Symfonyでは、インジェクション構造は単純な設定ファイルを使用して定義できます。 これらの3つの注入タイプを構成する方法は次のとおりです。
services: my_service: class: MyClass constructor_injection_service: class: SomeClass1 arguments: ["@my_service"] method_injection_service: class: SomeClass2 calls: - [ setProperty, "@my_service" ] property_injection_service: class: SomeClass3 properties: property: "@my_service"
ブートストラッププロジェクト
基本的なアプリケーション構造を作成しましょう。 その間に、SymfonyDIコンテナコンポーネントをインストールします。
$ mkdir trueDI $ cd trueDI $ composer init $ composer require symfony/dependency-injection $ composer require symfony/config $ composer require symfony/yaml $ mkdir config $ mkdir www $ mkdir src
Composerオートローダーがsrcフォルダーで独自のクラスを検出できるようにするには、composer.jsonファイルに「autoloader」プロパティを追加します。
{ // ... "autoload": { "psr-4": { "": "src/" } } }
そして、コンテナビルダーを作成してコンテナインジェクションを禁止しましょう。
// in src/TrueContainer.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ContainerInterface; class TrueContainer extends ContainerBuilder { public static function buildContainer($rootPath) { $container = new self(); $container->setParameter('app_root', $rootPath); $loader = new YamlFileLoader( $container, new FileLocator($rootPath . '/config') ); $loader->load('services.yml'); $container->compile(); return $container; } public function get( $id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE ) { if (strtolower($id) == 'service_container') { if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior ) { return; } throw new InvalidArgumentException( 'The service definition "service_container" does not exist.' ); } return parent::get($id, $invalidBehavior); } }
ここでは、ConfigコンポーネントとYamlsymfonyコンポーネントを使用します。 詳細については、こちらの公式ドキュメントをご覧ください。 また、念のため、ルートパスパラメータ「app_root」を定義しました。 getメソッドは、親クラスのデフォルトのget動作をオーバーロードし、コンテナーが「service_container」を返さないようにします。
次に、アプリケーションのエントリポイントが必要です。
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__));
これはhttpリクエストを処理するためのものです。 コンソールコマンド、cronタスクなどのエントリポイントを増やすことができます。 各エントリポイントは特定のサービスを受けることになっており、DIコンテナの構造について知っている必要があります。 コンテナからサービスをリクエストできるのはここだけです。 この時点から、DIコンテナ構成ファイルのみを使用してこのアプリケーションを構築しようとします。
HttpKernel
HttpKernel(サービスロケーターの問題があるフレームワークカーネルではありません)が、アプリケーションのWeb部分の基本コンポーネントになります。 典型的なHttpKernelワークフローは次のとおりです。
緑の四角はイベントです。
HttpKernelは、要求および応答オブジェクトにHttpFoundationコンポーネントを使用し、イベントシステムにEventDispatcherコンポーネントを使用します。 DIコンテナ構成ファイルで初期化しても問題ありません。 HttpKernelは、EventDispatcher、ControllerResolver、およびオプションでRequestStack(サブリクエストの場合)サービスで初期化する必要があります。
そのためのコンテナ構成は次のとおりです。
# in config/events.yml services: dispatcher: class: Symfony\Component\EventDispatcher\EventDispatcher
# in config/kernel.yml services: request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] request_stack: class: Symfony\Component\HttpFoundation\RequestStack resolver: class: Symfony\Component\HttpKernel\Controller\ControllerResolver http_kernel: class: Symfony\Component\HttpKernel\HttpKernel arguments: ["@dispatcher", "@resolver", "@request_stack"]
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' }
ご覧のとおり、「factory」プロパティを使用してリクエストサービスを作成します。 HttpKernelサービスは、Requestオブジェクトのみを取得し、Responseオブジェクトを返します。 これはフロントコントローラーで実行できます。
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $HTTPKernel = $container->get('http_kernel'); $request = $container->get('request'); $response = $HTTPKernel->handle($request); $response->send();
または、「factory」プロパティを使用して、構成内のサービスとして応答を定義できます。
# in config/kernel.yml # ... response: class: Symfony\Component\HttpFoundation\Response factory: [ "@http_kernel", handle] arguments: ["@request"]
そして、それをフロントコントローラーに入れます。
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $response = $container->get('response'); $response->send();
コントローラリゾルバサービスは、コントローラを解決するためにリクエストサービスの属性から「_controller」プロパティを取得します。 これらの属性はcontainerconfigで定義できますが、単純な配列の代わりにParameterBagオブジェクトを使用する必要があるため、少し注意が必要です。
# in config/kernel.yml # ... request_attributes: class: \Symfony\Component\HttpFoundation\ParameterBag calls: - [ set, [ _controller, \App\Controller\DefaultController::defaultAction ]] request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] properties: attributes: "@request_attributes" # ...
そして、これがdefaultActionメソッドを持つDefaultControllerクラスです。
// in src/App/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; class DefaultController { function defaultAction() { return new Response("Hello cruel world"); } }
これらすべてが整ったら、実用的なアプリケーションができあがります。
このコントローラーは、サービスにアクセスできないため、まったく役に立ちません。 Symfonyフレームワークでは、この問題は、コントローラーにDIコンテナーを挿入し、それをサービスロケーターとして使用することで解決されます。 私たちはそれをしません。 それでは、コントローラーをサービスとして定義し、その中にリクエストサービスを注入しましょう。 構成は次のとおりです。
# in config/controllers.yml services: controller.default: class: App\Controller\DefaultController arguments: [ "@request"]
# in config/kernel.yml # ... request_attributes: class: \Symfony\Component\HttpFoundation\ParameterBag calls: - [ set, [ _controller, ["@controller.default", defaultAction ]]] request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] properties: attributes: "@request_attributes" # ...
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' }
そしてコントローラーコード:
// in src/App/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class DefaultController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction() { $name = $this->request->get('name'); return new Response("Hello $name"); } }
これで、コントローラーは要求サービスにアクセスできるようになります。 ご覧のとおり、このスキームには循環依存関係があります。 DIコンテナは、作成後、メソッドおよびプロパティの注入前にサービスを共有するため、機能します。 したがって、コントローラーサービスが作成されているとき、要求サービスは既に存在します。
仕組みは次のとおりです。
ただし、これが機能するのは、リクエストサービスが最初に作成されるためです。 フロントコントローラーで応答サービスを取得すると、要求サービスが最初に初期化された依存関係になります。 最初にコントローラーサービスを取得しようとすると、循環依存エラーが発生します。 メソッドまたはプロパティインジェクションを使用して修正できます。
しかし、別の問題があります。 DI-containerは、依存関係を使用して各コントローラーを初期化します。 したがって、必要がない場合でも、既存のすべてのサービスを初期化します。 幸い、コンテナには遅延読み込み機能があります。 Symfony DIコンポーネントは、プロキシクラスに「ocramius/proxy-manager」を使用します。 それらの間にブリッジを設置する必要があります。
$ composer require symfony/proxy-manager-bridge
そして、コンテナ構築段階でそれを定義します。
// in src/TrueContainer.php //... use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; // ... $container = new self(); $container->setProxyInstantiator(new RuntimeInstantiator()); // ...
これで、レイジーサービスを定義できます。
# in config/controllers.yml services: controller.default: lazy: true class: App\Controller\DefaultController arguments: [ "@request" ]
したがって、コントローラーは、実際のメソッドが呼び出されたときにのみ、依存サービスの初期化を引き起こします。 また、コントローラーサービスは実際の初期化の前に共有されるため、循環依存エラーを回避できます。 ただし、循環参照は避ける必要があります。 この場合、リクエストサービスにコントローラサービスを注入したり、コントローラサービスにリクエストサービスを注入したりしないでください。 明らかに、コントローラーにリクエストサービスが必要なので、コンテナの開始段階でリクエストサービスが挿入されないようにしましょう。 HttpKernelには、この目的のためのイベントシステムがあります。
ルーティング
どうやら、リクエストごとに異なるコントローラーが必要なようです。 したがって、ルーティングシステムが必要です。 symfonyルーティングコンポーネントをインストールしましょう。
$ composer require symfony/routing
ルーティングコンポーネントには、ルーティング構成ファイルを使用できるクラスRouterがあります。 ただし、これらの構成は、RouteクラスのKey-Valueパラメーターにすぎません。 Symfonyフレームワークは、FrameworkBundleの独自のコントローラーリゾルバーを使用して、「ContainerAware」インターフェイスを備えたコントローラーにコンテナーを挿入します。 これはまさに私たちが避けようとしていることです。 HttpKernelコントローラーリゾルバーは、クラスオブジェクトが'_controller'属性に既に存在する場合、コントローラーオブジェクトとアクションメソッド文字列を含む配列として返します(実際には、コントローラーリゾルバーはそれが単なる配列であるかのようにそのまま返します)。 したがって、各ルートをサービスとして定義し、それにコントローラーを注入する必要があります。 他のコントローラーサービスを追加して、どのように機能するかを見てみましょう。

# in config/controllers.yml # ... controller.page: lazy: true class: App\Controller\PageController arguments: [ "@request"]
// in src/App/Controller/PageController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class PageController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction($id) { return new Response("Page $id doesn't exist"); } }
HttpKernelコンポーネントには、「kernel.request」イベントを使用するRouteListenerクラスがあります。 レイジーコントローラーで可能な構成の1つを次に示します。
# in config/routes/default.yml services: route.home: class: Symfony\Component\Routing\Route arguments: path: / defaults: _controller: ["@controller.default", 'defaultAction'] route.page: class: Symfony\Component\Routing\Route arguments: path: /page/{id} defaults: _controller: ["@controller.page", 'defaultAction']
# in config/routing.yml imports: - { resource: 'routes/default.yml' } services: route.collection: class: Symfony\Component\Routing\RouteCollection calls: - [ add, ["route_home", "@route.home"] ] - [ add, ["route_page", "@route.page"] ] router.request_context: class: Symfony\Component\Routing\RequestContext calls: - [ fromRequest, ["@request"] ] router.matcher: class: Symfony\Component\Routing\Matcher\UrlMatcher arguments: [ "@route.collection", "@router.request_context" ] router.listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener arguments: matcher: "@router.matcher" request_stack: "@request_stack" context: "@router.request_context"
# in config/events.yml service: dispatcher: class: Symfony\Component\EventDispatcher\EventDispatcher calls: - [ addSubscriber, ["@router.listener"]]
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' }
また、アプリケーションにはURLジェネレーターが必要です。 ここにあります:
# in config/routing.yml # ... router.generator: class: Symfony\Component\Routing\Generator\UrlGenerator arguments: routes: "@route.collection" context: "@router.request_context"
URLジェネレーターは、コントローラーおよびレンダリングサービスに挿入できます。 これで、基本アプリケーションができました。 その他のサービスは、構成ファイルが特定のコントローラーまたはイベントディスパッチャーに挿入されるのと同じ方法で定義できます。 たとえば、TwigとDoctrineのいくつかの構成を次に示します。
小枝
TwigはSymfony2フレームワークのデフォルトのテンプレートエンジンです。 多くのSymfony2コンポーネントは、アダプターなしでそれを使用できます。 したがって、これは私たちのアプリケーションにとって明らかな選択です。
$ composer require twig/twig $ mkdir src/App/View
# in config/twig.yml services: templating.twig_loader: class: Twig_Loader_Filesystem arguments: [ "%app_root%/src/App/View" ] templating.twig: class: Twig_Environment arguments: [ "@templating.twig_loader" ]
教義
DoctrineはSymfony2フレームワークで使用されるORMです。 他のORMを使用することもできますが、Symfony2コンポーネントはすでに多くのDocrine機能を使用できます。
$ composer require doctrine/orm $ mkdir src/App/Entity
# in config/doctrine.yml parameters: doctrine.driver: "pdo_pgsql" doctrine.user: "postgres" doctrine.password: "postgres" doctrine.dbname: "true_di" doctrine.paths: ["%app_root%/src/App/Entity"] doctrine.is_dev: true services: doctrine.config: class: Doctrine\ORM\Configuration factory: [ Doctrine\ORM\Tools\Setup, createAnnotationMetadataConfiguration ] arguments: paths: "%doctrine.paths%" isDevMode: "%doctrine.is_dev%" doctrine.entity_manager: class: Doctrine\ORM\EntityManager factory: [ Doctrine\ORM\EntityManager, create ] arguments: conn: driver: "%doctrine.driver%" user: "%doctrine.user%" password: "%doctrine.password%" dbname: "%doctrine.dbname%" config: "@doctrine.config"
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' } - { resource: 'twig.yml' } - { resource: 'doctrine.yml' }
アノテーションの代わりにYMLおよびXMLマッピング構成ファイルを使用することもできます。 'createYAMLMetadataConfiguration'および'createXMLMetadataConfiguration'メソッドを使用し、これらの構成ファイルを含むフォルダーへのパスを設定する必要があります。
すべてのコントローラーに必要なすべてのサービスを個別に注入することは、すぐに非常に煩わしくなります。 それをもう少し良くするために、DIコンテナコンポーネントには抽象的なサービスとサービスの継承があります。 したがって、いくつかの抽象コントローラーを定義できます。
# in config/controllers.yml services: controller.base_web: lazy: true abstract: true class: App\Controller\Base\WebController arguments: request: "@request" templating: "@templating.twig" entityManager: "@doctrine.entity_manager" urlGenerator: "@router.generator" controller.default: class: App\Controller\DefaultController parent: controller.base_web controller.page: class: App\Controller\PageController parent: controller.base_web
// in src/App/Controller/Base/WebController.php namespace App\Controller\Base; use Symfony\Component\HttpFoundation\Request; use Twig_Environment; use Doctrine\ORM\EntityManager; use Symfony\Component\Routing\Generator\UrlGenerator; abstract class WebController { /** @var Request */ protected $request; /** @var Twig_Environment */ protected $templating; /** @var EntityManager */ protected $entityManager; /** @var UrlGenerator */ protected $urlGenerator; function __construct( Request $request, Twig_Environment $templating, EntityManager $entityManager, UrlGenerator $urlGenerator ) { $this->request = $request; $this->templating = $templating; $this->entityManager = $entityManager; $this->urlGenerator = $urlGenerator; } } // in src/App/Controller/DefaultController // … class DefaultController extend WebController { // ... } // in src/App/Controller/PageController // … class PageController extend WebController { // ... }
フォーム、コマンド、アセットなど、他にも多くの便利なSymfonyコンポーネントがあります。 これらは独立したコンポーネントとして開発されたため、DIコンテナを使用した統合は問題になりません。
タグ
DIコンテナにはタグシステムもあります。 タグは、CompilerPassクラスで処理できます。 Event Dispatcherコンポーネントには、イベントリスナーのサブスクリプションを簡素化するための独自のCompiler Passがありますが、EventDispatcherクラスの代わりにContainerAwareEventDispatcherクラスを使用します。 だから使えません。 ただし、イベント、ルート、セキュリティ、およびその他の目的のために、独自のコンパイラパスを実装できます。
たとえば、ルーティングシステムのタグを実装しましょう。 ここで、ルートを定義するには、config /routesフォルダーのrouteconfigファイルでルートサービスを定義してから、config/routing.ymlファイルのルートコレクションサービスに追加する必要があります。 ある場所でルーターパラメータを定義し、別の場所でルーター名を定義しているため、一貫性がないように見えます。
タグシステムでは、タグにルート名を定義し、タグ名を使用してこのルートサービスをルートコレクションに追加するだけです。
DIコンテナコンポーネントは、コンパイラパスクラスを使用して、実際の初期化の前にコンテナ構成に変更を加えます。 それでは、ルータータグシステム用のコンパイラパスクラスを実装しましょう。
// in src/CompilerPass/RouterTagCompilerPass.php namespace CompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class RouterTagCompilerPass implements CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { $routeTags = $container->findTaggedServiceIds('route'); $collectionTags = $container->findTaggedServiceIds('route_collection'); /** @var Definition[] $routeCollections */ $routeCollections = array(); foreach ($collectionTags as $serviceName => $tagData) $routeCollections[] = $container->getDefinition($serviceName); foreach ($routeTags as $routeServiceName => $tagData) { $routeNames = array(); foreach ($tagData as $tag) if (isset($tag['route_name'])) $routeNames[] = $tag['route_name']; if (!$routeNames) continue; $routeReference = new Reference($routeServiceName); foreach ($routeCollections as $collection) foreach ($routeNames as $name) $collection->addMethodCall('add', array($name, $routeReference)); } } }
// in src/TrueContainer.php //... use CompilerPass\RouterTagCompilerPass; // ... $container = new self(); $container->addCompilerPass(new RouterTagCompilerPass()); // ...
これで、構成を変更できます。
# in config/routing.yml # … route.collection: class: Symfony\Component\Routing\RouteCollection tags: - { name: route_collection } # ...
# in config/routes/default.yml services: route.home: class: Symfony\Component\Routing\Route arguments: path: / defaults: _controller: ["@controller.default", 'defaultAction'] tags: - { name: route, route_name: 'route_home' } route.page: class: Symfony\Component\Routing\Route arguments: path: /page/{id} defaults: _controller: ["@controller.page", 'defaultAction'] tags: - { name: route, route_name: 'route_page' }
ご覧のとおり、ルートコレクションはサービス名ではなくタグ名で取得されるため、ルートタグシステムは実際の構成に依存しません。 また、ルートは「add」メソッドを使用して任意の収集サービスに追加できます。 コンパイラーの通行人は、依存関係の構成を大幅に簡素化できます。 ただし、DIコンテナに予期しない動作が追加される可能性があるため、引数、メソッド呼び出し、クラス名の変更など、既存のロジックを変更しないことをお勧めします。 タグを使用して行ったように、既存のものの上に新しいものを追加するだけです。
要約
これで、DIコンテナパターンのみを使用するアプリケーションができました。これは、DIコンテナ構成ファイルのみを使用して構築されています。 ご覧のとおり、この方法でSymfonyアプリケーションを構築することに深刻な問題はありません。 また、すべてのアプリケーションの依存関係を簡単に視覚化できます。 人々がDIコンテナをサービスロケーターとして使用する唯一の理由は、サービスロケーターの概念が理解しやすいからです。 そして、サービスロケーターとして使用されるDIコンテナーを備えた巨大なコードベースは、おそらくその理由の結果です。
このアプリケーションのソースコードはGitHubにあります。