Laravel 的模型事件是一個非常方便的功能,它可以幫助您在對 Eloquent 模型執行某些操作時自動運行邏輯。但是,如果使用不當,有時可能會導致奇怪的副作用。
本文將探討模型事件是什麼以及如何在 Laravel 應用程序中使用它們。我們還將探討如何測試模型事件以及使用它們時需要注意的一些問題。最後,我們將介紹一些您可以考慮使用的模型事件替代方法。
您可能已經聽說過“事件”和“監聽器”。但如果您沒有聽說過,以下是它們的簡要概述:
這些是您希望在其上採取行動的應用程序中發生的事情——例如,用戶在您的網站上註冊、用戶登錄等。
通常,在 Laravel 中,事件是 PHP 類。除了框架或第三方包提供的事件外,它們通常保存在 app/Events
目錄中。
以下是一個簡單的事件類的示例,您可能希望在用戶註冊到您的網站時調度它:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
在上面的基本示例中,我們有一個 AppEventsUserRegistered
事件類,在其構造函數中接受一個 User
模型實例。此事件類是一個簡單的容器,用於保存已註冊的用戶實例。
調度時,事件將觸發任何正在偵聽它的監聽器。
以下是如何在用戶註冊時調度該事件的簡單示例:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
在上面的示例中,我們正在創建一個新用戶,然後使用用戶實例調度 AppEventsUserRegistered
事件。假設監聽器已正確註冊,這將觸發任何正在偵聽 AppEventsUserRegistered
事件的監聽器。
監聽器是您希望在特定事件發生時運行的代碼塊。
例如,堅持我們的用戶註冊示例,您可能希望在用戶註冊時向用戶發送歡迎電子郵件。您可以創建一個監聽器來偵聽 AppEventsUserRegistered
事件並發送歡迎電子郵件。
在 Laravel 中,監聽器通常(但並非總是如此——稍後我們將介紹這一點)是在 app/Listeners
目錄中找到的類。
在用戶註冊時向用戶發送歡迎電子郵件的監聽器示例可能如下所示:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
正如我們在上面的代碼示例中看到的,AppListenersSendWelcomeEmail
監聽器類有一個 handle
方法,它接受一個 AppEventsUserRegistered
事件實例。此方法負責向用戶發送歡迎電子郵件。
有關事件和監聽器的更深入說明,您可能需要查看官方文檔:https://www.php.cn/link/d9a8c56824cfbe66f28f85edbbe83e09
在您的 Laravel 應用程序中,您通常需要在發生某些操作時手動調度事件。正如我們在上面的示例中看到的,我們可以使用以下代碼調度事件:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
但是,當在 Laravel 中使用 Eloquent 模型時,會自動為我們調度一些事件,因此我們不需要手動調度它們。如果我們希望在事件發生時執行操作,我們只需要為它們定義監聽器即可。
下面的列表顯示了 Eloquent 模型自動調度的事件及其觸發器:
在上面的列表中,您可能會注意到一些事件名稱相似;例如,creating
和 created
。以 ing
結尾的事件在操作發生之前執行,並且更改會持久保存到數據庫中。而以 ed
結尾的事件在操作發生之後執行,並且更改會持久保存到數據庫中。
讓我們看看如何在 Laravel 應用程序中使用這些模型事件。
dispatchesEvents
偵聽模型事件偵聽模型事件的一種方法是在您的模型上定義一個 dispatchesEvents
屬性。
此屬性允許您將 Eloquent 模型事件映射到事件發生時應調度的事件類。這意味著您可以像處理任何其他事件一樣定義監聽器。
為了提供更多上下文,讓我們來看一個示例。
假設我們正在構建一個具有兩個模型的博客應用程序:AppModelsPost
和 AppModelsAuthor
。我們將說這兩個模型都支持軟刪除。當我們保存新的 AppModelsPost
時,我們希望根據內容的長度計算文章的閱讀時間。當我們軟刪除作者時,我們希望軟刪除作者的所有文章。
我們可能有一個如下所示的 AppModelsAuthor
模型:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
在上面的模型中,我們有:
dispatchesEvents
屬性,它將 deleted
模型事件映射到 AppEventsAuthorDeleted
事件類。這意味著當模型被刪除時,將調度一個新的 AppEventsAuthorDeleted
事件。我們將在稍後創建此事件類。 posts
關係。 IlluminateDatabaseEloquentSoftDeletes
特性在模型上啟用了軟刪除。 現在讓我們創建我們的 AppModelsPost
模型:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
在上面的 AppModelsPost
模型中,我們有:
dispatchesEvents
屬性,它將 saving
模型事件映射到 AppEventsPostSaving
事件類。這意味著當模型被創建或更新時,將調度一個新的 AppEventsPostSaving
事件。我們將在稍後創建此事件類。 author
關係。 IlluminateDatabaseEloquentSoftDeletes
特性在模型上啟用了軟刪除。 我們的模型現在已準備就緒,因此讓我們創建我們的 AppEventsAuthorDeleted
和 AppEventsPostSaving
事件類。
我們將創建一個 AppEventsPostSaving
事件類,該類將在保存新文章時調度:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
在上面的代碼中,我們可以看到 AppEventsPostSaving
事件類,它在其構造函數中接受一個 AppModelsPost
模型實例。此事件類是一個簡單的容器,用於保存正在保存的文章實例。
類似地,我們可以創建一個 AppEventsAuthorDeleted
事件類,該類將在刪除作者時調度:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
在上面的 AppEventsAuthorDeleted
類中,我們可以看到構造函數接受一個 AppModelsAuthor
模型實例。
現在我們可以繼續創建監聽器了。
讓我們首先創建一個可以用來計算文章估計閱讀時間的監聽器。
我們將創建一個新的 AppListenersCalculateReadTime
監聽器類:
UserRegistered::dispatch($user);
正如我們在上面的代碼中看到的,我們只有一個 handle
方法。這是在調度 AppEventsPostSaving
事件時將自動調用的方法。它接受 AppEventsPostSaving
事件類的實例,其中包含正在保存的文章。
在 handle
方法中,我們使用一個簡單的公式來計算文章的閱讀時間。在這個例子中,我們假設平均閱讀速度為每分鐘 265 個單詞。我們正在計算以秒為單位的閱讀時間,然後在文章模型上設置 read_time_in_seconds
屬性。
由於此監聽器將在觸發 saving
模型事件時調用,這意味著每次創建或更新文章之前將其持久保存到數據庫中時,都會計算 read_time_in_seconds
屬性。
我們還可以創建一個監聽器,在軟刪除作者時軟刪除所有相關的文章。
我們可以創建一個新的 AppListenersSoftDeleteAuthorRelationships
監聽器類:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
在上面的監聽器中,handle
方法接受 AppEventsAuthorDeleted
事件類的實例。此事件類包含正在刪除的作者。然後,我們使用 posts
關係上的 delete
方法刪除作者的文章。
因此,每當軟刪除 AppModelsAuthor
模型時,所有作者的文章也將被軟刪除。
順便說一句,值得注意的是,您可能希望使用更強大、更可重用的解決方案來實現此目的。但出於本文的目的,我們將其保持簡單。
您可以使用的另一種方法是在模型本身上定義監聽器作為閉包。
讓我們來看一下我們之前軟刪除作者時軟刪除文章的示例。我們可以更新我們的 AppModelsAuthor
模型以包含一個偵聽 deleted
模型事件的閉包:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
我們在上面的模型中可以看到,我們正在模型的 booted
方法中定義監聽器。我們想偵聽 deleted
模型事件,所以我們使用了 self::deleted
。同樣,如果我們想為 created
模型事件創建一個監聽器,我們可以使用 self::created
,依此類推。 self::deleted
方法接受一個閉包,該閉包接收正在被刪除的 AppModelsAuthor
。此閉包將在刪除模型時執行,因此會刪除所有作者的文章。
我非常喜歡這種定義監聽器邏輯的方法,因為它在打開模型類時可以立即清楚地看到它是否註冊了觀察者。因此,儘管邏輯仍然“隱藏”在單獨的文件中,但我們可以知道我們至少為模型的一個事件註冊了監聽器。但是,如果這些閉包中的代碼變得更複雜,則可能值得將邏輯提取到單獨的監聽器類中。
一個方便的小技巧是,您還可以使用 IlluminateEventsqueueable
函數使閉包可排隊。這意味著監聽器的代碼將被推送到隊列中,以便在後台運行,而不是在同一個請求生命週期中運行。我們可以將監聽器更新為可排隊的,如下所示:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
正如我們在上面的示例中看到的,我們將閉包包裝在 IlluminateEventsqueueable
函數中。
您可以採取的另一種偵聽模型事件的方法是使用模型觀察者。模型觀察者允許您在一個類中為模型定義所有監聽器。
通常,它們是在 app/Observers
目錄中存在的類,並且它們具有對應於您想要偵聽的模型事件的方法。例如,如果您想偵聽 deleted
模型事件,您將在觀察者類中定義一個 deleted
方法。如果您想偵聽 created
模型事件,您將在觀察者類中定義一個 created
方法,依此類推。
讓我們看看如何為我們的 AppModelsAuthor
模型創建一個偵聽 deleted
模型事件的模型觀察者:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
正如我們在上面的代碼中看到的,我們創建了一個具有 deleted
方法的觀察者。此方法接受正在刪除的 AppModelsAuthor
模型的實例。然後,我們使用 posts
關係上的 delete
方法刪除作者的文章。
假設,例如,我們還想為 created
和 updated
模型事件定義監聽器。我們可以像這樣更新我們的觀察者:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
為了運行 AppObserversAuthorObserver
方法,我們需要指示 Laravel 使用它。為此,我們可以使用 #[IlluminateDatabaseEloquentAttributesObservedBy]
屬性。這允許我們將觀察者與模型關聯起來,這與我們使用 #[ScopedBy]
屬性註冊全局查詢範圍的方式類似(如在了解如何在 Laravel 中掌握查詢範圍中所示)。我們可以像這樣更新我們的 AppModelsAuthor
模型以使用觀察者:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
我真的很喜歡這種定義監聽器邏輯的方式,因為它在打開模型類時可以立即清楚地看到它是否註冊了觀察者。因此,儘管邏輯仍然“隱藏”在單獨的文件中,但我們可以知道我們至少為模型的一個事件註冊了監聽器。
無論您使用哪種模型事件方法,您都可能希望編寫一些測試以確保您的邏輯按預期運行。
讓我們看看如何測試我們在上面示例中創建的模型事件。
我們首先編寫一個測試,以確保在軟刪除作者時會軟刪除作者的文章。測試可能如下所示:
UserRegistered::dispatch($user);
在上面的測試中,我們正在創建一個新的作者和該作者的文章。然後我們軟刪除作者並斷言作者和文章都被軟刪除了。
這是一個非常簡單但有效的測試,我們可以用它來確保我們的邏輯按預期工作。這種測試的優點是它應該適用於我們在本文中討論的每種方法。因此,如果您在我們在本文中討論的任何方法之間切換,您的測試仍然應該通過。
同樣,我們還可以編寫一些測試來確保在創建或更新文章時計算文章的閱讀時間。測試可能如下所示:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
我們在上面有兩個測試:
儘管模型事件非常方便,但在使用它們時需要注意一些問題。
模型事件僅從 Eloquent 模型調度。這意味著,如果您使用 IlluminateSupportFacadesDB
facade 與數據庫中模型的基礎數據交互,則不會調度其事件。
例如,讓我們來看一個簡單的示例,我們使用 IlluminateSupportFacadesDB
facade 刪除作者:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
運行上面的代碼將按預期從數據庫中刪除作者。但是,不會調度 deleting
和 deleted
模型事件。因此,如果您在刪除作者時為這些模型事件定義了任何監聽器,則不會運行它們。
同樣,如果您使用 Eloquent 批量更新或刪除模型,則不會為受影響的模型調度 saved
、updated
、deleting
和 deleted
模型事件。這是因為事件是從模型本身調度的。但是,在批量更新和刪除時,模型實際上並沒有從數據庫中檢索,因此不會調度事件。
例如,假設我們使用以下代碼刪除作者:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
由於 delete
方法直接在查詢構建器上調用,因此不會為該作者調度 deleting
和 deleted
模型事件。
我喜歡在我的項目中使用模型事件。它們作為一種很好的方法來解耦我的代碼,並且還允許我在對影響模型的代碼沒有太多控制權時自動運行邏輯。例如,如果我在 Laravel Nova 中刪除作者,我仍然可以在刪除作者時運行一些邏輯。
但是,了解何時考慮使用不同的方法非常重要。
為了解釋這一點,讓我們來看一個我們可能想要避免使用模型事件的基本示例。擴展我們前面簡單的博客應用程序示例,假設我們希望在創建新文章時運行以下操作:
因此,我們可能會創建三個單獨的監聽器(每個任務一個),這些監聽器每次創建新的 AppModelsPost
實例時都會運行。
但是現在讓我們回顧一下我們之前的測試之一:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
如果我們運行上面的測試,當通過其工廠創建 AppModelsPost
模型時,它還會觸發這三個操作。當然,計算閱讀時間是一項次要任務,因此關係不大。但我們不想在測試期間嘗試進行 API 調用或發送通知。這些是意外的副作用。如果編寫測試的開發人員沒有意識到這些副作用,則可能難以追踪這些操作發生的原因。
我們還希望避免在監聽器中編寫任何特定於測試的邏輯,這會阻止這些操作在測試期間運行。這將使應用程序代碼更複雜,更難以維護。
這就是您可能想要考慮更明確的方法而不是依賴於自動模型事件的情況之一。
一種方法可以是將您的 AppModelsPost
創建代碼提取到服務或操作類中。例如,一個簡單的服務類可能如下所示:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
在上面的類中,我們正在手動調用計算閱讀時間、發送通知和將其發佈到 Twitter 的代碼。這意味著我們可以更好地控制何時運行這些操作。我們還可以輕鬆地模擬測試中的這些方法以防止它們運行。如果需要,我們仍然可以排隊這些操作(在這種情況下我們很可能會這樣做)。
因此,我們可以刪除這些操作的模型事件和監聽器的使用。這意味著我們可以在應用程序代碼中使用這個新的 AppServicesPostService
類,並在測試代碼中安全地使用模型工廠。
這樣做的額外好處是它還可以使代碼更容易理解。正如我簡要提到的那樣,使用事件和監聽器的常見批評是它可能會在意外的地方隱藏業務邏輯。因此,如果新的開發人員加入團隊,如果它們由模型事件觸發,他們可能不知道某些操作在哪里或為什麼發生。
但是,如果您仍然希望為此類邏輯使用事件和監聽器,您可以考慮使用更明確的方法。例如,您可以從服務類調度一個事件來觸發監聽器。這樣,您仍然可以使用事件和監聽器的解耦優勢,但您可以更好地控制何時調度事件。
例如,我們可以更新上面我們 AppServicesPostService
示例中的 createPost
方法以調度事件:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
通過使用上述方法,我們仍然可以擁有單獨的監聽器來向 Twitter 發出 API 請求並發送通知。但是我們可以更好地控制何時運行這些操作,因此在使用模型工廠進行測試時不會運行它們。
在決定使用這些方法中的任何一種時,沒有任何黃金法則。這完全取決於您、您的團隊以及您正在構建的功能。但是,我傾向於遵循以下經驗法則:
為了快速總結我們在本文中介紹的內容,以下是使用模型事件的一些優缺點:
希望本文能為您概述模型事件是什麼以及使用它們的各種方法。它還應該向您展示瞭如何測試模型事件代碼以及使用它們時需要注意的一些問題。
您現在應該有足夠的信心在您的 Laravel 應用程序中使用模型事件了。
以上是Laravel的指南的詳細內容。更多資訊請關注PHP中文網其他相關文章!