Laravel 13 事件系统进阶

Laravel 的事件系统实现了观察者模式,允许在应用程序中订阅和监听各种事件。本文将深入探讨 Laravel 13 事件系统的高级用法。

定义事件

创建事件类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\Order;

class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public function __construct(
public Order $order,
public string $trackingNumber,
public ?string $notes = null
) {}
}

使用 SerializesModels

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App\Events;

use Illuminate\SerializesModels;
use App\Models\User;

class UserRegistered
{
use SerializesModels;

public function __construct(
public User $user
) {}
}

定义监听器

创建监听器

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
<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Services\ShippingService;

class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;

public function __construct(
private ShippingService $shippingService
) {}

public function handle(OrderShipped $event): void
{
$this->shippingService->sendNotification(
$event->order,
$event->trackingNumber
);
}

public function failed(OrderShipped $event, \Throwable $exception): void
{
\Log::error('Shipment notification failed', [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
}
}

队列监听器

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
37
38
39
40
41
42
<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendWelcomeEmail implements ShouldQueue
{
use InteractsWithQueue;

public string $queue = 'emails';

public int $delay = 60;

public int $tries = 3;

public int $backoff = [10, 30, 60];

public bool $shouldFailOnTimeout = true;

public function handle(UserRegistered $event): void
{
\Mail::to($event->user)->send(new WelcomeEmail($event->user));
}

public function shouldQueue(UserRegistered $event): bool
{
return $event->user->email_verified;
}

public function viaConnection(): string
{
return 'redis';
}

public function viaQueue(): string
{
return 'low-priority';
}
}

注册事件和监听器

在 EventServiceProvider 中注册

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
<?php

namespace App\Providers;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
use App\Listeners\UpdateUserLoginTime;
use App\Listeners\ClearUserSession;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
protected $listen = [
OrderShipped::class => [
SendShipmentNotification::class,
UpdateInventory::class,
LogShipment::class,
],
Login::class => [
UpdateUserLoginTime::class,
],
Logout::class => [
ClearUserSession::class,
],
];

protected $subscribe = [
UserEventSubscriber::class,
];
}

自动发现

1
2
3
4
5
6
7
8
9
10
11
12
13
// EventServiceProvider.php
public function shouldDiscoverEvents(): bool
{
return true;
}

protected function discoverEventsWithin(): array
{
return [
$this->app->path('Listeners'),
$this->app->path('Modules/*/Listeners'),
];
}

分发事件

基本分发

1
2
3
4
5
6
7
8
9
10
use App\Events\OrderShipped;
use App\Models\Order;

$order = Order::find(1);

OrderShipped::dispatch($order, 'TRACK123');

event(new OrderShipped($order, 'TRACK123'));

Order::find(1)->dispatch(new OrderShipped($order));

条件分发

1
2
3
4
5
6
7
8
use App\Events\OrderShipped;

if ($order->isReady()) {
OrderShipped::dispatch($order);
}

OrderShipped::dispatchIf($order->isReady(), $order);
OrderShipped::dispatchUnless($order->isCancelled(), $order);

事件订阅者

创建订阅者

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
<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;

class UserEventSubscriber
{
public function handleUserLogin(Login $event): void
{
\Log::info('User logged in', ['user_id' => $event->user->id]);
}

public function handleUserLogout(Logout $event): void
{
\Log::info('User logged out', ['user_id' => $event->user->id]);
}

public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}

事件监听器队列

配置队列连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Listeners;

use App\Events\PodcastProcessed;
use Illuminate\Contracts\Queue\ShouldQueue;

class ProcessPodcast implements ShouldQueue
{
public $connection = 'redis';

public $queue = 'podcasts';

public $delay = 10;

public function handle(PodcastProcessed $event): void
{
}
}

手动释放作业

1
2
3
4
5
6
7
8
9
public function handle(PodcastProcessed $event): void
{
if ($event->podcast->isLocked()) {
$this->release(30);
return;
}

$event->podcast->process();
}

模型事件

模型事件钩子

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
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
protected static function booted(): void
{
static::creating(function (User $user) {
$user->uuid = (string) \Str::uuid();
});

static::created(function (User $user) {
$user->profile()->create([]);
});

static::updating(function (User $user) {
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
});

static::deleting(function (User $user) {
$user->posts()->delete();
$user->comments()->delete();
});

static::deleted(function (User $user) {
\Storage::delete($user->avatar);
});
}
}

使用观察者

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
37
38
39
40
41
42
43
44
45
<?php

namespace App\Observers;

use App\Models\User;

class UserObserver
{
public function creating(User $user): void
{
$user->uuid = (string) \Str::uuid();
}

public function created(User $user): void
{
$user->profile()->create([]);
}

public function updating(User $user): void
{
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}

public function deleting(User $user): void
{
$user->posts()->delete();
}

public function deleted(User $user): void
{
\Storage::delete($user->avatar);
}

public function restored(User $user): void
{
$user->posts()->withTrashed()->restore();
}

public function forceDeleted(User $user): void
{
$user->posts()->forceDelete();
}
}

注册观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
// AppServiceProvider.php
use App\Models\User;
use App\Observers\UserObserver;

public function boot(): void
{
User::observe(UserObserver::class);
}

// 或在 EventServiceProvider.php
protected $observers = [
User::class => [UserObserver::class],
];

静默模型事件

1
2
3
4
5
6
7
8
9
10
11
12
use App\Models\User;

User::withoutEvents(function () {
User::factory()->count(10)->create();
});

$user = User::withoutEvents(function () {
$user = User::find(1);
$user->name = 'New Name';
$user->save();
return $user;
});

自定义事件

广播事件

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
37
38
39
40
41
42
43
44
45
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ServerCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public function __construct(
public array $server
) {}

public function broadcastOn(): array
{
return [
new PrivateChannel('user.'.$this->server['user_id']),
];
}

public function broadcastAs(): string
{
return 'server.created';
}

public function broadcastWith(): array
{
return [
'id' => $this->server['id'],
'name' => $this->server['name'],
];
}

public function broadcastQueue(): string
{
return 'broadcasts';
}
}

测试事件

断言事件被分发

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
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Events\OrderShipped;
use App\Models\Order;
use Illuminate\Support\Facades\Event;

class OrderTest extends TestCase
{
public function test_order_shipped_event_is_dispatched(): void
{
Event::fake();

$order = Order::factory()->create();

$this->postJson("/api/orders/{$order->id}/ship");

Event::assertDispatched(OrderShipped::class);
Event::assertDispatched(OrderShipped::class, 1);
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
Event::assertNotDispatched(OrderFailed::class);
}

public function test_specific_events(): void
{
Event::fake([OrderShipped::class]);

Event::assertDispatched(OrderShipped::class);
}
}

断言监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function test_listener_handles_event(): void
{
Event::fake();

$order = Order::factory()->create();
$event = new OrderShipped($order, 'TRACK123');

$listener = new SendShipmentNotification(
$this->mock(ShippingService::class)
);

$listener->handle($event);

Event::assertNotDispatched(OrderShipped::class);
}

最佳实践

1. 使用事件解耦

1
2
3
4
5
6
7
8
9
10
11
12
// 好的做法:使用事件解耦
OrderShipped::dispatch($order);

// 监听器可以独立处理
class SendShipmentNotification { }
class UpdateInventory { }
class LogShipment { }

// 不好的做法:直接调用
$order->sendNotification();
$order->updateInventory();
$order->logShipment();

2. 合理使用队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 耗时操作使用队列
class SendShipmentNotification implements ShouldQueue
{
public function handle(OrderShipped $event): void
{
\Mail::to($event->order->user)->send(new ShipmentEmail());
}
}

// 快速操作不使用队列
class UpdateOrderStatus
{
public function handle(OrderShipped $event): void
{
$event->order->update(['status' => 'shipped']);
}
}

3. 使用 SerializesModels

1
2
3
4
5
6
7
8
9
// 好的做法:使用 SerializesModels 处理模型
class OrderShipped
{
use SerializesModels;

public function __construct(public Order $order) {}
}

// 这样在队列中只会存储模型ID,避免序列化整个模型

总结

Laravel 13 的事件系统提供了强大而灵活的事件处理能力。通过合理使用事件和监听器,可以实现应用程序的解耦,提高代码的可维护性和可扩展性。记住使用队列处理耗时操作,利用模型观察者简化模型事件处理,并通过事件广播实现实时功能。