در حال بارگزاری ...

ثبت فعالیت های کاربران در پایگاه داده لاراول

توسط مریم مهربان
آخرین به روز رسانی سه شنبه 29 مهر 1399

ثبت فعالیتهای کاربران و ردیابی آن‌ها یکی از ویژگی‌های جذاب برای هر وب سایتی محسوب می‌شود. ثبت فعالیت کاربران را می‌توان برای تجزیه و تحلیل علاقه‌مندی‌های کاربران، دسته بندی کاربران براساس میزان فعالیت آن‌ها، نظارت بر اقداماتی که در وب سایت انجام می‌شود و غیره به کار گرفت.

ابتدا یک کلاس به نام ActivityTest با محتوای زیر ایجاد می‌کنیم و آن را در فولدر Unit test  قرار می‌دهیم.

نمونه تابع تست

هر زمان که کاربری یک نخ جدید ایجاد می‌کند، تابع تست آن را تایید می‌کند، سپس یک فعالیت جدید ایجاد می‌کند و به پایگاه داده اضافه می‌کند. به همین منظور، نام این تابع را به test_it_records_activity_when_a_thread_is_created() تغییر دادیم و کد بدنه را به صورت زیر وارد کردیم:

<?php
 
namespace Tests\Feature;
 
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
 
class ActivityTest extends TestCase
{
    use DatabaseMigrations;
 
    public function test_it_records_activity_when_a_thread_is_created()
    {
        $this->signIn();
 
        $thread = create('App\Thread');
 
        $this->assertDatabaseHas('activities', [
            'type' => 'created_thread',
            'user_id' => auth()->id(),
            'subject_id' => $thread->id,
            'subject_type' => 'App\Thread'
        ]);
    }
}

اگر برنامه بالا را اجرا کنیم، با خطای زیر مواجه می‌شویم. پس باید جدولی که فعالیت‌های کاربر در آن نگهداری می‌شود، ایجاد کنیم.  

[email protected]:~/Code/forumio$ phpunit --filter test_it_records_activity_when_a_thread_is_createdPHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 796 ms, Memory: 8.00MB

There was 1 error:

1) Tests\Feature\ActivityTest::test_it_records_activity_when_a_thread_is_created
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: activities (SQL: select count(*) as aggregate from "activities" where ("type" = created_thread and "user_id" = 1 and "subject_id" = 1 and "subject_type" = App\Thread))

ایجاد مدل Activity و Migration

با کمک artisan، یک مدل و یک مایگرشن ایجاد می‌کنیم :

[email protected]:~/Code/forumio$ php artisan make:model Activity -m
Model created successfully.
Created Migration: 2018_01_30_164752_create_activities_table

با اجرای کد بالا، خطای موجود نبودن جدول رفع می‌شود. اما اگر تابع test_it_records_activity_when_a_thread_is_created() را اجرا کنیم، همچنان با خطا مواجه می‌شویم. طبق این خطا، جدول موردنظر وجود دارد، اما داده‌های اضافه شده در طول ایجاد هر نخ جدید را نمی‌پذیرد.

[email protected]:~/Code/forumio$ phpunit --filter test_it_records_activity_when_a_thread_is_createdPHPUnit 6.5.5 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 2.44 seconds, Memory: 8.00MB

There was 1 failure:

1) Tests\Feature\ActivityTest::test_it_records_activity_when_a_thread_is_created
Failed asserting that a row in the table [activities] matches the attributes {
    "type": "created_thread",
    "user_id": 1,
    "subject_id": 1,
    "subject_type": "App\\Thread"
}.

The table is empty.

/home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php:22
/home/vagrant/Code/forumio/tests/Unit/ActivityTest.php:24

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

استفاده از رخدادهای مدل در Thread.php

در اینجا، بهترین راه حل استفاده از رخدادهای مدل است. ما listernerهایی را ایجاد می‌کنیم که به زمان اجرای مدل نخ گوش بدهند و هر زمان که این مدل فراخوانی شد، اقداماتی را انجام دهند. این رخداد را به تابع boot اضافه می‌کنیم. طبق این کد، هر زمان که نخی در پایگاه داده ایجاد می‎شود، بلافاصله یک فعالیت هم به پایگاه داده اضافه می‌شود.

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\ActivityTest;
 
class Thread extends Model
{
    protected $guarded = [];
 
    protected $with = ['creator', 'channel'];
 
    protected static function boot()
    {
        parent::boot();
 
        static::addGlobalScope('replyCount', function ($builder) {
            $builder->withCount('replies');
        });
 
        static::deleting(function ($thread) {
            $thread->replies()->delete();
        });
 
        static::created(function ($thread) {
            Activity::create([
                'type' => 'created_thread',
                'user_id' => auth()->id(),
                'subject_id' => $thread->id,
                'subject_type' => 'App\Thread'
            ]);
        });
    }

مجددا تابع test_it_records_activity_when_a_thread_is_created() را اجرا می‌کنیم و همچنان با خطا مواجه می‌شویم.

[email protected]:~/Code/forumio$ phpunit --filter test_it_records_activity_when_a_thread_is_createdPHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 1.13 seconds, Memory: 8.00MB

There was 1 error:

1) Tests\Feature\ActivityTest::test_it_records_activity_when_a_thread_is_created
Illuminate\Database\Eloquent\MassAssignmentException: type

برای رفع خطای زیر، لازم است در مدل Activity، قابلیت mass assignment را غیرفعال کنیم :

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Activity extends Model
{
    protected $guarded = [];
}

تابع test_it_records_activity_when_a_thread_is_created() را دوباره اجرا می‌کنیم و با خطای زیر مواجه می‌شویم. برای رفع این خطا باید مایگرشن activity را به صورت زیر به روزرسانی کنیم.

[email protected]:~/Code/forumio$ phpunit --filter test_it_records_activity_when_a_thread_is_createdPHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 798 ms, Memory: 8.00MB

There was 1 error:

1) Tests\Feature\ActivityTest::test_it_records_activity_when_a_thread_is_created
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 table activities has no column named type (SQL: insert into "activities" ("type", "user_id", "subject_id", "subject_type", "updated_at", "created_at") values (created_thread, 1, 1, App\Thread, 2018-01-30 17:00:35, 2018-01-30 17:00:35))

به روزرسانی مایگرشن Activity

در کد زیر، فیلدهای user_id، subject_id، subject_type و type را به پایگاه داده اضافه می‌کنیم:

<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateActivitiesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('activities', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id')->index();
            $table->unsignedInteger('subject_id')->index();
            $table->string('subject_type', 50);
            $table->string('type');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('activities');
    }
}

با این به روزرسانی، تابع test_it_records_activity_when_a_thread_is_created() باید با موفقیت اجرا شود:

تست برنامه ثبت فعالیتهای کاربر

گسترش قابلیت ثبت اقدامات کاربر در پایگاه داده

تاکنون هر نخ جدیدی در پایگاه داده ثبت می‌شود. می‌توانیم تابع test_it_records_activity_when_a_thread_is_created() خود را به گونه‌ای گسترش دهیم که پاسخ‌ها را هم به عنوان یک فعالیت جدید در پایگاه داده ثبت کند. طبق کدی که در تابع boot مدل Thread اضافه کردیم، subject_type به فولدر “App\Thread” اضافه می‌شود. اکنون این مسیر را به get_class($thread) و created_thread را نیز به created_strtolower((new \ReflectionClass($thread)) تغییر می‌دهیم.

static::created(function ($thread) {
    Activity::create([
        'type' => 'created_' . strtolower((new \ReflectionClass($thread))->getShortName()),
        'user_id' => auth()->id(),
        'subject_id' => $thread->id,
        'subject_type' => get_class($thread)
    ]);
});

اگر تابع تست خود را اجرا کنیم، مشاهده می‌کنیم که این قابلیت نیز به خوبی کار می‌کند:

[email protected]:~/Code/forumio$ phpunit --filter test_it_records_activity_when_a_thread_is_createdPHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 1.14 seconds, Memory: 8.00MB

OK (1 test, 1 assertion)

استخراج یک متد برای شی

در حال حاضر، ما می‌توانیم برای شفاف تر شدن کد، منطق رخداد مدل را در یک تابع جداگانه بنویسیم. به این منظور، یک تابع محافظت شده به نام recordActivity() به کلاس خود اضافه می‌کنیم:

protected function recordActivity($event)
{
    Activity::create([
        'type' => 'created_' . strtolower((new \ReflectionClass($this))->getShortName()),
        'user_id' => auth()->id(),
        'subject_id' => $this->id,
        'subject_type' => get_class($this)
    ]);
}

 و تکه کد اضافه شده به تابع boot در مدل thread ، قابل کاهش به کد زیر است:

static::created(function ($thread) {
    $thread->recordActivity('created');
});

پس از این تغییرات، ما دو تابع محافظت شده و یک رخداد مدل مربوط به فعالیت ثبت فعالیت‌ها خواهیم داشت :

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\ActivityTest;
 
class Thread extends Model
{
    protected $guarded = [];
 
    protected $with = ['creator', 'channel'];
 
    protected static function boot()
    {
        parent::boot();
 
        static::addGlobalScope('replyCount', function ($builder) {
            $builder->withCount('replies');
        });
 
        static::deleting(function ($thread) {
            $thread->replies()->delete();
        });
 
        static::created(function ($thread) {
            $thread->recordActivity('created');
        });
    }
 
    protected function recordActivity($event)
    {
        Activity::create([
            'type' => $this->getActivityType($event),
            'user_id' => auth()->id(),
            'subject_id' => $this->id,
            'subject_type' => get_class($this)
        ]);
    }
 
    protected function getActivityType($event)
    {
        return 'created_' . strtolower((new \ReflectionClass($this))->getShortName());
    }
 
    public function path()
    {
        return '/threads/' . $this->channel->slug . '/' . $this->id;
    }
 
    public function replies()
    {
        return $this->hasMany(Reply::class);
    }
 
    public function creator()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
 
    public function channel()
    {
        return $this->belongsTo(Channel::class);
    }
 
    public function addReply($reply)
    {
        $this->replies()->create($reply);
    }
 
    public function scopeFilter($query, $filters)
    {
        return $filters->apply($query);
    }
}

 تبدیل به یک قابلیت جدید

 شما می‌توانید از این برنامه در پروژه‌های دیگرتان استفاده کنید. برای این منظور، عبارت use را به کار ببرید.

گام اول : فایل تست جدید را ایجاد کنید.

ایجاد فایل تست جدید

<?php
 
namespace App;
 
 
trait RecordsActivity
{
 
}

گام دوم : عبارت use را به مدل Thread اضافه کنید.

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\ActivityTest;
 
class Thread extends Model
{
    use RecordsActivity;
    
    protected $guarded = [];

گام سوم : متدهای موردنظر را انتخاب کنید.

توابع فایل تست

<?php
 
namespace App;
 
 
trait RecordsActivity
{
    protected function recordActivity($event)
    {
        Activity::create([
            'type' => $this->getActivityType($event),
            'user_id' => auth()->id(),
            'subject_id' => $this->id,
            'subject_type' => get_class($this)
        ]);
    }
 
    protected function getActivityType($event)
    {
        return 'created_' . strtolower((new \ReflectionClass($this))->getShortName());
    }
}

به روزرسانی رخداد مدل

برای تکمیل کدهای خود می‌توانیم از همریختی روابط زیاد استفاده کنیم. برای این منظور، تابع morphMany()   را به کار می‌بریم. با به کار بردن ‘subject’ ، لاراول به صورت پویا subject_id و subject_type صحیح را برای هر فعالیت بررسی می‌کند.

protected function recordActivity($event)
{
    $this->activity()->create([
        'type' => $this->getActivityType($event),
        'user_id' => auth()->id(),
    ]);
}
 
public function activity()
{
    return $this->morphMany('App\Activity', 'subject');
}

توضیح مورد کاربری دیگر

تست زیر، نشان می‌دهد که رابطه subject باید معتبر باشد. به عبارت دیگر، اگر یک مدل Activity وجود داشته باشد و شما به subject این فعالیت درخواست دهید، این مدل باید مربوط به مدل thread باشد.

<?php
 
namespace Tests\Feature;
 
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
 
class ActivityTest extends TestCase
{
    use DatabaseMigrations;
 
    public function test_it_records_activity_when_a_thread_is_created()
    {
        $this->signIn();
 
        $thread = create('App\Thread');
 
        $this->assertDatabaseHas('activities', [
            'type' => 'created_thread',
            'user_id' => auth()->id(),
            'subject_id' => $thread->id,
            'subject_type' => 'App\Thread'
        ]);
 
        $activity = Activity::first();
        $this->assertEquals($activity->subject->id, $thread->id);
    }
}

 اضافه کردن تابع morphTo

برای اینکه برنامه بالا به درستی اجرا شود، باید تابع subject() را تعریف کنید:

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Activity extends Model
{
    protected $guarded = [];
 
    public function subject()
    {
        return $this->morphTo();
    }
}

ثبت فعالیتهای مربوط به پاسخ کاربران

برای ثبت فعالیتهای پاسخگویی کاربران، تابع زیر را ایجاد می‌کنیم.

public function test_it_records_activity_when_a_reply_is_created()
{
    $this->signIn();
 
    $reply = create('App\Reply');
 
    $this->assertEquals(2, Activity::count());
}

 اما اگر این برنامه را اجرا کنیم با خطای زیر مواجه می‌شویم. برای رفع آنها، از روابط همریختی استفاده می‌کنیم.

[email protected]:~/Code/forumio$ phpunit --filter test_it_records_activity_when_a_reply_is_created
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 801 ms, Memory: 8.00MB

There was 1 failure:

1) Tests\Feature\ActivityTest::test_it_records_activity_when_a_reply_is_created
Failed asserting that 1 matches expected 2.

/home/vagrant/Code/forumio/tests/Unit/ActivityTest.php:38

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

 به این منظور، کد زیر را ایجاد کنید.

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Reply extends Model
{
    use Favoriteable, RecordsActivity;
 
    protected $guarded = [];
 
    protected $with = ['owner', 'favorites'];
 
    public function owner()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

 اضافه کردن تابع getActivitiesToRecord

 به منظور ایجاد قابلیت ردیابی برخی رخدادها، تابع جدید زیر را ایجاد می‌کنیم. پس از ایجاد این تابع، باید تابع bootRecordsActivity()   را به روزرسانی کنیم.

<?php
 
namespace App;
 
 
trait RecordsActivity
{
    protected static function bootRecordsActivity()
    {
        foreach (static::getActivitiesToRecord() as $event) {
            static::$event(function ($model) use ($event) {
                $model->recordActivity($event);
            });
        }
    }
 
    protected static function getActivitiesToRecord()
    {
        return ['created'];
    }
 
    function recordActivity($event)
    {
        $this->activity()->create([
            'type' => $this->getActivityType($event),
            'user_id' => auth()->id(),
        ]);
    }
 
    public function activity()
    {
        return $this->morphMany('App\Activity', 'subject');
    }
 
    protected function getActivityType($event)
    {
        $type = strtolower((new \ReflectionClass($this))->getShortName());
        return "{$event}_{$type}";
    }
}

 اگر همه برنامه ها را اجرا کنیم، با خطای زیر مواجه می‌شویم. این خطا بیان کننده آن است که ما فعالیت‌ها را صرف نظر از اینکه کاربر لاگین کرده است یا خیر، ثبت می‌کنیم. بنابراین باید این بررسی را انجام دهیم.

[email protected]:~/Code/forumio$ phpunit
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.....E........EEEEEEE..E.EEEEEE                                   31 / 31 (100%)

Time: 3.69 seconds, Memory: 14.00MB

There were 15 errors:

 فایل تست را به صورت زیر به روزرسانی می‌کنیم.

<?php
 
namespace App;
 
 
trait RecordsActivity
{
    protected static function bootRecordsActivity()
    {
        if(auth()->guest()) return;
        
        foreach (static::getActivitiesToRecord() as $event) {
            static::$event(function ($model) use ($event) {
                $model->recordActivity($event);
            });
        }
    }

 حذف فعالیتهای مربوطه

اکنون می‌توانیم روی این قابلیت کار کنیم که آیا کاربر اجازه حذف آیتمی را دارد یا خیر. به این منظور، تابع test_authorized_users_can_delete_threads را در کلاس CreateThreadsTest  ایجاد می‌کنیم.

public function test_authorized_users_can_delete_threads()
{
    $this->withoutExceptionHandling()->signIn();
 
    $thread = create('App\Thread', ['user_id' => auth()->id()]);
    $reply = create('App\Reply', ['thread_id' => $thread->id]);
 
    $response = $this->json('DELETE', $thread->path());
 
    $response->assertStatus(204);
 
    $this->assertDatabaseMissing('threads', ['id' => $thread->id]);
    $this->assertDatabaseMissing('replies', ['id' => $reply->id]);
    $this->assertDatabaseMissing('activities', [
        'subject_id' => $thread->id,
        'subject_type' => get_class($thread)
    ]);
}

اجرای برنامه بالا با خطای زیر مواجه می‌شود:

[email protected]:~/Code/forumio$ phpunit --filter test_authorized_users_can_delete_threads
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 887 ms, Memory: 10.00MB

There was 1 failure:

1) Tests\Feature\CreateThreadsTest::test_authorized_users_can_delete_threads
Failed asserting that a row in the table [activities] does not match the attributes {
    "subject_id": 1,
    "subject_type": "App\\Thread"
}.

Found: [
    {
        "id": "1",
        "user_id": "1",
        "subject_id": "1",
        "subject_type": "App\\Thread",
        "type": "created_thread",
        "created_at": "2018-01-30 21:15:04",
        "updated_at": "2018-01-30 21:15:04"
    },
    {
        "id": "2",
        "user_id": "1",
        "subject_id": "1",
        "subject_type": "App\\Reply",
        "type": "created_reply",
        "created_at": "2018-01-30 21:15:04",
        "updated_at": "2018-01-30 21:15:04"
    }
].

/home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php:42
/home/vagrant/Code/forumio/tests/Feature/CreateThreadsTest.php:80

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

 برای رفع خطای بالا، یک event listener جدید ایجاد می‌کنیم :

<?php
 
namespace App;
 
 
trait RecordsActivity
{
    protected static function bootRecordsActivity()
    {
        if (auth()->guest()) return;
 
        foreach (static::getActivitiesToRecord() as $event) {
            static::$event(function ($model) use ($event) {
                $model->recordActivity($event);
            });
        }
 
        static::deleting(function ($model) {
            $model->activity()->delete();
        });
    }

 به روزرسانی event listener مربوط به حذف آیتمها در مدل Thread

 کد بالا این اطمینان را ایجاد می‌کند که فعالیت مربوط به ایجاد نخ حذف شده است. برای اطمینان از حذف نخ مربوط به پاسخ را می‌توانیم از کد زیر استفاده کنیم.

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\ActivityTest;
 
class Thread extends Model
{
    use RecordsActivity;
 
    protected $guarded = [];
 
    protected $with = ['creator', 'channel'];
 
    protected static function boot()
    {
        parent::boot();
 
        static::addGlobalScope('replyCount', function ($builder) {
            $builder->withCount('replies');
        });
 
        static::deleting(function ($thread) {
            $thread->replies->each->delete();
        });
    }

 و در نهایت، همه برنامه‌ها را با موفقیت اجرا می‌کنیم.

اجرای موفقیت آمیز تست

 امیدوارم بتوانید از این آموزش در پروژه‌های خود استفاده کنید. دیگر آموزش‌های ما را در کتابخانه تخصصی لیداوب دنبال کنید.

دیدگاه ها

دیدگاه ها : 0


متاسفانه فقط اعضای سایت قادر به ثبت دیدگاه هستند

رایگان

اشتراک گذاری در
سورس خرید و فروش ارزهای دیجیتال
ثبت امتیاز
5 (1 رای)

   لطفا صبر کنید ...