支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
在前章,我们通过从同名组件中继承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 创作共用授权。