看起来你想要知道Symfony2是如何工作的,以及要如何对它进行扩展。这让我非常高兴。本节深入Symfony2内部,对其进行说明。
如果你想了解Symfony2是如何工作的,或者你想扩展Symfony2的话,你只需要阅读本节。
自动加载没有被框架直接管理;它是在UniversalClassLoader类和src/autoload.php的帮助下独立实现的。更多信息请阅读专用章节。
HttpFoundation组件
- Request 类抽象出主要的PHP全局变量。如$_GET、$_POST、$_COOKIE、$_FILES、和 $_SERVER;
- Response类抽象出一些PHP函数。如header()、setcookie()和echo;
- Session类和SessionStorageInterface接口抽象会话管理函数。如session_*()。
HttpKernel组件
FrameworkBundle组件
FrameworkBundle组件是联系主要组件和库的钮带,它用于构建一个轻量级的、快速的MVC框架。它具有合理的缺省配置和约定,用以缓和学习曲线。
内核
HttpKernel类是Symfony2的核心类,负责处理客户端的请求。它的主要目标是将Request对象“转换”成Response对象。
每个Symfony2的Kernel都实现HttpKernelInterface接口:
1 |
function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) |
要将Request转换成Response,Kernel依赖“控制器”。控制器可以是任一有效的PHP调用。
Kernel代表控制器选择应该执行哪个ControllerResolverInterface实现:
1 2 3 |
public function getController(Request $request); public function getArguments(Request $request, $controller); |
getController()方法返回与给定Request相关的控制器(一个PHP调用)。缺省实现 (ControllerResolver) 查找请求参数_controller,该参数代表控制器名称(一个”class::method”字符串,如Bundle\BlogBundle\PostController:indexAction)。
getArguments()方法返回一个发送给控制器调用的参数数组。缺省实现基于Request属性自动解析方法参数。
从Request属性匹配控制器方法参数
对于每个访问参数,Symfony2尝试从Request的同名属性中取值。如果没有定义则需要参数的缺省值:
1 2 3 4 5 6 |
// Symfony2 will look for an 'id' attribute (mandatory) // and an 'admin' one (optional) public function showAction($id, $admin = true) { // ... } |
处理请求
handle()方法接受一个Request并总是返回一个Response。为转换Request,handle()依赖Resolver和一个Event通知的步骤(每个Event的详细信息参见下一章节):
- 在做其它任何事之前,发送core.request事件通知。如果监听器返回Response,则直接跳到步骤8;
- 调用Resolver以确定执行Controller;
- core.response事件监听器现在可以按它们希望的方式(改变它、封装它…)调用Controller;
- Kernel检查Controller实际是一个有效的PHP调用;
- 调用Resolver以确定发送给Controller的参数;
如果在处理过程中抛出一个异常,那么发送core.exception事件通知,监听器将得到一个改变去将异常转换成Response。如果正常工作,那么发送core.response事件通知;否则将再次抛出异常。
如果你不想异常被捕获(如内嵌的请求),那么发送false给handle()方法的第3个参数,禁用core.exception事件。
内部请求
有时,在处理请求(“主”请求)时,可以处理子请求。你可以将请求类型发送到handle()方法(它是第2个参数):
- HttpKernelInterface::MASTER_REQUEST;
- HttpKernelInterface::SUB_REQUEST.
事件和监听器可以根据发送的类型处理(一些进程只能在主请求中发生)。
事件
每个通过Kernel抛出的事件都是KernelEvent的子类。这就是说每个事件都可以访问到相同的基本信息:
- getRequestType() – 返回请求类型 (HttpKernelInterface::MASTER_REQUEST或 HttpKernelInterface::SUB_REQUEST);
- getKernel() – 返回处理请求的Kernel;
- getRequest() – 返回当前正在处理的Request。
getRequestType()
getRequestType()方法可以让监听器知道请求类型。例如,如果监听器只能被主请求激活,那么在你监听器方法的开始添加以下代码:
1 2 3 4 5 6 |
use Symfony\Component\HttpKernel\HttpKernelInterface; if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { // return immediately return; } |
如果你对Symfony2事件调度不熟的话,请先查看调度章节。
core.request事件
core.controller事件
该事件没有被FrameworkBundle使用,但可以作为修改应该执行控制器的切入点:
1 2 3 4 5 6 7 8 9 10 |
use Symfony\Component\HttpKernel\Event\FilterControllerEvent; public function onCoreController(FilterControllerEvent $event) { $controller = $event->getController(); // ... // the controller can be changed to any PHP callable $event->setController($controller); } |
core.view事件
事件类:GetResponseForControllerResultEvent
该事件没有被FrameworkBundle使用,但它可以用来实现一个视图子系统。该事件仅当Controller没有返回Response对象时被调用。该事件的目的是让其它的返回值转换成Response。
通过Controller返回的值可以由getControllerResult方法访问:
1 2 3 4 5 6 7 8 9 10 11 |
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpFoundation\Response; public function onCoreView(GetResponseForControllerResultEvent $event) { $val = $event->getReturnValue(); $response = new Response(); // some how customize the Response from the return value $event->setResponse($response); } |
core.response事件
该事件目标是在其被创建后让其它系统去修改或替代Response对象:
1 2 3 4 5 |
public function onCoreResponse(FilterResponseEvent $event) { $response = $event->getResponse(); // .. modify the response object } |
FrameworkBundle注册几个监听器:
- ProfilerListener: 为当前请求收集数据;
- WebDebugToolbarListener: 注入Web调试工具条;
- ResponseListener: 修复基于请求格式的Response的Content-Type;
- EsiListener: 当需要为ESI标签分析Response时添加一个Surrogate-Control HTTP头。
core.exception事件
事件类:GetResponseForExceptionEvent
FrameworkBundle注册一个将Request转发给指定Controller(exception_listener.controller参数的值必须在class::method注释中)的ExceptionListener。
该事件的监听器可以创建和设置一个Response对象,或者什么也不做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpFoundation\Response; public function onCoreException(GetResponseForExceptionEvent $event) { $exception = $event->getException(); $response = new Response(); // setup the Response object based on the caught exception $event->setResponse($response); // you can alternatively set a new Exception // $exception = new \Exception('Some special exception'); // $event->setException($exception); } |
事件调度
面向对象代码在确保代码的扩展性方面已经走了很长的路。通过创建有着明确职责的类,你的代码可以变得更加灵活,并且开发者可以扩展它们的子类去修改它们的行为。但如果他想与其它那些也生成他们自己子类的开发者去分享他的改变时,代码继承已经没有意义。
考虑一下真实世界的例子,你想为你的项目提供一个插件系统。该插件可能添加一个方法,在该方法执行前或后做些事情,而不干扰其它插件。这并不是一个可以通过继承和多重继承(PHP可能做到)就可以容易解决的问题,它们有着自身的缺陷。
Symfony2事件调度用一种简单而有效的方式实现了观察者模式,它可以让这一切都变得可能,并且让你的项目真正可扩展。
就拿Symfon2的HttpKernel组件做一个简单的例子。一旦Response对象被创建,那么在该对象实际生效前让系统中的其它元素可以修改它(如:添加缓存头)是很有用的。为了实现这种可能,Symfony2内核抛出一个事件:core.response。下面是它如何工作的说明:
- 监听器(PHP对象)告诉核心调度对象它希望监听core.response事件;
- Symfony2内核告诉调度对象去调度core.response事件,并将该事件发给可以访问Response对象的事件对象;
- 调度通知(如:调用方法)所有的core.response事件的监听器,允许它们对Response对象做一些修改。
事件
命名约定
- 只使用小写字母、数字、点号(.)和下划线(_);
- 名称空间的前缀后跟点号(如:core.);
- 名字后面跟动词,表明采取什么动作(如:request)。
- core.response
- form.pre_set_data
事件名和Event对象
调度器
调试器是事件调度系统的中心对象。通常,创建一个调度器,它维护着监听器的注册。当一个事件通过该调度器调度时,它通知所有注册了该事件的监听器。
1 2 3 |
use Symfony\Component\EventDispatcher\EventDispatcher; $dispatcher = new EventDispatcher(); |
连接监听器
为了利用现有事件的优点,你需要将监听器连接到调度器,以便当事件被调用时,可以通知它。调用调度器addListener()方法会将任何有效的PHP调用和事件关联起来:
1 2 |
$listener = new AcmeListener(); $dispatcher->addListener('foo.action', array($listener, 'onFooAction')); |
addListener()方法最多需要3个参数:
- 监听器希望监听的事件名(字符串);
- PHP调用,在其监听的事件被抛出时被通知;
- 一个可选的优先级整数(越高越重要),在监听器被触发时用来与其它监听器比较(缺少为0)。如果两个监听器有着相同的优先级,那么它们按照被加入到调拨器的顺序执行。
PHP调用
是一个PHP变量,被call_user_func()函数使用,并且在发送给is_callable()函数时返回true。它可以是\Closure实例,一个表示函数的字符串或表示对象方法和类方法的数组。
到目前为止,你已经明白PHP对象怎么注册成监听器。你也可以将PHPClosures注册成监听器:
1 2 3 4 5 |
use Symfony\Component\EventDispatcher\Event; $dispatcher->addListener('foo.action', function (Event $event) { // will be executed when the foo.action event is dispatched }); |
一旦监听器通过调度器注册后,它将监听直到事件被通知。在上面的例子中当foo.action事件被调用时,调度器调用AcmeListener:onFooAction方法并将Event对象作为单一的参数发送:
1 2 3 4 5 6 7 8 9 10 11 |
use Symfony\Component\EventDispatcher\Event; class AcmeListener { // ... public function onFooAction(Event $event) { // do something } } |
如果你使用Symfony2的MVC框架,监听器可以通过你的配置进行注册。幸运的是,监听器对象仅在需要时被实例化。
在许多情况下,一个特定事件的Event子类被发送到监听器。这样就给监听器权限去访问事件相关的信息。检查每个事件的文档或实现,以确保发送正确的Symfony\Component\EventDispatcher\Event实例。例如,core.event事件发送Symfony\Component\HttpKernel\Event\FilterResponseEvent的一个实例:
1 2 3 4 5 6 7 8 9 |
use Symfony\Component\HttpKernel\Event\FilterResponseEvent public function onCoreResponse(FilterResponseEvent $event) { $response = $event->getResponse(); $request = $event->getRequest(); // ... } |
创建和调拨一个事件
除了注册现有事件的监听器,你还可以创建并抛出自己的事件。这在你创建第三方库时,或者当你想保持你系统不同组件的灵活性和松耦合性是有用的。
静态事件类
假设你想创建一个新的Event,store.order,每次被调拨时在你的应用程序中都会创建命令。为了保持组织性,在你的应用程序中创建StoreEvents类,去定义和记录你的事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace Acme\StoreBundle; final class StoreEvents { /** * The store.order event is thrown each time an order is created * in the system. * * The event listener receives an Acme\StoreBundle\Event\FilterOrderEvent * instance. * * @var string */ const onStoreOrder = 'store.order'; } |
注意,该类其实并不能做什么。StoreEvents类的目标只是成为常用事件集中信息的地方。还需要注意,特殊的FilterOrderEvent类将被发送到该事件的每个监听器。
创建一个Event对象
稍后,当你调拨这个新事件时,你将创建一个Eevent实例并将其发送给调拨器。调拨器然后将同样的实例发送到该事件的每个监听器。如果你不需要发送任何信息到你的监听器,你可以使用缺省的Symfony\Component\EventDispatcher\Event类。然而,大多数时候你需要发送该事件的信息给每个监听器。要完成这一点,你要创建一个新的类去扩展Symfony\Component\EventDispatcher\Event。
在本例中,每个监听器都需要访问一些假想的Order对象。创建一个Event类,使之成为可能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace Acme\StoreBundle\Event; use Symfony\Component\EventDispatcher\Event; use Acme\StoreBundle\Order; class FilterOrderEvent extends Event { protected $order; public function __construct(Order $order) { $this->order = $order; } public function getOrder() { return $this->order; } } |
每个监听器现在可以通过getOrder方法去访问Order对象。
调度事件
dispatch()方法通知所有批定事件的监听器。它有两个参数:要调度的事件和要发送给每个监听器的Event实例:
1 2 3 4 5 6 7 8 9 10 11 |
use Acme\StoreBundle\StoreEvents; use Acme\StoreBundle\Order; use Acme\StoreBundle\Event\FilterOrderEvent; // the order is somehow created or retrieved $order = new Order(); // ... // create the FilterOrderEvent and dispatch it $event = new FilterOrderEvent($order); $dispatcher->dispatch(StoreEvents::onStoreOrder, $event); |
注意指定FilterOrderEvent对象被创建并发送给dispatch方法。现在任何监听store.order事件的监听器将收到FilterOrderEvent,并可以通过getOrder方法访问Order对象:
1 2 3 4 5 6 7 8 |
// some listener class that's been registered for onStoreOrder use Acme\StoreBundle\Event\FilterOrderEvent; public function onStoreOrder(FilterOrderEvent $event) { $order = $event->getOrder(); // do something to or with the order } |
传递给事件调度器对象
如果你看过EventDispatcher类,你将注意到该类并不是一个单例模式(那里没有getInstance()静态方法)动作。这是有意的,因为你可能想在单个PHP请求中有着多个并发事件调度。但它也意味着你需要一种方式去将调拨器发送到需要连接或通知事件的对象。
最好的实践是注入事件调度器对象到你的对象中,也称依赖注入。
你可以使用构造器注入:
1 2 3 4 5 6 7 8 9 |
class Foo { protected $dispatcher = null; public function __construct(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } } |
或者Setter注入:
1 2 3 4 5 6 7 8 9 |
class Foo { protected $dispatcher = null; public function setEventDispatcher(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } } |
在两者之间选择确实是个人习惯的问题。往往会采用构造器注入,因为对象可以在构造时就完全被初始化。但当你有一个长长的依赖列表时,使用Setter注入则是要走的路,尤其是对于可选依赖而言。
如果你象我们在上面两个示例那样使用依赖注入,接下来你就可以使用
Symfony2依赖注入组件来优雅地管理这样对象了。
使用事件订阅
监听事件最常用的方式就是通过调度器注册事件监听器。该监听器可以监听一个或更多事件,并在这些事件每次被调度时通知。
另一种监听事件的方式是通过事件订阅。事件订阅是一个PHP类,它能够准确地告诉调度器哪个事件要被订阅。它实现了
EventSubscriberInterface接口,该接口要求单个名为getSubscribedEvents的静态方法。下面是订阅core.response和store.order事件的订阅示例:
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 |
namespace Acme\StoreBundle\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; class StoreSubscriber implements EventSubscriberInterface { static public function getSubscribedEvents() { return array( 'core.response' => 'onCoreResponse', 'store.order' => 'onStoreOrder', ); } public function onCoreResponse(FilterResponseEvent $event) { // ... } public function onStoreOrder(FilterOrderEvent $event) { // ... } } |
这与监听器类十分相似,除了该类自己可以告诉调度器它将监听哪个事件。要通过调度器注册订阅,可以使用:method:Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber方法:
1 2 3 4 |
use Acme\StoreBundle\Event\StoreSubscriber; $subscriber = new StoreSubscriber(); $dispatcher->addSubscriber($subscriber); |
调度器将自动为每个通过getSubscribedEvents返回的事件注册订阅。象监听器一样,addSubscriber有一个可选的第2参数,它可以给每个事件设置优先级。
停止事件流/传播
在有时情况下,监听器防止事件被其它监听器被调用。换句话说,监听器需要能告诉调度器去停止该事件到其它监听器的所有传播(如:不通知更多的监听器)。这可以在监听器内部通过stopPropagation()方法实现:
1 2 3 4 5 6 7 8 |
use Acme\StoreBundle\Event\FilterOrderEvent; public function onStoreOrder(FilterOrderEvent $event) { // ... $event->stopPropagation(); } |
任何还没有被调用的store.order监听器,现在都不能被调用了。