事件和事件监听

3.4 版本
维护中的版本

在symfony程序执行期间,大量的事件通知(event notifications)会被触发。你的程序可以监听这些通知,并执行任意代码作为回应。

Symfony自身提供的内部事件,被定义在KernelEvents类中。第三方Bundle和类库也会触发大量事件,你自己的程序可以触发自定义事件

本文展示的所有例子,考虑到一致性,使用了相同的KernelEvents::EXCEPTION事件。在你自己的程序中,你可以使用任何事件,甚至在同一订阅器中(subscriber)混合若干事件。

创建一个事件监听 

监听一个事件最常用的方式是注册一个事件监听(event listener)

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
38
39
40
// src/AppBundle/EventListener/ExceptionListener.php
namespace AppBundle\EventListener;
 
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 
class ExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        // You get the exception object from the received event
        // 你可以从接收到的事件中,取得异常对象
        $exception = $event->getException();
        $message = sprintf(
            'My Error says: %s with code: %s',
            $exception->getMessage(),
            $exception->getCode()
        );
 
        // Customize your response object to display the exception details
        // 自定义响应对象,来显示异常的细节
        $response = new Response();
        $response->setContent($message);
 
        // HttpExceptionInterface is a special type of exception that
        // holds status code and header details
        // HttpExceptionInterface是一个特殊类型的异常,持有状态码和头信息的细节
        if ($exception instanceof HttpExceptionInterface) {
            $response->setStatusCode($exception->getStatusCode());
            $response->headers->replace($exception->getHeaders());
        } else {
            $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
        }
 
        // Send the modified response object to the event
        // 发送修改后的响应对象到事件中
        $event->setResponse($response);
    }
}

每一个事件,都要接收“类型略有不同”的$event对象。对于kernel.exception事件,这个对象是GetResponseForExceptionEvent。要了解每一个“事件监听”所接收到的“事件对象”之类型,参考KernelEvents,或是你要监听的特定事件之文档。

现在,类被创建了,你只需把它注册成服务,然后通过使用一个特殊的“tag”(标签),告诉Symfony这是一个针对kernel.exception事件的“监听”:

1
2
3
4
5
6
# app/config/services.yml
services:
    app.exception_listener:
        class: AppBundle\EventListener\ExceptionListener
        tags:
            - { name: kernel.event_listener, event: kernel.exception }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="app.exception_listener"
            class="AppBundle\EventListener\ExceptionListener">
 
            <tag name="kernel.event_listener" event="kernel.exception" />
        </service>
    </services>
</container>
1
2
3
4
5
// app/config/services.php
$container
    ->register('app.exception_listener', 'AppBundle\EventListener\ExceptionListener')
    ->addTag('kernel.event_listener', array('event' => 'kernel.exception'))
;

有一个可选的tag属性是method,它定义了“当事件被触发时,哪个方法要被执行”。默认时,方法的名字是on+“驼峰事件名”。如果事件是kernel.exception的话,默认执行的方法则是onKernelException()

另有一个可选的tag属性是priority,它的默认值是0,用来控制监听被执行的顺序(一个监听器的优先级愈高则愈早被执行)。这在你要“确保某个监听在其他监听之前被执行”时是有用的。Symfony的内部监听,其优先级范围是-255255,但你自己的监听可以使用任何正或负的整数。

创建一个事件订阅 

另一种监听事件的方式是event subscriber事件订阅,它是一个类,定义了一或多个方法,用于监听一或多个事件。同事件监听的主要区别在于,订阅器始终知道它们正在监听的事件是哪一个。

在一个给定的订阅器中,不同的方法可以监听同一个事件。方法被执行时的顺序,通过每一个方法中的priority参数来定义(优先级愈高则方法愈早被调用)。要了解更多关于订阅器的内容,参考EventDispatcher组件

下例展示了一个事件订阅,定义了若干方法,监听的是同一个kernel.exception事件:

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
// src/AppBundle/EventSubscriber/ExceptionSubscriber.php
namespace AppBundle\EventSubscriber;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
 
class ExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        // return the subscribed events, their methods and priorities
        // 返回被订阅的事件,以及它们的方法和属性
        return array(
           KernelEvents::EXCEPTION => array(
               array('processException', 10),
               array('logException', 0),
               array('notifyException', -10),
           )
        );
    }
 
    public function processException(GetResponseForExceptionEvent $event)
    {
        // ...
    }
 
    public function logException(GetResponseForExceptionEvent $event)
    {
        // ...
    }
 
    public function notifyException(GetResponseForExceptionEvent $event)
    {
        // ...
    }
}

现在,你只需把这个类注册成服务,并打上kernel.event_subscriber标签,即可告诉Symofny这是一个事件订阅器:

1
2
3
4
5
6
# app/config/services.yml
services:
    app.exception_subscriber:
        class: AppBundle\EventSubscriber\ExceptionSubscriber
        tags:
            - { name: kernel.event_subscriber }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="app.exception_subscriber"
            class="AppBundle\EventSubscriber\ExceptionSubscriber">
 
            <tag name="kernel.event_subscriber"/>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
// app/config/services.php
$container
    ->register(
        'app.exception_subscriber',
        'AppBundle\EventSubscriber\ExceptionSubscriber'
    )
    ->addTag('kernel.event_subscriber')
;

Request事件,检查Type 

一个单一页面,可以产生若干次请求(一个主请求[master request],然后是多个子请求[sub-requests],典型的像是如何在模板中嵌入控制器)。对于Symfony核心事件,你可能需要检查一下,看这个事件是一个“主”请求还是一个“子”请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/EventListener/RequestListener.php
namespace AppBundle\EventListener;
 
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
 
class RequestListener
{
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            // don't do anything if it's not the master request
            // 如果不是主请求,就什么也不做
            return;
        }
 
        // ...
    }
}

特定行为,像是对真正的请求进行检查这种,可能并不需要在子请求的监听中进行。

监听还是订阅 

监听器和订阅器,在同一程序中使用时,可能界限模糊。决定使用哪一种,通常由个人口味决定。但是,每种都有各自的优点:

  • 订阅器易于复用,因为与事件有关的内容存在于类中,而不是存在于服务定义中。这导致Symfony内部使用订阅器;

  • 监听器更灵活,因为bundles可以有条件地开启或关闭它们,基于配置文件中的某些“选项值”。

对事件监听器进行调试 

使用命令行,你可以找到“哪些监听被注册到事件派遣器”。要显示全部事件及其监听,运行:

1
$  php bin/console debug:event-dispatcher

通过指定事件名称,你可以得到针对此特定事件进行注册的监听:

1
$  php bin/console debug:event-dispatcher kernel.exception

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)