Laravel Model Events

Tổng quan về Eloquent Model Events

Trong quá trình hoạt động của mình, mỗi Eloquent Model có thể tạo ra nhiều sự kiện khác nhau, cho phép chúng ta thao tác với những thời điểm khác nhau trong chu kỳ hoạt động của model đó. Các phương thức tương ứng với các sự kiện đó là: creating, created, updating, updated, saving, saved, deleting, deleted, restoringrestored.

Tên của các phương thức trên cho chúng ta biết sự kiện nào sẽ được thực hiện và được thực hiện tại thời điểm nào. Các sự kiện creatingcreated được thực hiện khi một model được lưu lại trong cơ sở dữ liệu lần đầu tiên, nếu model đã tồn tại và phương thức save được gọi trên model đó hai sự kiện updatingupdated sẽ được thực hiện. Khi một model được xóa bằng phương pháp Soft Delete và được khôi phục lại qua phương thức restore hai event restoringrestored sẽ được thực hiện.

Sau đây chúng ta sẽ xem xét một ví dụ về việc sử dụng Model Events trong Laravel. Giả sử chúng ta có hai model là UserPost và một người dùng có thể có nhiều bài viết khác nhau.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Bây giờ chúng ta muốn xóa toàn bộ những bài viết của một người dùng khi người dùng đó bị xóa khỏi hệ thống. Chúng ta sẽ thực hiện thao tác đó sử dụng sự kiện deleting của model User. Có rất nhiều cách để thực hiện thao tác trên này, cách đơn giản nhất là sử dụng phương thức deleting của model User trong một ServiceProvider chẳng hạn như AppServiceProvider có sẵn cùng với Laravel Framework.

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        User::deleting(function ($user) {
            $user->posts()->delete();
        });
    }
}

Cách thứ hai tương tự với cách đầu tiên nhưng thay vì sử dụng một Service Provider chúng ta sẽ ghi đè phương thức boot của class Illuminate\Database\Eloquent\Model bên trong User model. Và một cách khác là sử dụng Model Observer, trong đó các logic liên quan đến việc xử lý các sự kiện sẽ được lưu vào một class riêng biệt. Cụ thể như sau:

Tạo Event Observer class cho User model
<?php

namespace App\Models\Observers;

use App\Models\User;

class UserObserver
{
    /**
     * Hook into user deleting event.
     *
     * @param User $user
     * @return void
     */
    public function deleting(User $user)
    {
        $user->posts()->delete();
    }

    public function created(User $user)
    {
        //
    }

    public function restoring(User $user)
    {
        //
    }
}

Đăng ký Observer class trong EventServiceProvider
<?php

namespace App\Providers;

use App\Models\User;
use App\Models\Observers\UserObserver;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * Register any other events for your application.
     *
     * @param  \Illuminate\Contracts\Events\Dispatcher $events
     * @return void
     */
    public function boot(DispatcherContract $events)
    {
        User::observe(new UserObserver);
    }
}

Ví dụ về việc sử dụng Model Events để thu thập các hoạt động của người dùng

Bài toán đặt ra ở đây là chúng ta muốn ghi lại những hoạt động của một người dùng nào đó. Chẳng hạn khi người dùng đó tạo mới hoặc xóa một bài viết chúng ta muốn lưu lại thông tin đó để tạo Activity Feed sau này. Trong những mục tiếp theo chúng ta sẽ sử dụng Model Events để giải quyết bài toán này.

Tạo Activity Model

Bước đầu tiên chúng ta cần tạo một model liên quan đến các hoạt động của người dùng. Chúng ta sẽ gọi model đó là Activity. Cấu trúc của model trong ví dụ này khá đơn giản, chúng ta cần biết người dùng nào đã thực hiện hành động, hành động đó liên quan đến model nào khác - target model (giả sử khi người dùng tạo mới một bài viết thì model liên quan sẽ là Post) và tên của hành động đó là gì. Target model sẽ không cố định mà có thể là nhiều model khác nhau vì vậy chúng ta sẽ sử dụng Polymorphic Relations để thuận tiện cho việc tìm target model sau này. Dưới đây là logic cho Activity model.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Activity extends Model
{
    protected $fillable = ['user_id', 'targetable_id', 'targetable_type', 'action'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function targetable()
    {
        return $this->morphTo();
    }
}

CapturesActivity Trait

Trong bước này chúng ta sẽ sử dụng Model Events để ghi lại các hành động của một người dùng nào đó. Trước hết chúng ta sẽ xem xét một cách khác để đăng ký Model Events bằng cách sử dụng trait. Đi sâu một chút vào bên trong class Illuminate\Database\Eloquent\Model chúng ta sẽ thấy một phương thức tĩnh có tên là bootTraits cụ thể như sau:

protected static function bootTraits()
{
    foreach (class_uses_recursive(get_called_class()) as $trait) {
        if (method_exists(get_called_class(), $method = 'boot'.class_basename($trait))) {
            forward_static_call([get_called_class(), $method]);
        }
    }
}

Phương thức này sẽ được gọi mỗi khi model được khởi tạo. Nếu để ý kỹ trong điều kiện if của phương thức bootTraits chúng ta sẽ thấy một đoạn logic khá đặc biệt $method = 'boot'.class_basename($trait). Với mỗi trait được sử dụng bện trong model, khi model được khởi tạo Laravel sẽ kiểm tra sự tồn tại của một phương thức đặc biệt trong trait đó - boot + <trait_name> và gọi phương thức đó nếu có. Nếu chúng ta đăng ký Model Events trong phương thức đặc biệt này thì chúng cũng sẽ hoạt động tương tự như ba cách làm phía trên.

Quay trở lại với bài toán đang xem xét, vì logic liên quan đến việc lưu hành động của người dùng sẽ là giống nhau khi target model thay đổi; chúng ta sẽ tách logic đó ra một trait riêng biệt và sử dụng nó trong các model cần thiết. Trong ví dụ này chúng ta sẽ gọi trait đó là CapturesActivity

<?php

namespace App\Models\Traits;

use App\Models\Activity;

trait CapturesActivity
{
    /**
     * Listen for events on model and capture the
     * appropriate activities.
     *
     * @return void
     */
    protected static function bootCapturesActivity()
    {
        // Register Models Event
        // Create appropriate activities
    }
}

Những sự kiên cần ghi lại

Tiếp theo chúng ta cần quyết định xem những sự kiện nào sẽ được ghi lại, tên hành động là gì, foreign key (nếu không tuân theo mặc định của framework) và các thông tin liên quan đến việc tạo mới hành động.

protected static function getModelEvents()
{
    if (isset(static::$capturedEvents)) {
        return static::$capturedEvents;
    }

    return ['created', 'updated', 'deleted'];
}

Nếu trong model có khai báo một biến static có tên $capturedEvents là một mảng các sự kiện thì đó sẽ là những sự kiện được ghi lại sau này. Trong trường hợp mặc định ba sự kiện là created, updateddeleted sẽ được ghi lại. Ngoài ra chúng ta có thể khai báo $capturedEvents là một mảng rỗng nếu chúng ta không muốn ghi lại các hành động liên quan đến Model Events.

Tên hành động
public function getActivityName($model, $action)
{
    $name = strtolower(class_basename($model));

    return "{$action}_{$name}";
}
Thông tin liên quan đến việc tạo mới activity
protected static function mapAttributes()
{
    return [
        'activityUserId' => 'user_id',
        'activityTargetId' => 'id',
        'activityTargetType' => static::class,
    ];
}

Phương thức này trả về một mảng các thuộc tính (foreign keys và tên của target model) cần thiết để tạo activity và các giá trị mặc định của nó. Trong hầu hết các trường hợp các giá trị mặc định sẽ được sử dụng, tuy nhiên trong một vài trường hợp chúng ta không thể làm như vậy. Giả sử chúng ta muốn ghi lại hành động một người dùng follow/unfollow một người dùng khác; trong trường hợp này các giá trị tương ứng cho activityUserId, activityTargetId, activityTargetType sẽ là follower_id, followed_idUser::class.

Tạo hành động và đăng ký các Model Events
public function captureActivity($event)
{
    $attributes = fetch_static_attributes(static::class, static::mapAttributes());
    list($userId, $targetId, $targetType) = $attributes;
    Activity::create([
        'user_id' => $this->$userId,
        'targetable_id' => $this->$targetId,
        'targetable_type' => $targetType,
        'action' => $this->getActivityName($this, $event),
    ]);
}

Trước hết chúng ta cần kiểm tra xem trong target model có khai báo các biến static $activityUserId, $activityTargetId, $activityTargetType hay không. Trong trường hợp các biến static đó được khai báo giá trị của chúng sẽ được sử dụng trong việc tạo activity, nếu không thì các giá tri mặc đinh trong phương thức mapAttributes sẽ được sử dụng. Sau khi đã có đầy đủ thông tin cần thiết công việc tiếp theo là tạo mới một activity.

Để hoàn tất CapturesActivity trait, công việc cuối cùng là đăng ký các model event cần thiết. Chúng ta sẽ thay thế phần comment trong phương thức bootCapturesActivity phía trên bằng logic sau đây.

protected static function bootCapturesActivity()
{
    foreach (static::getModelEvents() as $event) {
        static::$event(function ($model) use ($event) {
            $model->captureActivity($event);
        });
    }
}

Sử dụng CapturesActivity

Sau đây sẽ là một số ví dụ về việc sử dụng CapturesActivity trait.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Models\Traits\CapturesActivity;

class Post extends Model
{
    use CapturesActivity;

    protected static $capturedEvents = ['created', 'updated'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Models\Traits\CapturesActivity;

class Relationship extends Model
{
    use CapturesActivity;

    protected $table = 'follows';
    protected $fillable = ['follower_id', 'followed_id'];

    protected static $activityUserId = 'follower_id';
    protected static $activityTargetId = 'followed_id';
    protected static $activityTargetType = User::class;
    protected static $capturedEvents = ['created', 'deleted'];

    public function followed()
    {
        return $this->belongsTo(User::class, 'followed_id');
    }

    public function follower()
    {
        return $this->belongsTo(User::class, 'follower_id');
    }
}

Tùy chỉnh tên hành động

Trong nhiều trường hợp, ngoài những hành động liên quan đến các Model Events, chúng ta muốn ghi lại những hành động đặc biệt khác của người dùng. Để thực hiện việc này, chúng ta sẽ tạo một phương thức trợ giúp trong User model cho phép chúng ta tùy biến tên của hành động. Cụ thể như sau:

public function pushActivity($name, $relatedEntity)
{
    if (!method_exists($relatedEntity, 'captureActivity')) {
        throw new BadMethodCallException;
    }

    return $relatedEntity->captureActivity($name);
}

Kết luận

Trong bài viết này chúng ta đã tìm hiểu về Model Events trong Laravel và sử dụng chúng để thực hiện một ví dụ đơn giản về việc ghi lại hành động của người dùng. Events nói chúng và Model Events nói riêng trong Laravel là những công cụ rất hữu ích khi xây dựng các ứng dụng dạng event-driven.

Tham khảo

https://laravel.com/docs/5.2/eloquent#events http://www.archybold.com/blog/post/booting-eloquent-model-traits http://laravel-tricks.com/tricks/using-model-events-to-delete-related-items

Repo