个性化阅读
专注于IT技术分析

带有Symfony组件的真正依赖注入

点击下载

本文概述

Symfony2, 一个高性能的PHP框架, 使用了依赖注入容器模式, 其中组件为DI容器提供了依赖注入接口。这使每个组件不必关心其他依赖项。 “内核”类初始化DI容器并将其注入到不同的组件中。但这意味着DI容器可以用作服务定位器。

Symfony2甚至为此提供了” ContainerAware”类。许多人认为Service Locator是Symfony2中的反模式。我个人不同意。与DI相比, 它是一种更简单的模式, 适用于简单的项目。但是将服务定位器模式和DI容器模式组合在单个项目中绝对是一种反模式。

带有Symfony组件的真正依赖注入

在本文中, 我们将尝试在不实现Service Locator模式的情况下构建Symfony2应用程序。我们将遵循一个简单的规则:只有DI容器构建者才能了解DI容器。

DI容器

在”依赖关系注入”模式中, DI容器定义服务依赖关系, 并且服务只能提供用于注入的接口。关于依赖注入的文章很多, 你可能已经阅读了全部。因此, 我们不要只关注理论, 而要看一下基本概念。 DI可以是3种类型:

带有Symfony组件的真正依赖注入2

在Symfony中, 可以使用简单的配置文件定义注入结构。以下是这三种注射类型的配置方式:

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"

引导项目

让我们创建基本的应用程序结构。在此期间, 我们将安装Symfony DI容器组件。

$ 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 autoloader在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和Yaml symfony组件。你可以在此处的官方文档中找到详细信息。另外, 我们以防万一, 定义了根路径参数” 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工作流程:

带有Symfony组件的真正依赖注入3

绿色方块是事件。

HttpKernel将HttpFoundation组件用于Request和Response对象, 将EventDispatcher组件用于事件系统。使用DI容器配置文件初始化它们没有问题。必须使用EventDispatcher, ControllerResolver和(可选)使用RequestStack(用于子请求)服务初始化HttpKernel。

带有Symfony组件的真正依赖注入4

这是它的容器配置:

# 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”属性以解析控制器。这些属性可以在容器配置中定义, 但是看起来有些棘手, 因为我们必须使用ParameterBag对象而不是简单的数组。

带有Symfony组件的真正依赖注入5
# 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容器并将其用作服务定位器来解决此问题。我们不会那样做。因此, 让我们将控制器定义为服务, 然后将请求服务注入其中。配置如下:

带有Symfony组件的真正依赖注入6
# 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容器在创建之后, 方法和属性注入之前共享服务。因此, 在创建控制器服务时, 请求服务已经存在。

运作方式如下:

带有Symfony组件的真正依赖注入7

但这仅是因为首先创建了请求服务。当我们在前端控制器中获得响应服务时, 请求服务是第一个初始化的依赖项。如果我们尝试首先获取控制器服务, 将导致循环依赖错误。可以使用方法或属性注入对其进行修复。

但是还有另一个问题。 DI容器将使用依赖项初始化每个控制器。因此, 即使不需要这些服务, 它也会初始化所有存在的服务。幸运的是, 该容器具有延迟加载功能。 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类的键值参数。 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组件具有RouteListener类, 该类使用” kernel.request”事件。这是使用惰性控制器的一种可能的配置:

带有Symfony组件的真正依赖注入8
# 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生成器。这里是:

带有Symfony组件的真正依赖注入9
# 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" ]

教义

原则是在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组件, 例如Form, Command和Assets。它们被开发为独立的组件, 因此使用DI容器进行集成应该不是问题。

标签

DI容器还具有标签系统。标签可以通过Compiler Pass类进行处理。 Event Dispatcher组件具有自己的Compiler Pass来简化事件侦听器的预订, 但它使用ContainerAwareEventDispatcher类而不是EventDispatcher类。所以我们不能使用它。但是我们可以为事件, 路由, 安全性和任何其他目的实现我们自己的编译器传递。

例如, 让我们为路由系统实现代码。现在要定义路由, 我们必须在config / routes文件夹中的路由配置文件中定义路由服务, 然后将其添加到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' }

如你所见, 我们通过标签名称而不是服务名称获取路由集合, 因此我们的路由标签系统不依赖于实际配置。此外, 可以使用”添加”方法将路线添加到任何收集服务。编译器传递者可以大大简化依赖项的配置。但是它们会给DI容器带来意外的行为, 因此最好不要修改现有的逻辑, 例如更改参数, 方法调用或类名。只需添加一个新标签, 就像我们使用标签所做的那样。

本文总结

现在, 我们有了一个仅使用DI容器模式的应用程序, 并且仅使用DI容器配置文件进行了构建。如你所见, 以这种方式构建Symfony应用程序并没有严重的挑战。你可以简单地可视化所有应用程序依赖项。人们使用DI容器作为服务定位器的唯一原因是, 服务定位器的概念更易于理解。使用DI容器作为服务定位器的巨大代码库可能就是这个原因的结果。

你可以在GitHub上找到此应用程序的源代码。

赞(0)
未经允许不得转载:srcmini » 带有Symfony组件的真正依赖注入

评论 抢沙发

评论前必须登录!