Laravel Events và Listeners năm 2026: Kiến trúc Event-Driven và Câu hỏi Phỏng vấn

Hướng dẫn toàn diện về Laravel events và listeners cho kiến trúc event-driven. Tìm hiểu observer pattern, queued listeners, event subscribers và câu hỏi phỏng vấn kỹ thuật.

Laravel Events và Listeners - Kiến trúc Event-Driven

Laravel events và listeners triển khai observer pattern, tách rời các component bằng cách cho phép các phần của ứng dụng phản ứng với các hành động mà không cần phụ thuộc trực tiếp. Khi người dùng đặt hàng, event OrderShipped được kích hoạt, và các listener riêng biệt xử lý thông báo, phân tích và cập nhật kho hàng một cách độc lập.

Laravel 12 Event Discovery

Laravel 12 tự động phát hiện các listener trong app/Listeners. Bất kỳ class nào có method handle hoặc __invoke với type-hint đến class event sẽ được đăng ký tự động, không cần file cấu hình.

Cơ chế Event Discovery hoạt động trong Laravel 12

Từ Laravel 11, EventServiceProvider đã bị loại bỏ. Laravel quét thư mục Listeners và sử dụng reflection để xác định event nào mà mỗi listener phản hồi dựa trên type hint trong chữ ký method handle.

app/Listeners/SendOrderConfirmation.phpphp
<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendOrderConfirmation
{
    public function handle(OrderShipped $event): void
    {
        // Gửi email xác nhận đến $event->order->user
    }
}

Listener trên được tự động đăng ký cho event OrderShipped. Không cần binding thủ công trong service provider. Điều này giảm boilerplate và giữ mối quan hệ event-listener gần với code triển khai.

Để xác minh các listener đã đăng ký, chạy php artisan event:list. Cho production, cache manifest event với php artisan event:cache để bỏ qua việc quét thư mục trên mỗi request.

Tạo Events và Listeners với Artisan

Laravel cung cấp các lệnh Artisan để tạo scaffold events và listeners:

bash
# Tạo class event
php artisan make:event OrderShipped

# Tạo listener liên kết với event
php artisan make:listener SendShipmentNotification --event=OrderShipped

Class event được tạo sử dụng ba trait: Dispatchable cho method static dispatch(), InteractsWithSockets cho broadcasting, và SerializesModels để serialize các model Eloquent khi listener được queue.

app/Events/OrderShipped.phpphp
<?php

namespace App\Events;

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

class OrderShipped
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

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

Class event là một container dữ liệu. Nó chứa model Order và không có gì khác. Business logic thuộc về listener, không phải event.

Dispatch Events từ Controllers và Services

Dispatch event sử dụng method static dispatch() trên class event:

app/Http/Controllers/OrderController.phpphp
<?php

namespace App\Http\Controllers;

use App\Events\OrderShipped;
use App\Models\Order;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function ship(Request $request, Order $order)
    {
        // Logic vận chuyển ở đây...
        
        OrderShipped::dispatch($order);
        
        return redirect()->route('orders.index');
    }
}

Để dispatch có điều kiện, sử dụng dispatchIf() hoặc dispatchUnless():

php
// Chỉ dispatch nếu đơn hàng có số tracking
OrderShipped::dispatchIf($order->tracking_number !== null, $order);

Sẵn sàng chinh phục phỏng vấn Laravel?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Queued Listeners cho các thao tác chậm

Các listener gửi email, gọi API bên ngoài, hoặc thực hiện tính toán nặng nên chạy bất đồng bộ. Triển khai ShouldQueue để đẩy listener vào queue:

app/Listeners/SendShipmentNotification.phpphp
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    public $queue = 'notifications';
    public $delay = 60; // Chờ 60 giây trước khi xử lý

    public function handle(OrderShipped $event): void
    {
        // Gửi thông báo đến $event->order->user
    }
}

Tùy chỉnh kết nối và tên queue với các thuộc tính $connection$queue, hoặc sử dụng các method runtime:

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

public function viaQueue(): string
{
    return $this->event->order->priority === 'high' 
        ? 'high-priority' 
        : 'default';
}

Conditional Queueing với shouldQueue

Đôi khi listener chỉ cần queue dựa trên dữ liệu event:

php
public function shouldQueue(OrderShipped $event): bool
{
    // Chỉ queue cho đơn hàng trên 2.000.000 VND
    return $event->order->total >= 2000000;
}

Database Transactions và Event Timing

Queued listener được dispatch bên trong transaction có thể xử lý trước khi transaction commit, gây ra lỗi dữ liệu không tìm thấy. Có hai giải pháp:

1. Cấu hình toàn cục trong config/queue.php:

php
'connections' => [
    'redis' => [
        'after_commit' => true,
    ],
],

2. Theo từng listener với ShouldQueueAfterCommit:

php
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;

class UpdateInventory implements ShouldQueueAfterCommit
{
    public function handle(OrderShipped $event): void
    {
        // An toàn để query đơn hàng, transaction đã commit
    }
}

Đối với event, triển khai ShouldDispatchAfterCommit trên class event:

php
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;

class OrderShipped implements ShouldDispatchAfterCommit
{
    // Event chỉ dispatch sau khi transaction commit
}

Unique Listeners để ngăn chặn trùng lặp

Interface ShouldBeUnique ngăn các listener trùng lặp vào queue khi một cái đang xử lý:

php
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;

class ProcessLicenseKey implements ShouldQueue, ShouldBeUnique
{
    public $uniqueFor = 3600; // Lock hết hạn sau 1 giờ

    public function uniqueId(LicensePurchased $event): string
    {
        return 'license:' . $event->license->id;
    }

    public function handle(LicensePurchased $event): void
    {
        // Tạo và gán license key
    }
}

Nếu cùng license ID kích hoạt nhiều event nhanh chóng, chỉ listener đầu tiên xử lý. Các lần thử tiếp theo tìm thấy lock đang được giữ và bỏ qua queueing.

Event Subscribers cho các Events liên quan

Khi nhiều event liên quan chia sẻ logic xử lý chung, event subscriber nhóm các handler trong một class duy nhất:

app/Listeners/UserActivitySubscriber.phpphp
<?php

namespace App\Listeners;

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

class UserActivitySubscriber
{
    public function handleLogin(Login $event): void
    {
        // Ghi log đăng nhập thành công, cập nhật timestamp last_login
    }

    public function handleLogout(Logout $event): void
    {
        // Ghi log đăng xuất, xóa dữ liệu session
    }

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

Subscriber tuân theo quy tắc automatic discovery. Nếu các method handler nằm trong class subscriber với tham số được type-hint đúng cách, Laravel đăng ký chúng tự động.

Lắng nghe Multiple Events với Union Types

Một listener có thể phản hồi nhiều loại event sử dụng PHP 8 union types:

php
public function handle(OrderShipped|OrderCancelled $event): void
{
    match (true) {
        $event instanceof OrderShipped => $this->notifyShipped($event),
        $event instanceof OrderCancelled => $this->notifyCancelled($event),
    };
}

Pattern này hoạt động tốt cho các thông báo tuân theo logic tương tự qua các loại event khác nhau.

Trì hoãn Events với Event::defer

Laravel 12 giới thiệu Event::defer() để trì hoãn dispatch event cho đến khi khối code hoàn thành. Điều này đảm bảo listener có quyền truy cập vào tất cả các record liên quan:

php
use Illuminate\Support\Facades\Event;

Event::defer(function () {
    $user = User::create(['name' => 'John']);
    $user->posts()->create(['title' => 'Welcome']);
});
// Event cho cả việc tạo User và Post dispatch ở đây

Nếu exception xảy ra bên trong closure, các event bị trì hoãn sẽ không bao giờ dispatch.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Câu hỏi Phỏng vấn về Laravel Events và Listeners

Các cuộc phỏng vấn kỹ thuật thường xuyên kiểm tra sự hiểu biết về kiến trúc event-driven. Dưới đây là các câu hỏi phân biệt developer Laravel có kinh nghiệm:

Q: Laravel 12 đăng ký event listener như thế nào mà không có EventServiceProvider?

Laravel quét app/Listeners sử dụng reflection. Nó đọc type hint trên method handle() hoặc __invoke() để xác định event nào mỗi listener phản hồi. Cache manifest trong production với php artisan event:cache.

Q: Khi nào listener nên triển khai ShouldQueue?

Bất cứ khi nào thao tác mất hơn vài mili giây: gửi email, gọi API bên ngoài, tạo báo cáo, hoặc xử lý file. Listener đồng bộ chặn HTTP response.

Q: ShouldQueueAfterCommit giải quyết vấn đề gì?

Queued listener có thể xử lý trước khi database transaction commit. Nếu listener query record vừa tạo, nó sẽ thất bại vì dữ liệu chưa được commit. ShouldQueueAfterCommit trì hoãn dispatch queue cho đến khi transaction hoàn thành.

Q: Làm thế nào để ngăn xử lý trùng lặp cho cùng một event?

Triển khai ShouldBeUnique. Định nghĩa uniqueId() để trả về cache key dựa trên dữ liệu event. Listener lấy atomic lock, và các dispatch trùng lặp bị bỏ qua nếu lock đang được giữ.

Q: Sự khác biệt giữa event listener và event subscriber là gì?

Listener xử lý một loại event. Subscriber là class đăng ký các handler cho nhiều event liên quan ở một nơi, hữu ích cho việc nhóm các event xác thực hoặc audit logging.

Ý nghĩa của Laravel Events cho các ứng dụng có khả năng mở rộng

  • Events tách rời các component, cho phép các tính năng như thông báo và phân tích phát triển độc lập
  • Queued listener chuyển các thao tác chậm sang queue, giữ HTTP response nhanh
  • ShouldQueueAfterCommit ngăn race condition giữa queue worker và database transaction
  • Automatic discovery trong Laravel 12 loại bỏ overhead cấu hình, giữ logic listener gần với triển khai
  • Event subscriber hợp nhất việc xử lý event liên quan, giảm sự phân tán file cho các pattern phổ biến như theo dõi hoạt động người dùng
  • Module /technologies/laravel/interview-questions/events-listeners trên SharpSkill bao gồm các pattern này với câu hỏi tương tác
Thử thách hôm nay

Bạn có tìm ra lỗi trong Laravel không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 20 tháng 8, 2026

Thẻ

#laravel
#events
#listeners
#event-driven
#observer-pattern
#queue

Chia sẻ

Bài viết liên quan