依赖注入组件(DependencyInjection Component)

在前章,我们通过从同名组件中继承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
// example.com/src/Simplex/Framework.php
namespace Simplex;
 
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Routing;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpKernel;
 
class Framework extends HttpKernel\HttpKernel
{
    public function __construct($routes)
    {
        $context = new Routing\RequestContext();
        $matcher = new Routing\Matcher\UrlMatcher($routes, $context);
 
        $controllerResolver = new HttpKernel\Controller\ControllerResolver();
        $argumentResolver = new HttpKernel\Controller\ArgumentResolver();
 
        $dispatcher = new EventDispatcher();
        $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));
 
        parent::__construct($dispatcher, $controllerResolver, new 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类里,令我们的框架更加“可配置”(configuable),与此同时,这也带来了很多问题:

  • 我们不再能注册自定义的监听(custom listeners),因为dispatcher在Framework类之外是不可用的(一个简单的办法可能是添加一个Framework::getEventDispatcher());

  • 我们失去了之前拥有的灵活性;你不再能改变UrlMatcher的实现,也无法改变ControllerResolver的实现;

  • 与之前的观点相关,我们不再能轻松测试我们的框架,因为模拟内部对象(mock internal objects)是不可能的;

  • 我们不再能改变“传入ResponseListener监听的charset字符集”(一种办法是把它当作构造参数来传入)。

这是否意味着,我们不得不在“灵活性、定制性、可测试以及[不需要在不同的前端控制器中复制/粘贴相同的代码]”之间,做出选择?可能你已料到,确有解决方案。我们可以解决所有这些问题,并且得到更多,只需使用Symfony DIC(dependency injection container,依赖注入容器):

1
$  composer require symfony/dependency-injection

创建一个新文件,用于容纳“DI容器”的配置信息:

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
// example.com/src/container.php
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
 
$sc = new DependencyInjection\ContainerBuilder();
$sc->register('context', 'Symfony\Component\Routing\RequestContext');
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
    ->setArguments(array($routes, new Reference('context')))
;
$sc->register('request_stack', 'Symfony\Component\HttpFoundation\RequestStack');
$sc->register('controller_resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver');
$sc->register('argument_resolver', 'Symfony\Component\HttpKernel\Controller\ArgumentResolver');
 
$sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener')
    ->setArguments(array(new Reference('matcher'), new Reference('request_stack')))
;
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
    ->setArguments(array('UTF-8'))
;
$sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener')
    ->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction'))
;
$sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher')
    ->addMethodCall('addSubscriber', array(new Reference('listener.router')))
    ->addMethodCall('addSubscriber', array(new Reference('listener.response')))
    ->addMethodCall('addSubscriber', array(new Reference('listener.exception')))
;
$sc->register('framework', 'Simplex\Framework')
    ->setArguments(array(
        new Reference('dispatcher'),
        new Reference('controller_resolver'),
        new Reference('request_stack'),
        new Reference('argument_resolver'),
    ))
;
 
return $sc;

这个文件的目的,就是要配置你的对象和它们的依赖。在此处的“配置”过程中并没有任何东西被实例化(instantiated)。这纯粹是对你需要操作的对象的静态描述,以及如何创建它们。对象,将在你从容器中访问它们时,或者在容器需要它们来创建其他对象时,被创建出来(译注:拿到实例)。

例如,要创建路由监听,我们告诉Symfony这个类的名字是Symfony\Component\HttpKernel\EventListener\RouterListener,以及它的构造器要接收一个matcher对象(new Reference('matcher'))。如你所见,每个对象,是被一个名字,即,一个“唯一的用于识别每个对象”的字符串,所引用的(referenced by a name)。这个名字,可以让我们取得一个对象,以及在别的对象定义中引用它(所代表的那个对象)。

默认时,每次你从容器中取得一个对象时,容器返回的都是完全相同的实例。这是因为容器管理的是你的“全局”对象。

现在,前端控制器只需把每样东西关联到一起:

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';
$sc = include __DIR__.'/../src/container.php';
 
$request = Request::createFromGlobals();
 
$response = $sc->get('framework')->handle($request);
 
$response->send();

由于所有对象现在都被创建到DI容器之中,框架代码继续保持之前的简易版本:

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,这是个简单的DIC,仅有60余行代码。

现在,看一下在前端控制器中,如何注册一个自定义的监听器:

1
2
3
4
$sc->register('listener.string_response', 'Simplex\StringResponseListener');
$sc->getDefinition('dispatcher')
    ->addMethodCall('addSubscriber', array(new Reference('listener.string_response')))
;

除了要描述你的对象,DI容器也可以通过参数来配置。我们创建一个参数,定义了是否处在debug模式中:

1
2
3
$sc->setParameter('debug', true);
 
echo $sc->getParameter('debug');

这些参数,在进行对象定义时,可以使用。让我们把charset变得“可配置”(configuable):

1
2
3
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
    ->setArguments(array('%charset%'))
;

做出如此改变之后,你必须在使用response监听对象之前设置好charset:

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

不需再依赖于$routes变量所定义的路由之命名约定,我们不妨再次使用参数(parameter):

1
2
3
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
    ->setArguments(array('%routes%', new Reference('context')))
;

在前端控制器中做相应调整:

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

很明显,前面我们只是对“你能用容器做些什么”稍加触碰而已:从“以参数充当类名”,到“覆写既存对象之定义”,从“服务共享”到“把容器剥离为纯PHP类”,乃至更多。Symfony的DIC是极度强大的,可以管理任何类型的PHP类。

如果你不想在你的框架中使用DIC,请别朝我喊叫。如果你不喜欢它,不要用它。那是你的框架,不是我的。

这(已经)是“如何创建你自己的框架”系列文章的最后一篇,文章基于Symfony组件作成。我也知道很多课题没有被详细地覆盖到,但希望这能给予你足够的信息,令你起步,同时更好地理解Symfony框架在内部是如何运作的。

如果你希望了解更多,阅读Silex微框架的源代码,特别是它的Application类。

Have fun!

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)