Events

Introduction

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

事件是分离应用程序各个方面的好方法,因为单个事件可以有多个互不依赖的侦听器。例如,您可能希望在每次订单发货时向您的用户发送一条 Slack 通知。您可以提出一个,而不是将您的订单处理代码耦合到您的 Slack 通知代码App\Events\OrderShipped 侦听器可以接收并用于发送 Slack 通知的事件。

注册事件和监听器

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

use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    OrderShipped::class => [
        SendShipmentNotification::class,
    ],
];

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

生成事件和监听器

当然,为每个事件和侦听器手动创建文件很麻烦。相反,将侦听器和事件添加到您的EventServiceProvider 并使用event:generate 工匠命令。此命令将生成您的列表中列出的任何事件或侦听器EventServiceProvider 尚不存在:

php artisan event:generate

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

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

手动注册事件

通常,事件应该通过EventServiceProvider $listen 大批;但是,您也可以在boot 你的方法EventServiceProvider:

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 使用执行监听器queue:

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) {
        // ...
    }));
}

与排队作业一样,您可以使用onConnection,onQueue, 和delay 自定义排队侦听器执行的方法:

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

如果您想处理匿名排队的侦听器失败,您可以关闭catch 方法同时定义queueable 听众。这个闭包将接收事件实例和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) {
    // ...
});

事件发现

而不是在$listen 的数组EventServiceProvider,您可以启用自动事件发现。启用事件发现后,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
    {
        // ...
    }
}

默认情况下禁用事件发现,但您可以通过覆盖shouldDiscoverEvents 你的应用程序的方法EventServiceProvider:

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

默认情况下,应用程序中的所有侦听器app/Listeners 目录将被扫描。如果你想定义额外的目录来扫描,你可以覆盖discoverEventsWithin 你的方法EventServiceProvider:

/**
 * 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 事件收到一个雄辩的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 购买的实例。这SerializesModels 如果使用 PHP 序列化事件对象,则事件使用的特征将优雅地序列化任何 Eloquent 模型serialize 功能,例如在使用时排队的听众.

定义监听器

接下来,让我们看一下示例事件的侦听器。事件侦听器在它们的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...
    }
}

Note
您的事件侦听器还可以类型提示它们在构造函数上需要的任何依赖项。所有事件监听器都通过 Laravel 解析服务容器,因此依赖项将自动注入。

停止传播事件

有时,您可能希望停止将事件传播到其他侦听器。您可以返回false 从你的听众那里handle 方法。

排队的事件监听器

如果您的侦听器要执行缓慢的任务(例如发送电子邮件或发出 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;
}

如果您想在运行时定义侦听器的队列连接或队列名称,您可以定义viaConnection 或者viaQueue 侦听器上的方法:

/**
 * Get the name of the listener's queue connection.
 */
public function viaConnection(): string
{
    return 'sqs';
}

/**
 * Get the name of the listener's queue.
 */
public function viaQueue(): string
{
    return 'listeners';
}

有条件地排队听众

有时,您可能需要根据一些仅在运行时可用的数据来确定是否应将侦听器排队。为了做到这一点,一个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,您仍然可以通过定义一个$afterCommit 侦听器类的属性:

<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public $afterCommit = true;
}

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

处理失败的工作

有时您排队的事件侦听器可能会失败。如果排队的侦听器超过队列工作者定义的最大尝试次数,则failed 方法将在您的侦听器上调用。这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);

Note
在测试时,断言某些事件已被分派而没有实际触发它们的侦听器会很有帮助。 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']
        );
    }
}

如果您的事件侦听器方法是在订阅者内部定义的,您可能会发现从订阅者的subscribe 方法。 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',
        ];
    }
}

注册事件订阅者

编写订阅者后,您就可以将其注册到事件调度程序。您可以使用以下方式注册订阅者$subscribe 上的财产EventServiceProvider.例如,让我们添加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,
    ];
}

Testing

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

使用Event 门面的fake 方法,您可以阻止侦听器执行,执行被测代码,然后断言您的应用程序使用assertDispatched,assertNotDispatched, 和assertNothingDispatched 方法:

<?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();
    }
}

您可以将闭包传递给assertDispatched 或者assertNotDispatched 方法,以便断言已派发的事件通过了给定的“真实性测试”。如果至少发送了一个通过给定真值测试的事件,则断言将成功:

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

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

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

Warning 打电话后Event::fake(),不会执行任何事件侦听器。因此,如果您的测试使用依赖于事件的模型工厂,例如在模型运行期间创建 UUIDcreating 事件,你应该打电话Event::fake() after使用你的工厂。

伪造事件的一个子集

如果你只想为一组特定的事件伪造事件监听器,你可以将它们传递给fake 或者fakeFor 方法:

/**
 * 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([...]);
    }
}
豫ICP备18041297号-2