跳到內容

EventDispatcher 組件

編輯此頁面

EventDispatcher 組件提供的工具,讓您的應用程式組件可以透過調度和監聽事件來彼此溝通。

簡介

物件導向程式碼在確保程式碼可擴展性方面取得了長足的進展。透過建立職責明確的類別,您的程式碼變得更具彈性,開發人員可以使用子類別來擴展它們以修改其行為。但是,如果他們想與其他也建立了自己的子類別的開發人員分享變更,程式碼繼承不再是答案。

考慮一下您想要為專案提供外掛系統的真實範例。外掛應該能夠新增方法,或在方法執行之前或之後執行某些操作,而不會干擾其他外掛。這不是單一繼承容易解決的問題,即使 PHP 可能支援多重繼承,它也有其自身的缺點。

Symfony EventDispatcher 組件實作了中介者觀察者設計模式,使所有這些事情成為可能,並使您的專案真正可擴展。

HttpKernel 組件為例。一旦建立 Response 物件,允許系統中的其他元素在實際使用之前修改它(例如,新增一些快取標頭)可能會很有用。為了實現這一點,Symfony 核心調度了一個事件 - kernel.response。以下是它的運作方式

  • 監聽器(PHP 物件)告訴中央調度器物件,它想要監聽 kernel.response 事件;
  • 在某個時間點,Symfony 核心告訴調度器物件調度 kernel.response 事件,並傳遞一個可以存取 Response 物件的 Event 物件;
  • 調度器通知(即呼叫方法)kernel.response 事件的所有監聽器,允許他們每個人修改 Response 物件。

安裝

1
$ composer require symfony/event-dispatcher

注意

如果您在 Symfony 應用程式之外安裝此組件,則必須在您的程式碼中引入 vendor/autoload.php 檔案,以啟用 Composer 提供的類別自動載入機制。請閱讀這篇文章以取得更多詳細資訊。

用法

另請參閱

本文說明如何在任何 PHP 應用程式中將 EventDispatcher 功能用作獨立組件。請閱讀事件和事件監聽器文章,以了解如何在 Symfony 應用程式中使用它。

事件

當事件被調度時,它會由唯一的名稱(例如 kernel.response)識別,任何數量的監聽器都可能正在監聽它。Event 實例也會被建立並傳遞給所有監聽器。正如您稍後將看到的,Event 物件本身通常包含有關正在調度的事件的資料。

事件名稱和事件物件

當調度器通知監聽器時,它會將實際的 Event 物件傳遞給這些監聽器。基礎 Event 類別包含一個用於停止事件傳播的方法,但沒有太多其他功能。

另請參閱

請閱讀「通用事件物件」以取得有關此基礎事件物件的更多資訊。

通常,關於特定事件的資料需要與 Event 物件一起傳遞,以便監聽器擁有所需的資訊。在這種情況下,當調度事件時,可以傳遞一個特殊的子類別,它具有用於檢索和覆寫資訊的額外方法。例如,kernel.response 事件使用 ResponseEvent,它包含取得甚至替換 Response 物件的方法。

調度器

調度器是事件調度器系統的中心物件。一般來說,會建立一個單一的調度器,它維護監聽器的註冊表。當透過調度器調度事件時,它會通知註冊到該事件的所有監聽器

1
2
3
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();

連接監聽器

若要利用現有的事件,您需要將監聽器連接到調度器,以便在調度事件時可以通知它。呼叫調度器的 addListener() 方法會將任何有效的 PHP 可呼叫物件與事件關聯起來

1
2
$listener = new AcmeListener();
$dispatcher->addListener('acme.foo.action', [$listener, 'onFooAction']);

addListener() 方法最多接受三個引數

  1. 此監聽器想要監聽的事件名稱(字串);
  2. 指定事件被調度時將執行的 PHP 可呼叫物件;
  3. 一個可選的優先順序,定義為正整數或負整數(預設為 0)。數字越大,監聽器被呼叫的時間越早。如果兩個監聽器具有相同的優先順序,則它們會按照新增到調度器的順序執行。

注意

PHP 可呼叫物件是一個 PHP 變數,可以被 call_user_func() 函數使用,並且當傳遞給 is_callable() 函數時會傳回 true。它可以是 \Closure 實例、實作 __invoke() 方法的物件(實際上就是閉包)、表示函數的字串或表示物件方法或類別方法的陣列。

到目前為止,您已經了解了如何將 PHP 物件註冊為監聽器。您也可以將 PHP 閉包註冊為事件監聽器

1
2
3
4
5
use Symfony\Contracts\EventDispatcher\Event;

$dispatcher->addListener('acme.foo.action', function (Event $event): void {
    // will be executed when the acme.foo.action event is dispatched
});

一旦監聽器在調度器中註冊,它就會等待直到事件被通知。在上面的範例中,當 acme.foo.action 事件被調度時,調度器會呼叫 AcmeListener::onFooAction() 方法,並將 Event 物件作為單一引數傳遞

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Contracts\EventDispatcher\Event;

class AcmeListener
{
    // ...

    public function onFooAction(Event $event): void
    {
        // ... do something
    }
}

$event 引數是在調度事件時傳遞的事件物件。在許多情況下,會傳遞具有額外資訊的特殊事件子類別。您可以查看每個事件的文件或實作,以確定傳遞了哪個實例。

僅註冊服務定義並使用 kernel.event_listenerkernel.event_subscriber 標籤標記它們,不足以啟用事件監聽器和事件訂閱器。您還必須在容器建構器中註冊一個名為 RegisterListenersPass() 的編譯器傳遞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;

$container = new ContainerBuilder(new ParameterBag());
// register the compiler pass that handles the 'kernel.event_listener'
// and 'kernel.event_subscriber' service tags
$container->addCompilerPass(new RegisterListenersPass());

$container->register('event_dispatcher', EventDispatcher::class);

// registers an event listener
$container->register('listener_service_id', \AcmeListener::class)
    ->addTag('kernel.event_listener', [
        'event' => 'acme.foo.action',
        'method' => 'onFooAction',
    ]);

// registers an event subscriber
$container->register('subscriber_service_id', \AcmeSubscriber::class)
    ->addTag('kernel.event_subscriber');

RegisterListenersPass 解析別名類別名稱,例如,允許透過事件類別的完整限定類別名稱 (FQCN) 來引用事件。傳遞將從專用容器參數中讀取別名對應。可以透過註冊另一個編譯器傳遞 AddEventAliasesPass 來擴展此參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;

$container = new ContainerBuilder(new ParameterBag());
$container->addCompilerPass(new AddEventAliasesPass([
    \AcmeFooActionEvent::class => 'acme.foo.action',
]));
$container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);

$container->register('event_dispatcher', EventDispatcher::class);

// registers an event listener
$container->register('listener_service_id', \AcmeListener::class)
    ->addTag('kernel.event_listener', [
        // will be translated to 'acme.foo.action' by RegisterListenersPass.
        'event' => \AcmeFooActionEvent::class,
        'method' => 'onFooAction',
    ]);

注意

請注意,AddEventAliasesPass 必須在 RegisterListenersPass 之前處理。

監聽器傳遞假設事件調度器的服務 ID 是 event_dispatcher,事件監聽器標記有 kernel.event_listener 標籤,事件訂閱器標記有 kernel.event_subscriber 標籤,並且別名對應儲存為參數 event_dispatcher.event_aliases

建立和調度事件

除了註冊現有事件的監聽器之外,您還可以建立和調度自己的事件。當建立第三方函式庫以及想要保持自己系統的不同組件彈性和解耦時,這非常有用。

建立事件類別

假設您想要建立一個新的事件,該事件在客戶每次使用您的應用程式訂購產品時調度。當調度此事件時,您將傳遞一個可以存取已下訂單的自訂事件實例。首先建立這個自訂事件類別並記錄它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Acme\Store\Event;

use Acme\Store\Order;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * This event is dispatched each time an order
 * is placed in the system.
 */
final class OrderPlacedEvent extends Event
{
    public function __construct(private Order $order) {}

    public function getOrder(): Order
    {
        return $this->order;
    }
}

現在,每個監聽器都可以透過 getOrder() 方法存取訂單。

調度事件

dispatch() 方法會通知給定事件的所有監聽器。它接受兩個引數:要傳遞給該事件的每個監聽器的 Event 實例,以及要調度的事件的名稱

1
2
3
4
5
6
7
8
9
10
use Acme\Store\Event\OrderPlacedEvent;
use Acme\Store\Order;

// the order is somehow created or retrieved
$order = new Order();
// ...

// creates the OrderPlacedEvent and dispatches it
$event = new OrderPlacedEvent($order);
$dispatcher->dispatch($event);

請注意,特殊的 OrderPlacedEvent 物件被建立並傳遞給 dispatch() 方法。現在,OrderPlacedEvent::class 事件的任何監聽器都將收到 OrderPlacedEvent

注意

如果您不需要將任何其他資料傳遞給事件監聽器,您也可以使用預設的 Event 類別。在這種情況下,您可以在通用的 StoreEvents 類別中記錄事件及其名稱,類似於 KernelEvents 類別

1
2
3
4
5
6
7
8
9
namespace App\Event;

class StoreEvents {

    /**
    * @Event("Symfony\Contracts\EventDispatcher\Event")
    */
    public const ORDER_PLACED = 'order.placed';
}

並使用 Event 類別來調度事件

1
2
3
use Symfony\Contracts\EventDispatcher\Event;

$this->eventDispatcher->dispatch(new Event(), StoreEvents::ORDER_PLACED);

使用事件訂閱器

監聽事件最常見的方式是向調度器註冊事件監聽器。此監聽器可以監聽一個或多個事件,並且每次調度這些事件時都會收到通知。

另一種監聽事件的方式是透過事件訂閱器。事件訂閱器是一個 PHP 類別,它能夠準確地告訴調度器它應該訂閱哪些事件。它實作了 EventSubscriberInterface 介面,該介面需要一個名為 getSubscribedEvents() 的單一靜態方法。以下是一個訂閱器範例,它訂閱了 kernel.responseOrderPlacedEvent::class 事件

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
namespace Acme\Store\Event;

use Acme\Store\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class StoreSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => [
                ['onKernelResponsePre', 10],
                ['onKernelResponsePost', -10],
            ],
            OrderPlacedEvent::class => 'onPlacedOrder',
        ];
    }

    public function onKernelResponsePre(ResponseEvent $event): void
    {
        // ...
    }

    public function onKernelResponsePost(ResponseEvent $event): void
    {
        // ...
    }

    public function onPlacedOrder(OrderPlacedEvent $event): void
    {
        $order = $event->getOrder();
        // ...
    }
}

這與監聽器類別非常相似,只是類別本身可以告訴調度器它應該監聽哪些事件。若要向調度器註冊訂閱器,請使用 addSubscriber() 方法

1
2
3
4
5
use Acme\Store\Event\StoreSubscriber;
// ...

$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);

調度器將自動為 getSubscribedEvents() 方法傳回的每個事件註冊訂閱器。此方法傳回一個陣列,該陣列以事件名稱作為索引,其值是要呼叫的方法名稱,或是由要呼叫的方法名稱和優先順序(預設為 0 的正整數或負整數)組成的陣列。

上面的範例示範了如何在訂閱器中為同一個事件註冊多個監聽器方法,並示範了如何傳遞每個監聽器方法的優先順序。數字越大,方法被呼叫的時間越早。在上面的範例中,當觸發 kernel.response 事件時,方法 onKernelResponsePre()onKernelResponsePost() 會依序被呼叫。

停止事件流/傳播

在某些情況下,監聽器阻止呼叫任何其他監聽器可能是有意義的。換句話說,監聽器需要能夠告訴調度器停止事件向未來監聽器的所有傳播(即,不再通知任何監聽器)。這可以從監聽器內部透過 stopPropagation() 方法完成

1
2
3
4
5
6
7
8
use Acme\Store\Event\OrderPlacedEvent;

public function onPlacedOrder(OrderPlacedEvent $event): void
{
    // ...

    $event->stopPropagation();
}

現在,任何尚未被呼叫的 OrderPlacedEvent::class 的監聽器都將不會被呼叫。

可以使用 isPropagationStopped() 方法來偵測事件是否已停止,該方法會傳回布林值

1
2
3
4
5
// ...
$dispatcher->dispatch($event, 'foo.event');
if ($event->isPropagationStopped()) {
    // ...
}

EventDispatcher 感知事件和監聽器

EventDispatcher 總是將已派發的事件、事件的名稱以及對自身的引用傳遞給監聽器。這可能會導致 EventDispatcher 的一些進階應用,包括在監聽器內部派發其他事件、鏈式事件,甚至是將延遲加載監聽器到派發器物件中。

事件名稱內省

EventDispatcher 實例以及已派發事件的名稱,都會作為參數傳遞給監聽器

1
2
3
4
5
6
7
8
9
10
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class MyListener
{
    public function myEventListener(Event $event, string $eventName, EventDispatcherInterface $dispatcher): void
    {
        // ... do something with the event name
    }
}

其他調度器

除了常用的 EventDispatcher 之外,此組件還帶有一些其他的派發器

本作品,包括程式碼範例,以 Creative Commons BY-SA 3.0 授權條款授權。
TOC
    版本