事件篇

事件在程序设计中是常见的解耦方式,也是常见的扩展方式。在Symfony应用程序中事件贯穿了整个框架,你可以通过框架的事件修改任何支持的结果。

事件定义

事件的定义有多种方式,服务tag、事件订阅EventSubscriberInterface, 属性AsEventListener

服务tag

注册事件监听类,下面以监听框架的ExceptionEvent为例。

// src/EventListener/ExceptionListener.php
namespace App\EventListener;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;

class ExceptionListener
{
    public function __invoke(ExceptionEvent $event): void
    {
        echo "event trigger";
    }
}

为事件处理器打上tag.

// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\EventListener\ExceptionListener;

return function(ContainerConfigurator $container): void {
    $services = $container->services();

    $services->set(ExceptionListener::class)
        ->tag('kernel.event_listener')
    ;
};

Symfony遵循以下逻辑来决定在事件监听器类中调用哪个方法:

  1. 如果kernel.event_listener标记定义了方法属性,这就是要调用的方法名称;
  2. 如果没有定义方法属性,则尝试调用__invoke()魔术方法(该方法使事件监听器可被调用);
  3. 如果也没有定义__invoke()方法,则抛出异常。
// custom method
$services->set(ExceptionListener::class)
        ->tag('kernel.event_listener', ['method' => 'listenerMethod']); 

属性AsEventListener

PHP 8中支持了原生的attributes, Symfony也在6.2版本中支持了多种属性。了解更多属性信息请查看此文章

// src/EventListener/ExceptionListener.php
namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;

#[AsEventListener(event: ExceptionEvent::class)]
#[AsEventListener(event: 'custom_event',  method: "customEvent")]
class ExceptionListener
{
    public function __invoke(ExceptionEvent $event): void
    {
        echo "event trigger";
    }
    
    public function customEvent(ExceptionEvent $event): void 
    {
        echo "custom_event event trigger.";
    }
}

当然,AsEventListener也可以直接应用于方法上:

// src/EventListener/ExceptionListener.php
namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;

class ExceptionListener
{
    #[AsEventListener]
    public function __invoke(ExceptionEvent $event): void
    {
        echo "event trigger";
    }
    
    #[AsEventListener(event: 'custom_event')]
    public function customEvent(ExceptionEvent $event): void 
    {
        echo "custom_event event trigger.";
    }
}

请注意,如果该方法已经类型提示了预期事件,则该属性不需要设置其事件参数。

事件订阅

事件订阅者它是一个定义了监听一个或多个事件的方法的类。它与事件监听器的主要区别在于,订阅者始终知道自己要监听的事件。

// src/EventSubscriber/ExceptionSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class ExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        // 订阅事件,有三个被调用的方法.
        return [
            KernelEvents::EXCEPTION => [
                ['processException', 10],
                ['logException', 0],
                ['notifyException', -10],
            ],
        ];
    }
    
    public function processException(ExceptionEvent $event): void
    {
        // 处理异常
    }

    public function logException(ExceptionEvent $event): void
    {
        // 记录异常
    }

    public function notifyException(ExceptionEvent $event): void
    {
        // 通知异常
    }
}

之后在您的services.{yaml|php}文件应该已经设置为从EventSubscriber目录加载服务。Symfony会处理剩下的事情。

// config/services.php 

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $configurator, ContainerBuilder $container) {
    $services = $configurator->services()
        ->defaults()
        ->autowire()
        ->autoconfigure();
    $services->load("App\\", "../src/*")
         ->exclude('../src/{DependencyInjection,Entity,Tests,Kernel.php}');

};

事件顺序

如果不同的事件被多个订阅者或监听器同一监听,其顺序由优先级参数(priority)决定。该值是一个正或负整数,默认值为0,数字越大,方法被调用的时间越早。 所有监听器和订阅器的优先级都是汇总的,因此您的方法可能会在其他监听器和订阅器定义的方法之前或之后被调用。

调试事件监听器

Symfonyconsole提供了开箱即用的事件调试命令。

您可以使用控制台了解在事件调度程序中注册了哪些侦听器。要显示所有事件及其侦听器,请运行:

$ php bin/console debug:event-dispatcher

您可以通过指定特定事件的名称来获取其注册的侦听器:

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

或者可以获得与事件名称部分匹配的所有内容:

# 匹配 "kernel.exception", "kernel.response" 等
$ php bin/console debug:event-dispatcher kernel 
# 匹配 "Symfony\Component\Security\Http\Event\CheckPassportEvent"
$ php bin/console debug:event-dispatcher Security 

调试显示没有达到你的预期说明监听器没有被注册,你需要检测你的services.yaml文件。

示例RequestID

通过上面的学习,我们编写一个拦截所有请求结果为其加上一个RequestId头或者json字段。

我们需要拦截请求异常和请求完成两个事件。

实现RequestID拦截

// src/EventListener/RequestIDListener.php
namespace App\EventListener;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\Uid\Uuid;

class RequestIDListener
{
    #[AsEventListener(priority: -127)]
    public function exception(ExceptionEvent $event): void
    {
        // 设置了异常的response 会继续发送response事件
        if (!$event->hasResponse()) {
            $event->setResponse(new JsonResponse([
                'message' => $event->getThrowable()->getMessage(),
            ]));
        }
    }
    #[AsEventListener(priority: -127)]
    public function response(ResponseEvent $event): void 
    {
        $response = $event->getResponse();
        $uuid = Uuid::v7();
        $response->headers->set("request_id", $uuid);
        if ($response instanceof JsonResponse) {
            $content = $response->getContent();
            $jsonArray = json_decode($content, true);
            $jsonArray['request_id'] = $uuid;
            $response->setData($jsonArray);
        }
    }
}

以上就是整个注入request_id的实现,代码中使用了uuid组件库。使用以下命令安装:

$ composer require symfony/uid

协作

如果你有更多问题或使用方法,可以通过github提交pr请求。有问题可以开issue编辑此页面