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中文网其他相关文章!