跳到内容

DependencyInjection 组件

编辑此页

在前一章节中,我们通过扩展同名组件中的 HttpKernel 类清空了 Simplex\Framework 类。看到这个空类,你可能会想把一些代码从前端控制器移到它里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;

class Framework extends HttpKernel\HttpKernel
{
    public function __construct($routes)
    {
        $context = new Routing\RequestContext();
        $matcher = new Routing\Matcher\UrlMatcher($routes, $context);
        $requestStack = new RequestStack();

        $controllerResolver = new HttpKernel\Controller\ControllerResolver();
        $argumentResolver = new HttpKernel\Controller\ArgumentResolver();

        $dispatcher = new EventDispatcher();
        $dispatcher->addSubscriber(new HttpKernel\EventListener\ErrorListener(
            'Calendar\Controller\ErrorController::exception'
        ));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher, $requestStack));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));
        $dispatcher->addSubscriber(new StringResponseListener());

        parent::__construct($dispatcher, $controllerResolver, $requestStack, $argumentResolver);
    }
}

前端控制器的代码会变得更加简洁

1
2
3
4
5
6
7
8
9
10
11
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';

$framework = new Simplex\Framework($routes);

$framework->handle($request)->send();

拥有简洁的前端控制器允许你为一个应用程序拥有多个前端控制器。 为什么这会很有用呢? 例如,为了允许为开发环境和生产环境拥有不同的配置。 在开发环境中,你可能希望开启错误报告并在浏览器中显示错误,以便于调试

1
2
ini_set('display_errors', 1);
error_reporting(-1);

......但是你肯定不希望在生产环境中使用相同的配置。 拥有两个不同的前端控制器使你有机会为它们中的每一个设置略有不同的配置。

所以,将代码从前端控制器移动到框架类使我们的框架更具可配置性,但与此同时,它也引入了很多问题

  • 我们无法再注册自定义监听器,因为调度器在 Framework 类外部不可用(一种解决方法是添加一个 Framework::getEventDispatcher() 方法);
  • 我们失去了以前的灵活性; 你无法再更改 UrlMatcherControllerResolver 的实现;
  • 与前一点相关,我们无法再轻松地测试我们的框架,因为不可能模拟内部对象;
  • 我们无法再更改传递给 ResponseListener 的字符集(一种解决方法是将其作为构造函数参数传递)。

之前的代码没有表现出相同的问题,因为我们使用了依赖注入; 我们对象的所有依赖项都被注入到它们的构造函数中(例如,事件调度器被注入到框架中,以便我们完全控制其创建和配置)。

这是否意味着我们必须在灵活性、自定义性、易于测试以及不将相同的代码复制粘贴到每个应用程序前端控制器之间做出选择? 正如你可能期望的那样,有一个解决方案。 我们可以通过使用 Symfony 依赖注入容器来解决所有这些问题以及更多问题

1
$ composer require symfony/dependency-injection

创建一个新文件来托管依赖注入容器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// example.com/src/container.php
use Simplex\Framework;
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;

$container = new DependencyInjection\ContainerBuilder();
$container->register('context', Routing\RequestContext::class);
$container->register('matcher', Routing\Matcher\UrlMatcher::class)
    ->setArguments([$routes, new Reference('context')])
;
$container->register('request_stack', HttpFoundation\RequestStack::class);
$container->register('controller_resolver', HttpKernel\Controller\ControllerResolver::class);
$container->register('argument_resolver', HttpKernel\Controller\ArgumentResolver::class);

$container->register('listener.router', HttpKernel\EventListener\RouterListener::class)
    ->setArguments([new Reference('matcher'), new Reference('request_stack')])
;
$container->register('listener.response', HttpKernel\EventListener\ResponseListener::class)
    ->setArguments(['UTF-8'])
;
$container->register('listener.exception', HttpKernel\EventListener\ErrorListener::class)
    ->setArguments(['Calendar\Controller\ErrorController::exception'])
;
$container->register('dispatcher', EventDispatcher\EventDispatcher::class)
    ->addMethodCall('addSubscriber', [new Reference('listener.router')])
    ->addMethodCall('addSubscriber', [new Reference('listener.response')])
    ->addMethodCall('addSubscriber', [new Reference('listener.exception')])
;
$container->register('framework', Framework::class)
    ->setArguments([
        new Reference('dispatcher'),
        new Reference('controller_resolver'),
        new Reference('request_stack'),
        new Reference('argument_resolver'),
    ])
;

return $container;

此文件的目的是配置你的对象及其依赖项。 在此配置步骤中,没有任何东西被实例化。 这纯粹是你需要操作的对象以及如何创建它们的静态描述。 对象将在你从容器访问它们时或容器需要它们来创建其他对象时按需创建。

例如,为了创建路由监听器,我们告诉 Symfony 它的类名是 Symfony\Component\HttpKernel\EventListener\RouterListener,并且它的构造函数接受一个 matcher 对象 (new Reference('matcher'))。 正如你所见,每个对象都通过一个名称引用,该名称是唯一标识每个对象的字符串。 该名称允许我们获取一个对象并在其他对象定义中引用它。

注意

默认情况下,每次你从容器中获取对象时,它都会返回完全相同的实例。 这是因为容器管理你的“全局”对象。

现在,前端控制器仅用于将所有内容连接在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$routes = include __DIR__.'/../src/app.php';
$container = include __DIR__.'/../src/container.php';

$request = Request::createFromGlobals();

$response = $container->get('framework')->handle($request);

$response->send();

由于所有对象现在都在依赖注入容器中创建,因此框架代码应该是之前的简单版本

1
2
3
4
5
6
7
8
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\HttpKernel\HttpKernel;

class Framework extends HttpKernel
{
}

注意

如果你想要一个轻量级的容器替代方案,请考虑 Pimple,一个大约 60 行 PHP 代码的简单依赖注入容器。

现在,这是你如何在前端控制器中注册自定义监听器的方法

1
2
3
4
5
6
7
// ...
use Simplex\StringResponseListener;

$container->register('listener.string_response', StringResponseListener::class);
$container->getDefinition('dispatcher')
    ->addMethodCall('addSubscriber', [new Reference('listener.string_response')])
;

除了描述你的对象之外,依赖注入容器还可以通过参数进行配置。 让我们创建一个定义我们是否处于调试模式的参数

1
2
3
$container->setParameter('debug', true);

echo $container->getParameter('debug');

这些参数可以在定义对象定义时使用。 让我们使字符集可配置

1
2
3
4
// ...
$container->register('listener.response', HttpKernel\EventListener\ResponseListener::class)
    ->setArguments(['%charset%'])
;

在此更改之后,你必须在使用响应监听器对象之前设置字符集

1
$container->setParameter('charset', 'UTF-8');

与其依赖于路由由 $routes 变量定义的约定,不如再次使用参数

1
2
3
4
// ...
$container->register('matcher', Routing\Matcher\UrlMatcher::class)
    ->setArguments(['%routes%', new Reference('context')])
;

以及前端控制器中的相关更改

1
$container->setParameter('routes', include __DIR__.'/../src/app.php');

我们仅仅触及了你可以使用容器做的事情的表面:从作为参数的类名,到覆盖现有的对象定义,从共享服务支持到将容器转储到纯 PHP 类,以及更多。 Symfony 依赖注入容器非常强大,能够管理任何类型的 PHP 类。

如果你不想在你的框架中使用依赖注入容器,请不要对我大喊大叫。 如果你不喜欢它,就不要使用它。 这是你的框架,不是我的。

这(已经)是本书关于在 Symfony 组件之上创建框架的最后一章。 我知道许多主题尚未详细介绍,但希望它能为你提供足够的信息,让你开始自己的工作,并更好地理解 Symfony 框架的内部工作原理。

玩得开心!

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可协议获得许可。
目录
    版本