The DependencyInjection Component
再上一个章节中,我们通过继承同名组件中的 HttpKernel
类置空了Simplex\Framework
类。看到这个空类,你可以会忍不住从front控制器中搬一些代码到类里:
// 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);
$requestStack = new RequestStack();
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher, $requestStack));
$dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));
parent::__construct($dispatcher, $controllerResolver, $requestStack, $argumentResolver);
}
}
front控制器的代码更变更简洁了:
// 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();
拥有一个简洁的front控制器后,你可以在单个应用中同时设置多个这样的控制器。多个控制器有什么作用呢?举个例子,你可以在开发环境下使用多个配置,在生产环境下只使用一个。在开发环境下,你可能想要开启错误报告以及在浏览器中显示错误来简化调试的过程:
ini_set('display_errors', 1);
error_reporting(-1);
... 当时你当然不想在生产环境下使用相同的配置。有2个不同的front控制器可以设置2中不同的配置。
因此,把代码从front控制器移动到框架类中可以使我们的框架更加可配置化,但是同时,它也会带来一些问题:
我们不能注册定制的监听器了,因为分发器不适合放在Framework类的外面(一个简单的解决方式是添加一个
Framework::getEventDispatcher()
方法);我们失去了以前的灵活性;你不能改变
UrlMatcher
和ControllerResolver
的实现;和上一点相关,我们测试框架变得不那么简单了,因为内核对象无法被模拟;
我们不能改变创递给
ResponseListener
的字符集(一个解决方式是通过构造函数传递)。
前面的代码没有显示出这些问题,因为我们使用了依赖注入; 所有我们对象的依赖在它们的够咱函数中被注入(例如,事件分发器在框架中被注入,我们才能完全的控制它的创建和)。
这是不是意味着我们必须在灵活性,可定制性,易于测试性和不在每个应用的front控制器中复制和黏贴相同的代码之间做个选择? 就像你所想的,这是个问题。我们可以解决这些问题甚至更多其他的问题,通过使用Symfony的依赖注入容器:
$ composer require symfony/dependency-injection
创建一个新文件去管理依赖注入容器的配置:
// example.com/src/container.php
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;
use Symfony\Component\EventDispatcher;
use Simplex\Framework;
$sc = new DependencyInjection\ContainerBuilder();
$sc->register('context', Routing\RequestContext::class);
$sc->register('matcher', Routing\Matcher\UrlMatcher::class)
->setArguments(array($routes, new Reference('context')))
;
$sc->register('request_stack', HttpFoundation\RequestStack::class);
$sc->register('controller_resolver', HttpKernel\Controller\ControllerResolver::class);
$sc->register('argument_resolver', HttpKernel\Controller\ArgumentResolver::class);
$sc->register('listener.router', HttpKernel\EventListener\RouterListener::class)
->setArguments(array(new Reference('matcher'), new Reference('request_stack')))
;
$sc->register('listener.response', HttpKernel\EventListener\ResponseListener::class)
->setArguments(array('UTF-8'))
;
$sc->register('listener.exception', HttpKernel\EventListener\ExceptionListener::class)
->setArguments(array('Calendar\Controller\ErrorController::exceptionAction'))
;
$sc->register('dispatcher', EventDispatcher\EventDispatcher::class)
->addMethodCall('addSubscriber', array(new Reference('listener.router')))
->addMethodCall('addSubscriber', array(new Reference('listener.response')))
->addMethodCall('addSubscriber', array(new Reference('listener.exception')))
;
$sc->register('framework', Framework::class)
->setArguments(array(
new Reference('dispatcher'),
new Reference('controller_resolver'),
new Reference('request_stack'),
new Reference('argument_resolver'),
))
;
return $sc;
这个文件的目的是配置你的对象和它们的依赖。在配置这一步中,什么都还没被实例化。这只是一个你需要操作的对象以及如何创建它们的的静态描述。当你在容器中访问这些对象或者当容器需要通过这些对象创建其他对象的时候,对象才会被创建。
举个例子,创建一个路由监听器,我们告诉Symfony它的类名叫做 Symfony\Component\HttpKernel\EventListener\RouterListener
,以及它的够咱函数需要一个匹配器对象(new Reference('matcher')
)。正如你所见,每个对象引用自一个名称,一个用来区别各个对象的字符串。名称允许我们获取一个对象,或者在其他对象的定义中引用它。
默认情况下,每次你从容器中获取一个对象,它都会返回完全相同的实例。这是因为一个容器管理的是“全局”的对象。
front控制器现在只是用来将所有其他的文件串联起来:
// 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();
因为所有的对象现在都是通过依赖注入容易创建的,框架的代码应该变成之前那个简单的版本:
// example.com/src/Simplex/Framework.php
namespace Simplex;
use Symfony\Component\HttpKernel\HttpKernel;
class Framework extends HttpKernel
{
}
如果你想要使你的容器更加轻便,考虑下Pimple,一个只有60行PHP代码,简单的依赖注入容器。
现在,我们就可以向下面一样在front控制器中注册一个定制的监听器:
// ...
use Simplex\StringResponseListener;
$sc->register('listener.string_response', StringResposeListener::class);
$sc->getDefinition('dispatcher')
->addMethodCall('addSubscriber', array(new Reference('listener.string_response')))
;
除了描述你的对象,依赖注入容器还可以通过参数配置。让我们创建一个容器,用来定义是否开始调试模式:
$sc->setParameter('debug', true);
echo $sc->getParameter('debug');
这些参数可以在定义对象的时候被使用。让我们将字符集可配置化:
// ...
$sc->register('listener.response', HttpKernel\EventListener\ResponseListener::class)
->setArguments(array('%charset%'))
;
在这个改变之后,你需要使用响应监听器对象来设置字符集:
$sc->setParameter('charset', 'UTF-8');
让我们再次使用一个参数,来取代用$routes
变量定义路由的常规做法:
// ...
$sc->register('matcher', Routing\Matcher\UrlMatcher::class)
->setArguments(array('%routes%', new Reference('context')))
;
然后在front控制器做相应的修改:
$sc->setParameter('routes', include __DIR__.'/../src/app.php');
我们仅仅接触到容器能做的事的表面: 从以类名为参数,到覆盖已存在的类定义, 从共享服务支持到将容器转储为纯PHP类,等等。Symfony 依赖注入容器非常的强大,能够管理任何类型的PHP类。
如果你不想要在你的框架里使用依赖注入容易,也被对我大吼大叫。如果你不喜欢可以不去用它。这是你的框架,不是我的。
这是本书(使用Symfony组件创建框架)的最后一个章节。我知道很多细节上的话题还没有涉及到,但是希望本书已经给予你足够的信息,来开始你自己的框架,以及对Symfony是怎么运作的有了更深的理解。
如果你想要了解更多,阅读微型框架Silex的源码,尤其是Application类。
Have fun!