Quản lý sự kiện trong ứng dụng với Laravel Event

Cách đây 3 năm, tôi có một dự án web mà lúc đó mới chỉ biết sơ sơ về PHP, thế rồi với từ khóa “best PHP framework” Google đã trả lời hàng triệu kết quả mà chỉ cần lướt qua trang đầu, tôi đã thấy câu trả lời đó là Laravel, một cái tên thật lạ tai. Trước đó, tôi đã từng được nghe đến Zend framework thông qua một số đồng nghiệp. Tôi tự hỏi, Laravel là gì? tại sao Laravel có thể là framework tốt nhất và tôi tự tìm hiểu thì càng thấy ngạc nhiên hơn khi Laravel mới chỉ bắt đầu được tạo ra vào tháng 7 năm 2011, thật ấn tượng với thứ hạng số một chỉ sau 3 năm hoạt động.

Càng tìm hiểu về Laravel, tôi được tiếp xúc với nhiều các khái niệm, cách thức làm việc mới, đơn giản, dễ sử dụng. Laravel Event là một trong số đó, nó chỉ là một phần nhỏ trong những thứ làm nên một Laravel thú vị.

Laravel Event là gì?

Một sự kiện trong máy tính là hành động hay một cái gì đó xảy ra tại một thời điểm xác định bởi một chương trình máy tính và có thể quản lý bằng các chương trình máy tính. Một vài ví dụ về sự kiện trong Laravel:

  • Một người dùng đăng ký mới.
  • Một bài viết được đăng.
  • Một thành viên viết lời bình luận.
  • Người dùng like một bức ảnh.

Tại sao sử dụng Laravel Event?

Có một số lý do cơ bản làm chúng ta thấy được Laravel Event có ý nghĩa như thế nào:

  • Laravel Event giúp chúng ta tách biệt các thành phần trong ứng dụng, khi một sự kiện xảy ra, sự kiện không cần biết những gì khác ngoài việc thực thi công việc của nó.
  • Không có Laravel Event sẽ phải đặt rất nhiều các code logic ở một chỗ.
  • Một lý do nữa khi sử dụng Laravel Event đó là tăng tốc độ trong xử lý dữ liệu ứng dụng. Ví dụ, một bài viết mới được đăng sẽ gửi mail đến tất cả các thành viên, việc gửi mail đến hàng triệu thành viên sẽ mất khá nhiều thời gian, với Laravel event gửi email sẽ là một trigger khi một bài viết mới được tạo ra. Với cách viết code cũ trước đây, tất cả xử lý đưa vào tính năng đăng bài mới khiến việc đăng một bài mất rất nhiều thời gian. Một ví dụ khác, khi thanh toán đơn hàng, phải gửi email thông báo đến khách hàng, việc thanh toán đơn hàng là ưu tiên nhất và cần thời gian xử lý cực nhanh đảm bảo trải nghiệm tốt cho khách hàng, nếu email khách hàng nhận hiện đang có vấn đề, xử lý gửi mail sẽ vào một vòng lặp thử liên tục gửi, như vậy việc thanh toán sẽ rất lâu. Với Laravel Event mọi việc được tách biệt rất rõ và tăng tốc trong xử lý dữ liệu ứng dụng.

Bắt đầu với Laravel Event

Chúng ta đã dần hiểu Laravel Event là gì và lợi ích của nó, tiếp theo chúng ta sẽ đi vào tìm hiểu xem Laravel Event hoạt động như thế nào?

Laravel Event cho phép bạn đăng ký sự kiện và tạo các listener cho rất nhiều các sự kiện xảy ra trong ứng dụng. Các class Event được lưu trữ trong app/Events và các listener được lưu trong app/Listeners. Nếu bạn không thấy hai thư mục này trong project cũng đừng lo vì các câu lệnh artisan tạo các event, listerner sẽ tự động tạo ra chúng. Một event có thể có rất nhiều listerner được tạo ra và các listener này độc lập với nhau. Chúng ta sẽ xem các bước tạo, đăng ký và sử dụng Event, Listerner trong Laravel.

Bước 1: Định nghĩa Event

Event được tạo ra đơn giản bằng cách sử dụng câu lệnh artisan

php artisan make:event OrderPayment

nó sẽ tạo ra một class Event nằm trong app\Events như sau:

<?php

namespace App\Events;

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

class OrderPayment
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

Bước 2: Tạo Event Listener

Ngay khi một event xảy ra, ứng dụng cần biết để thực hiện các việc khác, đó chính là lý do cần Event Listerner. Để tạo Listerner đơn giản là sử dung lệnh artisan:

c:\xampp\htdocs\laravel-test>php artisan make:listener SendEmailAfterOrderPayment --event="OrderPayment"
Listener created successfully.

Khi đó, sẽ tạo ra class SendEmailAfterOrderPayment.php trong app\Listerners với nội dung:

<?php

namespace App\Listeners;

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

class SendEmailAfterOrderPayment
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  OrderPayment  $event
     * @return void
     */
    public function handle(OrderPayment $event)
    {
        //
    }
}

Phương thức handle() với tham số đầu vào là một instance của Event mà Listener được gán vào, phương thức này sẽ được dùng để thực hiện các công việc cần thiết để đáp lại Event. Instance đầu vào của phương thức cũng chứa cả các giá trị mà Event truyền sang. Ví dụ:

    /**
     * Handle the event.
     *
     * @param  OrderPayment  $event
     * @return void
     */
    public function handle(OrderPayment $event)
    {
        //
        $order_amout = $event->order->amount;
       ...
    }

Đôi khi bạn muốn dừng quảng bá Event này đến các Listener khác, rất đơn giản bạn trả về giá trị false trong phương thức handle() của Listerner đang xử lý.

Bước 3: Đăng ký Event

Trước khi đến các bước xử lý trong Event, chúng ta cần đăng ký Event, mở file app/Providers/EventServiceProvider.php và tìm đến thuộc tính $listener.

protected $listen = [
    'App\Events\OrderPayment' => [
        'App\Listeners\SendEmailAfterOrderPayment',
    ],
];

Chúng ta đã khai báo Listerner này của Event nào, một Event có thể có nhiều các Listener khác nhau:

protected $listen = [
    'App\Events\OrderPayment' => [
        'App\Listeners\SendEmailAfterOrderPayment',
        'App\Listeners\SendSMSAfterOrderPayment',
    ],
];

Ba bước trên có thể được thực hiện đơn giản hơn bằng cách gán Listener cho Event trong file app/Providers/EventServiceProvider.php sau đó chạy lệnh:

c:\xampp\htdocs\laravel-test>php artisan event:generate
Events and listeners generated successfully!

Khi đó, các class trong app\Events và app\Listeners sẽ được tự động sinh ra.

Bước 4: Tạo ra thông báo sự kiện xảy ra

Khi thực hiện một đoạn code nào đó, muốn thông báo đến hệ thống là sự kiện xảy ra để các listener có thể biết được chúng ta sử dụng phương thức fire().

use Event;
use App\Events\OrderPayment;

// Các xử lý thanh toán đơn hàng ở đây
$order = new Order;
$order->amount = 2000000;
$order->note = 'Tạo đơn hàng mẫu';
...
$order->save();
// Phần xử lý thanh toán xong, tạo ra thông báo sự kiện xảy ra cho các Listener biết
event(new OrderPayment($order));

Hoặc cũng có thể dùng helper method event để tạo ra thông báo

use App\Events\OrderPayment;

// Các xử lý thanh toán đơn hàng ở đây
...
// Phần xử lý thanh toán xong, tạo ra thông báo sự kiện xảy ra cho các Listener biết
event(new OrderPayment($order));

Đối tượng $order sẽ được truyền sang Listener.

Bước 5 (Tùy chọn): Đưa Event Listener vào hàng đợi

Đôi khi bạn không muốn các event và listener làm ảnh hưởng đến hoạt động của ứng dụng, ví dụ khi người dùng thanh toán đơn hàng không phải chờ đợi thời gian email về thanh toán gửi tới địa chỉ mail người dùng, bạn có thể đưa event listener vào hàng đợi và nó sẽ được quản lý bởi queue driver do bạn xác định. Để làm điều này rất đơn giản là implement Illuminate\Contracts\Queue\ShouldQueue.

<?php

namespace App\Listeners;

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

class SendEmailAfterOrderPayment implements ShouldQueue
{
    //
}

Khi đó, khi Listener được gọi bởi một Event, nó sẽ tự động được đưa vào hàng đợi bởi hệ thống hàng đợi của Laravel. Nếu không có exception nào xảy ra, queue job sẽ tự động được xóa sau khi nó hoàn thành xử lý.

Laravel còn cho phép truy xuất các Listener trong queue job một cách thủ công bằng các phương thức như delete(), handle(), chú ý phải sử dụng trail Illuminate\Queue\InteractsWithQueue. Đôi khi các job trong hàng đợi cũng có thể gặp lỗi, để xử lý chúng dùng phương thức failed().

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(OrderShipped $event)
    {
        if (true) {
            $this->release(30);
        }
    }

    public function failed(OrderShipped $event, $exception)
    {
        //
    }
}

Ví dụ sử dụng Laravel Event

Mớ kiến thức ở trên có thể bạn chưa thể nắm bắt hết được, một ví dụ cụ thể sẽ giúp bạn hiểu cặn kẽ hơn, chúng ta bắt đầu thực hành ví dụ sau đây: Khi tạo mới một sản phẩm sẽ gửi email đến quản trị viên liên quan. Chúng ta giả sử quá trình gửi email mất 10 phút bằng cách đưa vào hàm sleep(600), và quá trình gửi email được xử lý bằng Laravel Event. Cuối ví dụ chúng ta cũng xem xét giữa lập trình tuần tự thông thường và sử dụng Laravel Event khác nhau như thế nào.

Bắt đầu thực hành bạn nhé!

Chúng ta quay lại với ví dụ về quản lý sản phẩm trong bài Laravel Query Builder và thực hiện theo các bước ở trên.

Bước 1: Tạo Event

c:\xampp\htdocs\laravel-test>php artisan make:event NewProduct
Event created successfully.

Câu lệnh này sẽ tạo ra class NewProduct trong app\Events\NewProduct.php

Bước 2: Tạo Listener

c:\xampp\htdocs\laravel-test>php artisan make:listener SendEmailAfterNewProduct --event="NewProduct"
Listener created successfully.

class SendEmailAfterNewProduct sẽ được tạo ra trong app\Listener.

Bước 3: Đăng ký Event và gán với Listener. Mở file EventServiceProvider.php trong app\Providers thêm vào thuộc tính $listener

    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'App\Events\NewProduct' => [
            'App\Listeners\SendEmailAfterNewProduct',
        ],
    ];

Bước 4: Thêm code bắn ra thông báo khi sự kiện NewProduct xảy ra và xử lý ở Listener.

Thêm code tạo thông báo sự kiện xảy ra trong phương thức store() ở ProductController.php (app\Controllers).

    use Event;
    use App\Events\NewProduct;
    ...

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name'       => 'required|max:255',
            'price'      => 'required|number',
            'content'    => 'required',
            'image_path' => 'required'
        ]);

        if ($validator->fails()) {
            return redirect('product/create')
                    ->withErrors($validator)
                    ->withInput();
        } else {
            // Lưu thông tin vào database, phần này sẽ giới thiệu ở bài về database
            $active = $request->has('active')? 1 : 0;
            $product_id = DB::table('product')->insertGetId(
                'name'       => $request->input('name'),
                'price'      => $request->input('price'),
                'content'    => $request->input('content'),
                'image_path' => $request->input('image_path'),
                'active'     => $active
                );
            // fire a new product event 
            $product = Product::find($product_id);
            event(new NewProduct($product));
            return redirect('product/create')
                    ->with('message', 'Sản phẩm được tạo thành công với ID: ' . $product_id);
        }
    }

Thêm code vào phương thức handle() của Listener SendEmailAfterNewProduct trong app\Listeners. Do bài viết về sử dụng Email chưa được thực hiện nên chúng ta tạm thời coi như việc ghi vào file text nội dung tạo sản phẩm mới như là việc gửi email.

    /**
     * Handle the event.
     *
     * @param  NewProduct  $event
     * @return void
     */
    public function handle(NewProduct $event)
    {
        // Tam dung 10 phut
        sleep(600);
 
        $fileName = $event->product->id . '.txt';
        $data = 'Sản phẩm mới tạo: ' . $event->product->name . ' với ID: ' . $event->product->id; 
        File::put(public_path('/txt/' . $fileName), $data);
        return true;
    }

OK, giờ chúng ta kiểm tra xem các đoạn code hoạt động như thế nào. Đầu tiên, chúng ta vào đường dẫn http://laravel.dev/admin/product/create, nhớ đăng nhập bằng user bạn đã đăng ký trong bài Laravel Authentication. Nhập vào các thông tin về sản phẩm, sau đó click vào Tạo sản phẩm.

Phần nhập sản phẩm trong ví dụ sử dụng Laravel Event

chúng ta sẽ thấy trình duyệt vẫn ở trạng thái waiting, sau 10 phút mới xong và xem trong thư mục public/txt có file text xuất hiện với nội dụng như trong handle().

Ví dụ sử dụng Laravel Event

Như ví dụ này chúng ta thấy rằng việc gửi email sau khi tạo sản phẩm mới đã được tách biệt giữa Event và Listener. Tuy nhiên, chúng ta thấy việc tạo sản phẩm mới vẫn mất quá nhiều thời gian do phần quản lý Listener mất hơn 10 phút. Để tạo sản phẩm mới nhanh, tách biệt hẳn với phần gửi email, chúng ta sẽ thực hiện đưa Listener vào hàng đợi như trong Bước 5 ở phần đầu bài viết.

 

Lời kết

Tôi đã rất ngạc nhiên và thích thú vô cùng khi lần đầu tiên được làm việc với tính năng Laravel Event, nó giúp chúng ta thực hiện các công việc mang tính sự kiện với tốc độ xử lý ở phía người dùng nhanh hơn, tạo trải nghiệm tốt hơn cho người dùng. Bạn có đồng ý như vậy không, hãy cho chúng tôi biết ý kiến của bạn bằng những comment ở cuối bài nhé!

13 thoughts on “Quản lý sự kiện trong ứng dụng với Laravel Event

    1. Ok bạn, mấy hôm nay đang bận chút nên chưa có bài mới, sắp tới sẽ là series viết game bằng Vue.js nhé

  1. bài viết này rất hữu ích, mong bạn có những bài viết có ví dụ cụ thể các công cụ của Laravel như thế này. Mình đọc gần hết các bài laravel 7 ngày của bạn viết, bài này mình khá tâm đắc vì có ví dụ thực tế cụ thể dễ hiểu.

    1. Các bài viết mình đều cố gắng lấy các ví dụ đã thực hiện trong các dự án thực tế để minh họa, nếu có thời gian mình sẽ trau chuốt hơn. Thanks namdao.

  2. Mình xác nhận lại 1 vấn đề đó là cho dù bạn có implements ShouldQueue trong Event thì bạn vẫn cứ phải chờ 10p nhé 😀

    1. Mình cũng thấy implements ShouldQueue chẳng thay đổi tí tốc độ nào @@. Có cách nào giúp xử lí nhanh hơn ko nhỉ.

  3. Cho mình hỏi sự khác nhau của Events và Queue. Vì mình thấy bài này và bài Laravel Queue bạn viết nói 2 thằng này khá giống nhau về việc xử lý công việc tách rời khỏi trình tuần tự.

  4. cho mình hỏi chút function handle(NewThongBao $event)
    mình ko lấy được event mặc dù bên kia minh đã truyền products cho nó rồi

  5. bạn phải khởi tạo trong Event NewProduct biến $product
    public $product;
    public function __construct($product)
    {
    $this->product = $product;
    }

Add Comment