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()
方法最多接受三個引數
- 此監聽器想要監聽的事件名稱(字串);
- 指定事件被調度時將執行的 PHP 可呼叫物件;
- 一個可選的優先順序,定義為正整數或負整數(預設為
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
引數是在調度事件時傳遞的事件物件。在許多情況下,會傳遞具有額外資訊的特殊事件子類別。您可以查看每個事件的文件或實作,以確定傳遞了哪個實例。
建立和調度事件
除了註冊現有事件的監聽器之外,您還可以建立和調度自己的事件。當建立第三方函式庫以及想要保持自己系統的不同組件彈性和解耦時,這非常有用。
建立事件類別
假設您想要建立一個新的事件,該事件在客戶每次使用您的應用程式訂購產品時調度。當調度此事件時,您將傳遞一個可以存取已下訂單的自訂事件實例。首先建立這個自訂事件類別並記錄它
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.response
和 OrderPlacedEvent::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
}
}