事件


介绍

Laravel 的事件提供了一个简单的观察者模式实现,允许您订阅和监听应用程序中发生的各种事件。事件类通常存储在 app/Events 目录中,而它们的监听器存储在 app/Listeners 目录中。如果您在应用程序中看不到这些目录,不用担心,当您使用 Artisan 控制台命令生成事件和监听器时,它们将为您创建。

事件作为解耦应用程序各个方面的好方法,因为单个事件可以有多个不依赖于彼此的监听器。例如,每次订单发货时,您可以希望给用户发送一条 Slack 通知。而不是将订单处理代码与 Slack 通知代码耦合在一起,您可以触发一个 App\Events\OrderShipped 事件,监听器可以接收并使用它来发送 Slack 通知。

注册事件和监听器

您的 Laravel 应用程序中包含的 App\Providers\EventServiceProvider 提供了一个方便的位置来注册所有应用程序的事件监听器。监听器属性包含所有事件(键)及其监听器(值)的数组。您可以根据应用程序的需要向此数组中添加任意多个事件。例如,让我们添加一个 OrderShipped 事件:

use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
 
/**
 * The event listener mappings for the application.
 *
 * @var array<class-string, array<int, class-string>>
 */
protected $listen = [
    OrderShipped::class => [
        SendShipmentNotification::class,
    ],
];

event:list 命令可用于显示您的应用程序注册的所有事件和监听器的列表。

生成事件和监听器

当然,手动为每个事件和监听器创建文件是繁琐的。相反,将监听器和事件添加到您的 EventServiceProvider 并使用 event:generate Artisan 命令。此命令将生成在 EventServiceProvider 中列出但尚不存在的任何事件或监听器:

php artisan event:generate

或者,您可以使用 make:eventmake:listener Artisan 命令生成单个事件和监听器:

php artisan make:event PodcastProcessed
 
php artisan make:listener SendPodcastNotification --event=PodcastProcessed

手动注册事件

通常,事件应通过 EventServiceProvider $listen 数组进行注册;但是,您也可以在 EventServiceProviderboot 方法中手动注册基于类或闭包的事件监听器:

use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
 
/**
 * Register any other events for your application.
 */
public function boot(): void
{
    Event::listen(
        PodcastProcessed::class,
        [SendPodcastNotification::class, 'handle']
    );
 
    Event::listen(function (PodcastProcessed $event) {
        // ...
    });
}

可排队的匿名事件监听器

当手动注册基于闭包的事件监听器时,您可以在 Illuminate\Events\queueable 函数中包装监听器闭包,以指示 Laravel 使用队列执行监听器:

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
 
/**
 * Register any other events for your application.
 */
public function boot(): void
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        // ...
    }));
}

与排队的任务一样,您可以使用 onConnectiononQueuedelay 方法来自定义队列监听器的执行:

Event::listen(queueable(function (PodcastProcessed $event) {
    // ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));

如果您希望处理匿名排队的监听器失败,您可以在定义可排队的监听器时向 catch 方法提供一个闭包。此闭包将接收事件实例和引起监听器失败的 Throwable 实例:

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
use Throwable;
 
Event::listen(queueable(function (PodcastProcessed $event) {
    // ...
})->catch(function (PodcastProcessed $event, Throwable $e) {
    // The queued listener failed...
}));

通配符事件监听器

您甚至可以使用*作为通配符参数注册监听器,这样您就可以在同一监听器上捕获多个事件。通配符监听器将事件名称作为第一个参数以及整个事件数据数组作为第二个参数:

Event::listen('event.*', function (string $eventName, array $data) {
    // ...
});

事件发现

您可以启用自动事件发现,而不是在 EventServiceProvider$listen 数组中手动注册事件和监听器。启用事件发现后,Laravel 将通过扫描应用程序的 Listeners 目录自动查找和注册事件和监听器。此外,EventServiceProvider 中明确定义的任何事件仍将被注册。

Laravel 使用 PHP 的反射服务扫描监听器类来找到事件监听器。当 Laravel 发现任何以 handle__invoke 开头的监听器类方法时,Laravel 将将这些方法作为事件监听器注册到方法签名中指定的事件中:

use App\Events\PodcastProcessed;
 
class SendPodcastNotification
{
    /**
     * Handle the given event.
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }
}

默认情况下,事件发现是禁用的,但可以通过覆盖应用程序的 EventServiceProvidershouldDiscoverEvents 方法来启用它:

/**
 * Determine if events and listeners should be automatically discovered.
 */
public function shouldDiscoverEvents(): bool
{
    return true;
}

默认情况下,应用的 app/Listeners 目录中的所有监听器都会被扫描。如果你想定义额外的目录进行扫描,你可以在你的 EventServiceProvider 中重写 discoverEventsWithin 方法:

/**
 * Get the listener directories that should be used to discover events.
 *
 * @return array<int, string>
 */
protected function discoverEventsWithin(): array
{
    return [
        $this->app->path('Listeners'),
    ];
}

生产环境事件发现

在生产环境中,在每个请求上扫描所有监听器并不高效。因此,在部署过程中,您应该运行 event:cache Artisan 命令,以缓存应用程序的所有事件和监听器的清单。框架将使用此清单来加速事件注册过程。可以使用event:clear 命令销毁缓存。

定义事件

事件类本质上是一个数据容器,用于保存与事件相关的信息。例如,假设 App\Events\OrderShipped 事件接收一个 Eloquent ORM 对象:

<?php
 
namespace App\Events;
 
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
 
class OrderShipped
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
 
    /**
     * Create a new event instance.
     */
    public function __construct(
        public Order $order,
    ) {}
}

正如您所见,此事件类不包含任何逻辑。它是一个保存已购买的 App\Models\Order 实例的容器。如果使用 PHP 的 serialize 函数对事件对象进行序列化,如使用排队的监听器时,事件使用的 SerializesModels 特性将优雅地序列化任何 Eloquent 模型。

定义监听器

接下来,让我们看一下示例事件的监听器。事件监听器在它们的 handle 方法中接收事件实例。event:generatemake:listener Artisan命令会自动导入正确的事件类并在 handle 方法上对事件进行类型提示。在 handle 方法中,您可以执行响应事件所需的任何操作:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
 
class SendShipmentNotification
{
    /**
     * Create the event listener.
     */
    public function __construct()
    {
        // ...
    }
 
    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Access the order using $event->order...
    }
}

您的事件监听器还可以在其构造函数中指定它们所需的任何依赖项。所有事件监听器都通过 Laravel 服务容器解析,因此依赖项将自动注入。

停止事件的传播

有时,您可能希望停止事件传播给其他监听器。您可以在监听器的 handle 方法中返回 false 来实现。

排队的事件监听器

如果您的监听器需要执行诸如发送电子邮件或进行 HTTP 请求等耗时任务,则将监听器排队可能会有益处。在使用排队的监听器之前,请确保配置队列并在服务器上或本地开发环境中启动队列工作者。

要指定将监听器排队,将 ShouldQueue 接口添加到监听器类中。由 event:generatemake:listener Artisan 命令生成的监听器已经导入了此接口到当前命名空间中,因此您可以立即使用它:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

就这样!现在,当由此监听器处理的事件被分派时,事件调度程序将使用 Laravel 的队列系统自动将监听器排队。如果监听器在队列中执行时没有抛出任何异常,则在处理完成后,排队的作业将自动删除。

自定义队列连接、名称和延迟

如果您想要自定义事件监听器的队列连接、队列名称或队列延迟时间,可以在监听器类中定义 $connection$queue$delay属性:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    /**
     * The name of the connection the job should be sent to.
     *
     * @var string|null
     */
    public $connection = 'sqs';
 
    /**
     * The name of the queue the job should be sent to.
     *
     * @var string|null
     */
    public $queue = 'listeners';
 
    /**
     * The time (seconds) before the job should be processed.
     *
     * @var int
     */
    public $delay = 60;
}

如果您想在运行时定义监听器的队列连接、队列名称或延迟时间,可以在监听器上定义 viaConnectionviaQueuewithDelay 方法:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    /**
     * The name of the connection the job should be sent to.
     *
     * @var string|null
     */
    public $connection = 'sqs';
 
    /**
     * The name of the queue the job should be sent to.
     *
     * @var string|null
     */
    public $queue = 'listeners';
 
    /**
     * The time (seconds) before the job should be processed.
     *
     * @var int
     */
    public $delay = 60;
}

有条件排队的监听器

有时,您可能需要根据仅在运行时可用的某些数据确定是否排队监听器。为了实现这一点,可以在监听器中添加shouldQueue 方法来确定是否应该将监听器排队。如果 shouldQueue 方法返回 false,将不会执行监听器:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
 
class RewardGiftCard implements ShouldQueue
{
    /**
     * Reward a gift card to the customer.
     */
    public function handle(OrderCreated $event): void
    {
        // ...
    }
 
    /**
     * Determine whether the listener should be queued.
     */
    public function shouldQueue(OrderCreated $event): bool
    {
        return $event->order->subtotal >= 5000;
    }
}

手动与队列交互

如果需要手动访问监听器的底层队列作业的 deleterelease 方法,可以使用Illuminate\Queue\InteractsWithQueue 特性。此特性已默认导入生成的监听器,并提供对这些方法的访问权限:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;
 
    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        if (true) {
            $this->release(30);
        }
    }
}

排队的事件监听器和数据库事务

在数据库事务中调度队列监听器时,它们可能会在数据库事务提交之前被队列处理。当发生这种情况时,在数据库事务期间对模型或数据库记录的任何更新可能尚未在数据库中反映出来。此外,在事务中创建的任何模型或数据库记录可能不存在于数据库中。如果您的监听器依赖于这些模型,当处理调度队列的作业时,可能会导致意外错误。

如果您的队列连接的 after_commit 配置选项设置为 false,则仍然可以指示特定排队的监听器在所有打开的数据库事务提交后调度:

<?php
 
namespace App\Listeners;
 
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;
 
    public $afterCommit = true;
}

要了解有关解决这些问题的更多信息,请查看有关排队作业和数据库事务的文档

处理失败的作业

有时,排队的事件监听器可能会失败。如果由于排队的监听器超过队列工作者定义的最大尝试次数,监听器将在失败方法上被调用。failed 方法接收事件实例和引起失败的 Throwable

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Throwable;
 
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;
 
    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // ...
    }
 
    /**
     * Handle a job failure.
     */
    public function failed(OrderShipped $event, Throwable $exception): void
    {
        // ...
    }
}

指定排队监听器的最大尝试次数

如果您的一个排队监听器遇到错误,您可能不希望它不断重试。因此,Laravel 提供了多种方法来指定监听器在何时或多久内会尝试。

您可以在监听器类上定义 $tries 属性,以指定在监听器被视为失败之前可以尝试多少次:

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;
 
    /**
     * The number of times the queued listener may be attempted.
     *
     * @var int
     */
    public $tries = 5;
}

作为定义监听器可以尝试多少次之前失败的替代方法,您可以定义监听器不再尝试的时间。这允许在给定时间范围内尝试任意次数的监听器。要定义不再尝试监听器的时间,请在监听器类中添加一个 retryUntil 方法。此方法应返回一个 DateTime 实例:

use DateTime;
 
/**
 * Determine the time at which the listener should timeout.
 */
public function retryUntil(): DateTime
{
    return now()->addMinutes(5);
}

事件调度

要调度一个事件,您可以在事件上调用静态的 dispatch 方法。这个方法是由Illuminate\Foundation\Events\Dispatchable 特征提供的。传递给 dispatch 方法的任何参数都将传递给事件的构造函数:

<?php
 
namespace App\Http\Controllers;
 
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
 
class OrderShipmentController extends Controller
{
    /**
     * Ship the given order.
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);
 
        // Order shipment logic...
 
        OrderShipped::dispatch($order);
 
        return redirect('/orders');
    }
}

如果您希望有条件地调度一个事件,您可以使用 dispatchIfdispatchUnless 方法:

OrderShipped::dispatchIf($condition, $order);
 
OrderShipped::dispatchUnless($condition, $order);

在测试时,可以通过断言某些事件被调度而不实际触发它们的监听器来帮助进行测试。Laravel 内置的测试助手使这成为一件轻而易举的事情。

事件订阅者

编写事件订阅者

事件订阅者是可以从订阅者类本身订阅多个事件的类,使您可以在单个类中定义多个事件处理程序。订阅者应该定义一个 subscribe 方法,该方法将传递一个事件调度器实例。您可以在给定的调度器上调用 listen 方法来注册事件监听器:

<?php
 
namespace App\Listeners;
 
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
 
class UserEventSubscriber
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin(string $event): void {}
 
    /**
     * Handle user logout events.
     */
    public function handleUserLogout(string $event): void {}
 
    /**
     * Register the listeners for the subscriber.
     */
    public function subscribe(Dispatcher $events): void
    {
        $events->listen(
            Login::class,
            [UserEventSubscriber::class, 'handleUserLogin']
        );
 
        $events->listen(
            Logout::class,
            [UserEventSubscriber::class, 'handleUserLogout']
        );
    }
}

如果事件监听器方法在订阅者本身中定义,您可能会发现从订阅者的订阅方法返回一个事件和方法名称数组更方便。当注册事件监听器时,Laravel 会自动确定订阅者的类名:

<?php
 
namespace App\Listeners;
 
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
 
class UserEventSubscriber
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin(string $event): void {}
 
    /**
     * Handle user logout events.
     */
    public function handleUserLogout(string $event): void {}
 
    /**
     * Register the listeners for the subscriber.
     *
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

注册事件订阅者

编写完订阅者后,您可以将其与事件调度器一起注册。您可以使用 EventServiceProvider上的 $subscribe 属性来注册订阅者。例如,让我们将 UserEventSubscriber 添加到列表中:

<?php
 
namespace App\Providers;
 
use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
 
class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        // ...
    ];
 
    /**
     * The subscriber classes to register.
     *
     * @var array
     */
    protected $subscribe = [
        UserEventSubscriber::class,
    ];
}

测试

在测试派发事件的代码时,您可能希望指示 Laravel 实际上不执行事件的监听器,因为监听器的代码可以直接进行测试并与调度相应事件的代码分离开来。当然,要测试监听器本身,您可以在测试中实例化监听器实例并直接调用 handle 方法。

使用 Event 门面的 fake 方法,您可以阻止监听器执行,执行测试代码,然后使用 assertDispatchedassertNotDispatchedassertNothingDispatched 方法来断言应用程序派发了哪些事件:

<?php
 
namespace Tests\Feature;
 
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
    /**
     * Test order shipping.
     */
    public function test_orders_can_be_shipped(): void
    {
        Event::fake();
 
        // Perform order shipping...
 
        // Assert that an event was dispatched...
        Event::assertDispatched(OrderShipped::class);
 
        // Assert an event was dispatched twice...
        Event::assertDispatched(OrderShipped::class, 2);
 
        // Assert an event was not dispatched...
        Event::assertNotDispatched(OrderFailedToShip::class);
 
        // Assert that no events were dispatched...
        Event::assertNothingDispatched();
    }
}

您可以将一个闭包传递给 assertDispatchedassertNotDispatched 方法,以便断言已发送通过给定的"truth test"。如果至少有一个通过给定的 truth test 的事件被派发,则断言将成功:

Event::assertDispatched(function (OrderShipped $event) use ($order) {
    return $event->order->id === $order->id;
});

如果您只想断言事件监听器正在监听给定的事件,可以使用 assertListening 方法:

Event::assertListening(
    OrderShipped::class,
    SendShipmentNotification::class
);

调用 Event::fake() 后,不会执行任何事件监听器。因此,如果您的测试使用依赖于事件的模型工厂,例如在模型创建事件期间创建 UUID,您应该在使用工厂后调用 Event::fake()

伪造事件的子集

如果您只想伪造特定一组事件的事件监听器,可以将它们传递给 fakefakeFor 方法:

/**
 * Test order process.
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);
 
    $order = Order::factory()->create();
 
    Event::assertDispatched(OrderCreated::class);
 
    // Other events are dispatched as normal...
    $order->update([...]);
}

您可以使用 except 方法伪造除了一组指定事件以外的所有事件:

Event::fake()->except([
    OrderCreated::class,
]);

作用域事件伪造

如果您只想在测试的一部分中伪造事件监听器,可以使用 fakeFor 方法:

<?php
 
namespace Tests\Feature;
 
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
    /**
     * Test order process.
     */
    public function test_orders_can_be_processed(): void
    {
        $order = Event::fakeFor(function () {
            $order = Order::factory()->create();
 
            Event::assertDispatched(OrderCreated::class);
 
            return $order;
        });
 
        // Events are dispatched as normal and observers will run ...
        $order->update([...]);
    }
}

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 契约

>> 下一篇: 文件存储