Symfony2内部

概述

看起来你想要知道Symfony2是如何工作的,以及要如何对它进行扩展。这让我非常高兴。本节深入Symfony2内部,对其进行说明。

如果你想了解Symfony2是如何工作的,或者你想扩展Symfony2的话,你只需要阅读本节。

Symfony2的代码由几个独立层构成,每层都构建在上一层之上。

自动加载没有被框架直接管理;它是在UniversalClassLoader类和src/autoload.php的帮助下独立实现的。更多信息请阅读专用章节
HttpFoundation组件

最底层是HttpFoundation组件。HttpFoundation提供处理HTTP所需的主对象。它是一些PHP函数和变量的面向对象的抽象:
  • Request 类抽象出主要的PHP全局变量。如$_GET、$_POST、$_COOKIE、$_FILES、和 $_SERVER;
  • Response类抽象出一些PHP函数。如header()、setcookie()和echo;
  • Session类和SessionStorageInterface接口抽象会话管理函数。如session_*()。

HttpKernel组件

HttpFoundation层之上是HttpKernel组件。HttpKernel处理HTTP的动态部分;它是一个Request和Response类的瘦封装,是规范请求处理的方式。它也提供了扩展点和工具,使之成为一个理想的基点,用以创建一个没有太大负载的Web框架。
它也可以增加配置性和扩展性,感谢依赖注入组件和强大的插件系统(Bundle)。
请阅读更多关于HttpKernel组件的内容、阅读更多关于依赖注入和Bundle的内容。

FrameworkBundle组件

FrameworkBundle组件是联系主要组件和库的钮带,它用于构建一个轻量级的、快速的MVC框架。它具有合理的缺省配置和约定,用以缓和学习曲线。

 

内核

HttpKernel类是Symfony2的核心类,负责处理客户端的请求。它的主要目标是将Request对象“转换”成Response对象。

每个Symfony2的Kernel都实现HttpKernelInterface接口:


 

要将Request转换成Response,Kernel依赖“控制器”。控制器可以是任一有效的PHP调用。

Kernel代表控制器选择应该执行哪个ControllerResolverInterface实现:


 

getController()方法返回与给定Request相关的控制器(一个PHP调用)。缺省实现 (ControllerResolver) 查找请求参数_controller,该参数代表控制器名称(一个”class::method”字符串,如Bundle\BlogBundle\PostController:indexAction)。

该缺省实现使用RequestListener去定义_controller请求属性(参见 kernel-core_request)。

 

getArguments()方法返回一个发送给控制器调用的参数数组。缺省实现基于Request属性自动解析方法参数。

从Request属性匹配控制器方法参数

对于每个访问参数,Symfony2尝试从Request的同名属性中取值。如果没有定义则需要参数的缺省值:


 

处理请求

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()方法可以让监听器知道请求类型。例如,如果监听器只能被主请求激活,那么在你监听器方法的开始添加以下代码:


 

如果你对Symfony2事件调度不熟的话,请先查看调度章节。

core.request事件

事件类:GetResponseEvent
该事件的目标是立即返回一个Response对象或设置变量,以便可以在事件后调用Controller。任何监听器都可以通过在事件的setResponse()方法返回一个Response对象。在这种情况下,其它所有监听器都不会被调用。
该事件被FrameworkBundle通过RequestListener来填充_controller请求属性。RequestListener使用RouterInterface对象来匹配Request,并确定Controller名(保存在_controller请求属性中)

core.controller事件

事件类:FilterControllerEvent

该事件没有被FrameworkBundle使用,但可以作为修改应该执行控制器的切入点:


 

core.view事件

事件类:GetResponseForControllerResultEvent

该事件没有被FrameworkBundle使用,但它可以用来实现一个视图子系统。该事件仅当Controller没有返回Response对象时被调用。该事件的目的是让其它的返回值转换成Response。

通过Controller返回的值可以由getControllerResult方法访问:


 

core.response事件

事件类:FilterResponseEvent

该事件目标是在其被创建后让其它系统去修改或替代Response对象:


 

FrameworkBundle注册几个监听器:

core.exception事件

事件类:GetResponseForExceptionEvent

FrameworkBundle注册一个将Request转发给指定Controller(exception_listener.controller参数的值必须在class::method注释中)的ExceptionListener

该事件的监听器可以创建和设置一个Response对象,或者什么也不做:


 

 

事件调度

面向对象代码在确保代码的扩展性方面已经走了很长的路。通过创建有着明确职责的类,你的代码可以变得更加灵活,并且开发者可以扩展它们的子类去修改它们的行为。但如果他想与其它那些也生成他们自己子类的开发者去分享他的改变时,代码继承已经没有意义。

考虑一下真实世界的例子,你想为你的项目提供一个插件系统。该插件可能添加一个方法,在该方法执行前或后做些事情,而不干扰其它插件。这并不是一个可以通过继承和多重继承(PHP可能做到)就可以容易解决的问题,它们有着自身的缺陷。

Symfony2事件调度用一种简单而有效的方式实现了观察者模式,它可以让这一切都变得可能,并且让你的项目真正可扩展。

就拿Symfon2的HttpKernel组件做一个简单的例子。一旦Response对象被创建,那么在该对象实际生效前让系统中的其它元素可以修改它(如:添加缓存头)是很有用的。为了实现这种可能,Symfony2内核抛出一个事件:core.response。下面是它如何工作的说明:

  • 监听器(PHP对象)告诉核心调度对象它希望监听core.response事件;
  • Symfony2内核告诉调度对象去调度core.response事件,并将该事件发给可以访问Response对象的事件对象;
  • 调度通知(如:调用方法)所有的core.response事件的监听器,允许它们对Response对象做一些修改。

事件

当一个事件被调用时,它被标识为一个唯一的名字(如:core.response),任何监听器都可以监听它。一个Event实例也被创建并发送给所有的监听器。正如你稍后所见,Event对象自身经常包含正在调用事件的数据。

命名约定

唯一的事件名可以是任意字符串,但最好能够遵循一些简单的命名约定:
  • 只使用小写字母、数字、点号(.)和下划线(_);
  • 名称空间的前缀后跟点号(如:core.);
  • 名字后面跟动词,表明采取什么动作(如:request)。
这里有一些推荐的事件名示例:
  • core.response
  • form.pre_set_data

事件名和Event对象

当调度通知监听器时,它发送一个实际的Event对象到那些监听器。基本的Event类十分简单:它包括一个停止事件传播的方法,仅此而已。
很多时候,特定事件所需的数据会和Event对象一起发送,以便监听器得到所需信息。在core.response事件示例中,创建并发送给每个监听器的Event对象其实是FilterResponseEvent类型,一个基本Event对象的子类。该类包含储如getResponse和setResponse这样的方法,以便让监听器可以得到甚至是替换Response对象。
这个故事的意思是:当为一个事件创建一个监听器时,发送给监听器的Event对象可以是一个特殊的子类,它有着从事件检索信息和响应事件的方法。

调度器

调试器是事件调度系统的中心对象。通常,创建一个调度器,它维护着监听器的注册。当一个事件通过该调度器调度时,它通知所有注册了该事件的监听器。


 

连接监听器

为了利用现有事件的优点,你需要将监听器连接到调度器,以便当事件被调用时,可以通知它。调用调度器addListener()方法会将任何有效的PHP调用和事件关联起来:


 

addListener()方法最多需要3个参数:

  • 监听器希望监听的事件名(字符串);
  • PHP调用,在其监听的事件被抛出时被通知;
  • 一个可选的优先级整数(越高越重要),在监听器被触发时用来与其它监听器比较(缺少为0)。如果两个监听器有着相同的优先级,那么它们按照被加入到调拨器的顺序执行。

PHP调用是一个PHP变量,被call_user_func()函数使用,并且在发送给is_callable()函数时返回true。它可以是\Closure实例,一个表示函数的字符串或表示对象方法和类方法的数组。

到目前为止,你已经明白PHP对象怎么注册成监听器。你也可以将PHPClosures注册成监听器:


 

一旦监听器通过调度器注册后,它将监听直到事件被通知。在上面的例子中当foo.action事件被调用时,调度器调用AcmeListener:onFooAction方法并将Event对象作为单一的参数发送:


 

如果你使用Symfony2的MVC框架,监听器可以通过你的配置进行注册。幸运的是,监听器对象仅在需要时被实例化。

在许多情况下,一个特定事件的Event子类被发送到监听器。这样就给监听器权限去访问事件相关的信息。检查每个事件的文档或实现,以确保发送正确的Symfony\Component\EventDispatcher\Event实例。例如,core.event事件发送Symfony\Component\HttpKernel\Event\FilterResponseEvent的一个实例:


 

创建和调拨一个事件

除了注册现有事件的监听器,你还可以创建并抛出自己的事件。这在你创建第三方库时,或者当你想保持你系统不同组件的灵活性和松耦合性是有用的。

静态事件类

假设你想创建一个新的Event,store.order,每次被调拨时在你的应用程序中都会创建命令。为了保持组织性,在你的应用程序中创建StoreEvents类,去定义和记录你的事件:


 

注意,该类其实并不能做什么。StoreEvents类的目标只是成为常用事件集中信息的地方。还需要注意,特殊的FilterOrderEvent类将被发送到该事件的每个监听器。

创建一个Event对象

稍后,当你调拨这个新事件时,你将创建一个Eevent实例并将其发送给调拨器。调拨器然后将同样的实例发送到该事件的每个监听器。如果你不需要发送任何信息到你的监听器,你可以使用缺省的Symfony\Component\EventDispatcher\Event类。然而,大多数时候你需要发送该事件的信息给每个监听器。要完成这一点,你要创建一个新的类去扩展Symfony\Component\EventDispatcher\Event。

在本例中,每个监听器都需要访问一些假想的Order对象。创建一个Event类,使之成为可能:


 

每个监听器现在可以通过getOrder方法去访问Order对象。

调度事件

dispatch()方法通知所有批定事件的监听器。它有两个参数:要调度的事件和要发送给每个监听器的Event实例:


 

注意指定FilterOrderEvent对象被创建并发送给dispatch方法。现在任何监听store.order事件的监听器将收到FilterOrderEvent,并可以通过getOrder方法访问Order对象:


 

传递给事件调度器对象

如果你看过EventDispatcher类,你将注意到该类并不是一个单例模式(那里没有getInstance()静态方法)动作。这是有意的,因为你可能想在单个PHP请求中有着多个并发事件调度。但它也意味着你需要一种方式去将调拨器发送到需要连接或通知事件的对象。

最好的实践是注入事件调度器对象到你的对象中,也称依赖注入。

你可以使用构造器注入:


 

或者Setter注入:


在两者之间选择确实是个人习惯的问题。往往会采用构造器注入,因为对象可以在构造时就完全被初始化。但当你有一个长长的依赖列表时,使用Setter注入则是要走的路,尤其是对于可选依赖而言。

如果你象我们在上面两个示例那样使用依赖注入,接下来你就可以使用Symfony2依赖注入组件来优雅地管理这样对象了。

使用事件订阅

监听事件最常用的方式就是通过调度器注册事件监听器。该监听器可以监听一个或更多事件,并在这些事件每次被调度时通知。

另一种监听事件的方式是通过事件订阅。事件订阅是一个PHP类,它能够准确地告诉调度器哪个事件要被订阅。它实现了EventSubscriberInterface接口,该接口要求单个名为getSubscribedEvents的静态方法。下面是订阅core.response和store.order事件的订阅示例:

这与监听器类十分相似,除了该类自己可以告诉调度器它将监听哪个事件。要通过调度器注册订阅,可以使用:method:Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber方法:


 

调度器将自动为每个通过getSubscribedEvents返回的事件注册订阅。象监听器一样,addSubscriber有一个可选的第2参数,它可以给每个事件设置优先级。

停止事件流/传播

在有时情况下,监听器防止事件被其它监听器被调用。换句话说,监听器需要能告诉调度器去停止该事件到其它监听器的所有传播(如:不通知更多的监听器)。这可以在监听器内部通过stopPropagation()方法实现:


 

任何还没有被调用的store.order监听器,现在都不能被调用了。

 

发表评论