Event组件分框架内置事件和自定义事件。在程序中埋下事件钩子通过监听器或订阅器实现业务逻辑。它能够使代码的业务逻辑变的非常清晰,增加代码的复用性,代码的耦合性也大大降低。
Kernel抛出的每一个事件都会是KernelEvent类的子类。这就意味着每个事件都可以访问相同的基础信息。
- getRequestType() 返回请求的类型(HttpKernelInterface::MASTER_REQUEST 或者 HttpKernelInterface::SUB_REQUEST)
- getKernel() 返回处理请求的Kernel
- getRequest() 返回一个当前被处理的请求
getRequestType()方法允许知道Listener请求的类型。比如,如果一个Listener必须是主请求才能激活它,你可以把该代码添加到你Listener方法的开头
use Symfony\Component\HttpKernel\HttpKernelInterface;
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
//立刻返回
return;
}
先看下系统内置事件
kernel.request 事件
事件类:GetResponseEvent
该事件的目标是立刻返回一个Response对象或者创建一个在事件结束后Controller可以调用的变量。任何监听器都可以通过event的setResponse()方法返回一个Response对象,当有Response对象被返回时,其它的监听器就不能在被调用了。
FrameworkBundle使用事件通过RouterListener来发布一个_controller 请求属性。
RequestListener 使用一个RouterInterface对象匹配Request,决定哪个Controller的名字会被存储到_controller的请求属性里。
kernel.controller 事件:
事件类: FilterControllerEvent
该事件可以被作为一个修改要执行的controller的一个入口点。初始化控制器稍后需要的东西(如参数转换器),甚至完全更改控制器都很有用.
use Symfony\Component\Httpkernel\Event\FilterControllerEvent;
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
// 此处controller可以被该换成任何PHP可回调函数
$event->setController($controller);
}
kernel.view 事件
事件类:GetResponseForControllerResultEvent
FrameworkBundle也不会使用该事件,但是它被用来实现一个视图子系统。该事件只有当Controller不能返回一个Response对象时才被调用。它的目的就是把其他类型的返回值转换成一个Response。
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$val = $event->getControllerResult();
$response = new Response();
//通过返回值自定义化Response
$event->setResponse($response);
}
kernel.response 事件
事件类:FilterResponseEvent
此事件在控制器或任何kernel.view Listener返回响应对象之后调度。该事件的目的是允许其它系统在Response对象被创建后对它进行修改或者替换。(例如,添加/修改HTTP头、添加cookie等)
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
//修改Response对象
}
kernel.finish_request 事件
此事件在子请求完成后调度。重置应用程序的全局状态很有用(例如,translator Listener将转换器区域设置重置为父请求之一)
public function onKernelFinishRequest(FinishRequestEvent $event)
{
if (null === $parentRequest = $this->requestStack->getParentRequest()) {
return;
}
// reset the locale of the subrequest to the locale of the parent request
$this->setLocale($parentRequest);
}
kernel.terminate
此事件在发送响应之后(在执行handle()方法之后)进行调度。执行不需要完成的缓慢或复杂任务来发送响应(例如发送电子邮件)很有用。
kernel.exception 事件:
事件类:GetResponseForExceptionEvent
FrameworkBundle注册一个ExceptionListener把请求定向到一个给定Contoller。这个事件的Listener可以创建和设置一个Response对象,创建设置一个新的Exception对象或者什么都不做。
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
$response = new Response();
// 基于捕获的异常创建一个Response对象
$event->setResponse($response);
// 你可以创建一个新的异常代替原有的
// $exception = new \Exception('Some special exception');
// $event->setException($exception);
}
查看事件和监听
php bin/console debug:event-dispatcher kernel.exception
假如你开发了一个API;有的资源能对外开放;有的需要验证才能访问资源;这就需要我们对请求来判断是否访问了需要的资源;以及他们所携带的token是否正确。假如我们上面的例子;客户token正确;访问资源完毕;我们需要回调给客户数据;客户那边也需要验证; 这时候我们就可以使用该事件了;
定义一个接口;以便区分哪些控制器需要验证
namespace AppBundle\Controller;
interface TokenAuthenticatedController
{
// ...
}
定义控制器;实现了上述的接口的控制器就需要验证才能访问
namespace AppBundle\Controller;
use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller implements TokenAuthenticatedController
{
// An action that needs authentication
public function barAction()
{
// ...
}
}
####创建一个事件监听者
<?php
namespace AppBundle\EventListener;
use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class TokenListener
{
private $tokens;
public function __construct($tokens)
{
$this->tokens = $tokens;#构造注入
}
//这是kernel.controller事件调用
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
$currentCtrl0bj = $controller['0']; //控制器对象
if ($currentCtrl0bj instanceof TokenAuthenticatedController) {
$token = $event->getRequest()->query->get('token');
if (!in_array($token, $this->tokens)) {
throw new AccessDeniedHttpException('This action needs a valid token!');
}
//验证通过;标记一下回调的auth_token
$event->getRequest()->attributes->set('auth_token', $token);
}
}
//这个是 kernel.response事件分发时候调用的
public function onKernelResponse(FilterResponseEvent $event)
{
//检查是否设置了auth_token
if (!$token = $event->getRequest()->attributes->get('auth_token')) {
return;
}
$response = $event->getResponse();
// 创建一个response header放入加密信息;
$hash = sha1($response->getContent() . $token);
$response->headers->set('X-CONTENT-HASH', $hash);
}
}
####注册监听者
# app/config/services.yml
services:
app.tokens.action_listener:
class: AppBundle\EventListener\TokenListener
arguments: ['%tokens%'] #把上面配置的tokens参数注入给监听者类
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: 100 }
服务标签(Tags):
上面的是定义标签。告诉Symfony这是一个针对kernel.controller事件的“监听”。有一个可选的tag属性是method,它定义了“当事件被触发时,哪个方法要被执行”。默认时,方法的名字是on+“驼峰事件名”。
如果事件是kernel.exception的话,默认执行的方法则是onKernelException()。
另有一个可选的tag属性是priority,它的默认值是0,用来控制监听被执行的顺序(一个监听器的优先级愈高则愈早被执行)。这在你要“确保某个监听在其他监听之前被执行”时是有用的。
Symfony的内部监听,其优先级范围是-255到255,但你自己的监听可以使用任何正或负的整数
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
这里的controller.service_arguments标签就是一个Symfony专用标签,更多专用标签symfony\framework-bundle\DependencyInjection\Compiler\UnusedTagsPass.php
Symfony自定义Event
除了以上框架自带的事件,还支持自定义事件和监听类。
1. 首先定义了一个Events的枚举类用于管理所有的event,通过定义一个枚举类方便的进行所有事件的管理,避免遗忘定义的事件
namespace App\Event;
final class Events
{
const HOMEPAGE_VISIT = "home.homepage_visit";
//下面是BookSubscriber
const BOOK_ENGLISH_NAME = 'english.name';
const BOOK_CHINESE_NAME = 'chinese.name';
const BOOK_MATH_NAME = 'math.name';
const BOOK_KERNEL_RESPONSE = 'kernel.response';
}
2. 自定义了一个event事件namespace App\Event;
// 这个是sf为你提供的一个基础类
use Symfony\Component\EventDispatcher\Event;
// 你的事件类
class HomepageVisit extends Event
{
public $name = self::class;
}
3.定义了一个listener监听
namespace App\EventListener;
use App\Event\HomepageVisit;
class FileListener
{
public function onHomepageVisit(HomepageVisit $event)
{
echo "我执行了\n";
}
}
4. 在service.yaml中注册服务监听相应事件
# config/services.yaml
file.listener:
class: App\EventListener\FileListener
tags:
- { name: kernel.event_listener, event: home.homepage_visit, method: onHomepageVisit }
5.在controller中触发事件
class DefaultController extends Controller
{
public function indexAction()
{
$event = new HomepageVisit();
// 在controller里取事件分发器
$dispatcher = $this->get('event_dispatcher');
$dispatcher->dispatch(Events::HOMEPAGE_VISIT, $event);
}
}
最后通过/home/index访问,控制台成功的打印出"我执行了"的日志。在控制器中移除监听
$listenerInst = null;
$evm = $em->getEventManager();
foreach ($evm->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof FileListener) {
$listenerInst = $listener;
break;
}
}
}
$listenerInst || die('Listener is not registered in the event manager');
// then you can remove events you like:
$evm->removeEventListener(array('onHomepageVisit'), $listenerInst);
最常见的方式是一个事件监听者通过dispatcher注册到某个事件,该监听者可以监听一个或者多个事件并且在每次该事件被分配时获得通知。事件订阅者实现了EventSubscriberInterface接口,它唯一需要实现的一个静态方法叫 getSubscribedEvents
<?php
namespace App\EventListener;
use App\Event\Events;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\Event;
class BookSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
Events::BOOK_CHINESE_NAME => "onChineseNameShow",
Events::BOOK_ENGLISH_NAME => [
["onEnglishNameShow", -10],
["onEnglishNameBefore", 10],
],
Events::BOOK_MATH_NAME => ["onMathNameShow", 100],
Events::BOOK_KERNEL_RESPONSE => 'onKernelResponse'
];
}
public function onKernelResponse(FilterResponseEvent $event)
{
echo "我是内部事件\n";
}
public function onChineseNameShow(Event $event)
{
echo "我是汉语书籍\n";
}
public function onEnglishNameShow(Event $event)
{
echo "我是英文书籍\n";
}
public function onEnglishNameBefore(Event $event)
{
echo "我是展示之后的英文书籍[来自于Event实例{$event->name}]\n";
}
public function onMathNameShow(Event $event)
{
echo "我是展示的数学书籍\n";
}
}
这里dispatcher会自动每一个订阅者的getSubscribedEvents方法返回的事件。该方法会返回一个以事件名字为索引的数组,它的值既可以是调用的方法名也可以是组合了方法名和调用优先级的数组。
根据上面示例的定义,当english.name事件被分配时,其监听方法的调用顺序依次是:
onEnglishNameBefore,onEnglishNameShow
use Symfony\Component\EventDispatcher\EventDispatcher;
use App\Event\Events;
$dispatcher = new EventDispatcher();
$subscriber = new BookSubscriber();
$dispatcher->addSubscriber($subscriber);
$dispatcher->dispatch(Events::BOOK_ENGLISH_NAME, new HomepageVisit());
$dispatcher->dispatch(Events::BOOK_CHINESE_NAME);
$dispatcher->removeSubscriber($subscriber); //移除订阅
$dispatcher->dispatch(Events::BOOK_MATH_NAME);
我是展示之后的英文书籍[来自于Event实例HomepageVisit]
我是英文书籍
我是汉语书籍
监听还是订阅
监听器和订阅器,在同一程序中使用时,可能界限模糊。决定使用哪一种,通常由个人口味决定。但是,每种都有各自的优点:
- 订阅器易于复用,因为与事件有关的内容存在于类中,而不是存在于服务定义中。这导致Symfony内部使用订阅器;
- 监听器更灵活,因为bundles可以有条件地开启或关闭它们,基于配置文件中的某些“选项值”。
依赖注入的常见方式
引用(注入)服务:
它仅仅在它的构造函数中接受一个参数,非常容易配置。只有当我们需要创建一个服务而它又依赖于一个或者多个其它服务时,我们才能看到服务容器的真正威力。
# app/config/services.yml
arguments: [@mailer]
asdf
public function __construct(Mailer $mailer)
{
}
可选依赖:
非构造函数参数注入,而是其他的方法的参数。setter注入就成了更好的选择。
services:
app.tokens.action_listener:
class: AppBundle\EventListener\TokenListener
calls:
- [setMailer, [@mailer] ]
public function setMailer(Mailer $mailer)
{
$this->mailer = $mailer;
}
设置可选引用:
有时候,你的应用可能有一个可选的依赖,这就意味着这个依赖对于你的服务运行不是必须的。
# app/config/services.yml
arguments: [@?mailer]
在YAML配置文件中,@?语法标示告诉服务容器该依赖是可选的。当然也需要相应的修改一下构造函数
public function __construct(Mailer $mailer = null)
{
}