ثبت فعالیت های کاربران در پایگاه داده لاراول
ثبت فعالیتهای کاربران و ردیابی آنها یکی از ویژگیهای جذاب برای هر وب سایتی محسوب میشود. ثبت فعالیت کاربران را میتوان برای تجزیه و تحلیل علاقهمندیهای کاربران، دسته بندی کاربران براساس میزان فعالیت آنها، نظارت بر اقداماتی که در وب سایت انجام میشود و غیره به کار گرفت.
ابتدا یک کلاس به نام 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();
});
}
و در نهایت، همه برنامهها را با موفقیت اجرا میکنیم.
امیدوارم بتوانید از این آموزش در پروژههای خود استفاده کنید. دیگر آموزشهای ما را در کتابخانه تخصصی لیداوب دنبال کنید.
دیدگاه ها
متاسفانه فقط اعضای سایت قادر به ثبت دیدگاه هستند
ورود به سایت