作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Vasilii Lapin's profile image

Vasilii Lapin

Vasilii曾是一名web架构师和PHP后端开发人员, specializing in NIX administration, and high-load, large scale projects.

Expertise

Years of Experience

12

Share

Symfony2, a high performance PHP framework, 使用依赖注入容器模式,其中组件为di容器提供依赖注入接口. 这允许每个组件不关心其他依赖项. Kernel类初始化di容器并将其注入到不同的组件中. 但这意味着DI-container可以用作服务定位器.

Symfony2甚至为此提供了“ContainerAware”类. 许多人认为服务定位器是Symfony2中的一个反模式. Personally, I do not agree. 与DI相比,它是一种更简单的模式,适用于简单的项目. 但是在单个项目中结合服务定位器模式和di -容器模式绝对是一种反模式.

Symfony组件的真正依赖注入

在本文中,我们将尝试构建一个不实现服务定位器模式的Symfony2应用程序. 我们将遵循一个简单的规则:只有DI-container构建器才能了解DI-container.

DI-container

In Dependency Injection pattern, DI-container定义了服务依赖关系,而服务只能提供一个接口用于注入. There are many articles about Dependency Injection, and you probably have read all of them. 我们先不关注理论,只看一下基本思想. DI can be of 3 types:

在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"

Bootstrapping Project

让我们创建基本的应用程序结构. 同时,我们将安装Symfony DI-container组件.

$ mkdir trueDI
$ cd trueDI
$ composer init
$ composer需要symfony/dependency-injection
$ composer require symfony/config
$ composer require symfony/yaml
$ mkdir config
$ mkdir www
$ mkdir src

让composer自动加载器在src文件夹中找到我们自己的类, 我们可以在composer中添加autoloader属性.json file:

{
// ...
  "autoload": {
    "psr-4": { "": "src/" }
  }
}

让我们创建我们的容器构建器并禁止容器注入.

// in src/TrueContainer.php
使用Symfony \ \ DependencyInjection \ ContainerBuilder组件;
use Symfony\Component\Config\FileLocator;
使用Symfony \组件\ DependencyInjection \装载机\ YamlFileLoader;
使用Symfony \ \ DependencyInjection \ ContainerInterface组件;

类TrueContainer扩展ContainerBuilder {

    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
    ) {
        如果(strtolower($id) == 'service_container') {
            如果(ContainerInterface:: EXCEPTION_ON_INVALID_REFERENCE 
                !== 
                $invalidBehavior
            ) {
                return;
            }
            throw new InvalidArgumentException(
                服务定义“service_container”不存在.'
            );
        }
        
        return parent::get($id, $invalidBehavior);
    }
}

这里我们使用Config和Yaml symfony组件. 你可以在官方文件中找到细节 here. 为了以防万一,我们还定义了根路径参数“app_root”. get方法重载父类的默认get行为,并阻止容器返回“service_container”。.

接下来,我们需要应用程序的入口点.

// in www/index.php
require_once('../vendor/autoload.php');

$container = TrueContainer::buildContainer(dirname(__DIR__));

This one is meant to handle http requests. 我们可以为控制台命令、cron任务等提供更多的入口点. 每个入口点都应该获得某些服务,并且应该了解di -容器结构. 这是我们可以从容器请求服务的唯一位置. 从现在开始,我们将尝试仅使用di容器配置文件构建此应用程序.

HttpKernel

HttpKernel(不是有服务定位器问题的框架内核)将成为应用程序web部分的基础组件. Here is a typical HttpKernel workflow:

Green squares are events.

HttpKernel使用HttpFoundation组件作为请求和响应对象,使用EventDispatcher组件作为事件系统. 使用di容器配置文件初始化它们没有问题. HttpKernel必须用EventDispatcher初始化, ControllerResolver, 以及可选的RequestStack(用于子请求)服务.

下面是它的容器配置:

# in config/events.yml
services:
  dispatcher:
    类:Symfony \ \ EventDispatcher \ EventDispatcher组件
# in config/kernel.yml
services:
  request:
    组件类:Symfony \ \ HttpFoundation \请求
    factory: [Symfony\Component\HttpFoundation\Request, createFromGlobals]
  request_stack:
    类:Symfony \ \ HttpFoundation \ RequestStack组件
  resolver:
    控制器组件类:Symfony \ \ HttpKernel \ \ ControllerResolver
  http_kernel:
    类:Symfony \ \ HttpKernel \ HttpKernel组件
    参数:["@dispatcher", "@resolver", "@request_stack"]
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }

正如你所看到的,我们使用' factory '属性来创建请求服务. HttpKernel服务只获取Request对象并返回Response对象. It can be done in the front controller.

// 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:
    组件类:Symfony \ \ HttpFoundation \响应
    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();

控制器解析器服务从Request服务的属性中获取' _controller '属性来解析控制器. 这些属性可以在容器配置中定义, 但它看起来有点棘手,因为我们必须使用一个ParameterBag对象而不是一个简单的数组.

# in config/kernel.yml
# ...
  request_attributes:
    类:\ Symfony \ \ HttpFoundation \ ParameterBag组件
    calls:
      - [set, [_controller, \App\Controller\DefaultController::defaultAction]]
  request:
    组件类:Symfony \ \ HttpFoundation \请求
    factory: [Symfony\Component\HttpFoundation\Request, createFromGlobals]
    properties:
      attributes: "@request_attributes"
# ...

这里是带有defaultAction功能的DefaultController类.

//在src/App/Controller/DefaultController中.php

namespace App\Controller;

组件使用Symfony \ \ HttpFoundation \反应;

class DefaultController {

    function defaultAction()
    {
        return new Response("Hello cruel world");
    }
}

有了所有这些,我们应该有一个工作的应用程序.

这个控制器非常无用,因为它不能访问任何服务. In Symfony framework, 这个问题可以通过在控制器中注入di容器并将其用作服务定位器来解决. We won’t do that. 因此,让我们将控制器定义为一个服务,并将请求服务注入其中. Here is the configuration:

# in config/controllers.yml
services:
  controller.default:
    class: App\Controller\DefaultController
    arguments: [ "@request"]
# in config/kernel.yml
# ...
  request_attributes:
    类:\ Symfony \ \ HttpFoundation \ ParameterBag组件
    calls:
      - [ set, [ _controller, ["@controller.default", defaultAction ]]]
  request:
    组件类:Symfony \ \ HttpFoundation \请求
    factory: [Symfony\Component\HttpFoundation\Request, createFromGlobals]
    properties:
      attributes: "@request_attributes"
# ...
#in config/services.yml

imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }

And the controller code:

//在src/App/Controller/DefaultController中.php

namespace App\Controller;

使用Symfony \ HttpFoundation \ \组件请求;
组件使用Symfony \ \ HttpFoundation \反应;

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容器在创建之后和方法注入和属性注入之前共享服务. 因此,当控制器服务创建时,请求服务已经存在.

Here’s how it works:

但这之所以有效,只是因为首先创建了请求服务. 当我们在前端控制器中得到响应服务时, 请求服务是第一个初始化的依赖项. 如果我们尝试先获取控制器服务,就会导致循环依赖错误. 它可以通过使用方法或属性注入来修复.

But there is another problem. DI-container将用依赖项初始化每个控制器. 因此,它将初始化所有现有的服务,即使它们不需要. 幸运的是,容器具有延迟加载功能. Symfony DI-component使用' ocramius/proxy-manager '作为代理类. We have to install a bridge between them.

$ composer需要symfony/proxy-manager-bridge

并在集装箱建造阶段定义它:

// in src/TrueContainer.php
//...
使用Symfony \ \ ProxyManager \ LazyProxy桥梁\ Instantiator \ RuntimeInstantiator;
// ...
    $container = new self();
    $container->setProxyInstantiator(new RuntimeInstantiator());
// ...

Now we can define lazy services.

# in config/controllers.yml
services:
  controller.default:
    lazy: true
    class: App\Controller\DefaultController
    arguments: [ "@request" ]

因此,只有在调用实际方法时,控制器才会初始化所依赖的服务. Also, it avoids circular dependency error because a controller service will be shared before actual initialization; although we still have to avoid circular references. 在这种情况下,我们不应该在请求服务中注入控制器服务,也不应该在请求服务中注入控制器服务. 显然,我们需要控制器中的请求服务, 因此,让我们避免在容器初始化阶段对请求服务进行注入. HttpKernel具有用于此目的的事件系统.

Routing

显然,我们希望针对不同的请求使用不同的控制器. So we need a routing system. 让我们安装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;

使用Symfony \ HttpFoundation \ \组件请求;
组件使用Symfony \ \ HttpFoundation \反应;

class PageController {

    /** @var Request */
    protected $request;

    function __construct(Request $request)
    {
        $this->request = $request;
    }

    function defaultAction($id)
    {
        return new Response("Page $id不存在");
    }
}

HttpKernel组件有一个RouteListener类,它使用了' kernel '.request’ event. 下面是一个使用延迟控制器的可能配置:

# 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:
    类:Symfony \ \路由\ RouteCollection组件
    calls:
      - [ add, ["route_home", "@route.home"] ]
      - [ add, ["route_page", "@route.page"] ]
  router.request_context:
      类:Symfony \ \路由\ RequestContext组件
      calls:
        - [ fromRequest, ["@request"] ]
  router.matcher:
    类:Symfony \ \ \匹配器\ UrlMatcher路由组件
    arguments: [ "@route.collection", "@router.request_context" ]
  router.listener:
    类:Symfony \ \ HttpKernel \ EventListener \ RouterListener组件
    arguments:
      matcher: "@router.matcher"
      request_stack: "@request_stack"
      context: "@router.request_context"
# in config/events.yml
service:
  dispatcher:
      类:Symfony \ \ EventDispatcher \ EventDispatcher组件
      calls:
        - [ addSubscriber, ["@router.listener"]]
#in config/services.yml
imports:
  - { resource: 'events.yml' }
  - { resource: 'kernel.yml' }
  - { resource: 'controllers.yml' }
  - { resource: 'routing.yml' }

此外,我们需要一个URL生成器在我们的应用程序. Here it is:

# in config/routing.yml
# ...
  router.generator:
    类:Symfony \ \ \发电机\ UrlGenerator路由组件
    arguments:
      routes: "@route.collection"
      context: "@router.request_context"

URL生成器可以被注入到控制器和呈现服务中. Now we have a base application. 任何其他服务都可以用与将配置文件注入特定控制器或事件调度程序相同的方式来定义. 例如,以下是Twig和Doctrine的一些配置.

Twig

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

Doctrine是Symfony2框架中使用的一个ORM. 我们可以使用任何其他ORM,但是Symfony2组件已经可以使用许多doctrine特性.

$ 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
    工厂:[Doctrine\ORM\Tools\Setup, createannotationmetadatconfiguration]
    arguments:
      paths: "%doctrine.paths%"
      isDevMode: "%doctrine.is_dev%"
  doctrine.entity_manager:
    class: Doctrine\ORM\EntityManager
    工厂:[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
//在src/App/Controller/Base/WebController中.php
namespace App\Controller\Base;

使用Symfony \ HttpFoundation \ \组件请求;
use Twig_Environment;
use Doctrine\ORM\EntityManager;
使用Symfony \组件\ \发电机\ 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;
    }
}

//在src/App/Controller/DefaultController中
// …
类DefaultController扩展WebController
{
    // ...
}

// in src/App/Controller/PageController
// …
class PageController extend WebController
{
    // ...
}

还有许多其他有用的Symfony组件,如表单、命令和资产. 它们是作为独立组件开发的,因此使用DI-container进行集成应该不成问题.

Tags

DI-container also has a tags system. 标记可以由编译器传递类处理. 事件分派器组件有自己的编译器通道来简化事件侦听器订阅, 但是它使用ContainerAwareEventDispatcher类而不是EventDispatcher类. So we can’t use it. 但我们可以为事件、路由、安全性和任何其他目的实现自己的编译器传递.

例如,让我们为路由系统实现标记. 现在,要定义路由,我们必须在config/routes文件夹中的路由配置文件中定义一个路由服务,然后将它添加到config/routing中的路由集合服务中.yml file. 它看起来不一致,因为我们在一个地方定义了路由器参数,在另一个地方定义了路由器名称.

With tag system, 我们只需要在标签中定义一个路由名,然后使用标签名将这个路由服务添加到路由集合中.

DI-container组件在实际初始化之前使用编译器传递类对容器配置进行任何修改. 因此,让我们为路由器标签系统实现编译器传递类.

//在src/CompilerPass/RouterTagCompilerPass中.php
namespace CompilerPass;

使用Symfony \编译器组件\ DependencyInjection \ \ CompilerPassInterface;
使用Symfony \ \ DependencyInjection \ ContainerBuilder组件;
使用Symfony \ DependencyInjection \ \组件定义;
使用Symfony \ DependencyInjection \ \组件参考;

类RouterTagCompilerPass实现了CompilerPassInterface
{
    /**
     你可以在容器被转储到PHP代码之前修改它.
     *
     * @param ContainerBuilder $container
     */
    公共函数进程(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;

            $ routerreference = 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());
// ...

Now we can modify our configuration:

# in config/routing.yml
# …
  route.collection:
    类:Symfony \ \路由\ 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'}

As you can see, 我们通过标签名而不是服务名来获取路由集合, 所以我们的路由标签系统不依赖于实际的配置. 此外,可以通过“add”方法将路由添加到任何集合服务中. 编译器传递器可以显著简化依赖项的配置. 但是它们可以向di容器添加意外的行为, 所以最好不要修改现有的逻辑,比如改变论点, method calls, or class names. 只需添加一个新的存在,就像我们通过使用标签所做的那样.

Wrap Up

我们现在有了一个只使用DI容器模式的应用程序, 它只使用di容器配置文件构建. 正如你所看到的,没有严重的挑战 building a Symfony application this way. 您可以简单地可视化所有应用程序依赖项. 人们使用DI-container作为服务定位器的唯一原因是服务定位器概念更容易理解. 使用di容器作为服务定位器的巨大代码库可能就是这个原因的结果.

您可以找到这个应用程序的源代码 on GitHub.

Hire a Toptal expert on this topic.
Hire Now
Vasilii Lapin's profile image
Vasilii Lapin

Located in Eindhoven, Netherlands

Member since December 3, 2015

About the author

Vasilii曾是一名web架构师和PHP后端开发人员, specializing in NIX administration, and high-load, large scale projects.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

12

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.