
Історія дій користувачів
15.06.2019 09:39 | Laravel
Реалізуємо на Laravel 5 перегляд історії дій користувачів. Наприклад, заходимо на сторінку профілю конкретного користувача і бачимо, коли він додав статтю, відповів на коментар, лайкнув якийсь контент і т.д. (За матеріалами laracasts)
Потрібно розуміти, що запис кожної дії повинен бути прив'язаний до конкретного користувача (один до багатьох), і в той же час ми не повинні залежати від типу контенту, з яким працюємо - тут використаємо поліморфні відносини один до багатьох. Створимо модель і міграцію:
php artisan make:model Activity -m
Відредагуємо створену міграцію (файл database/migrations/***_create_activities_table.php
):
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActivitiesTable extends Migration
{
public function up()
{
Schema::create('activities', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id')->index();
$table->unsignedBigInteger('subject_id')->index();
$table->string('subject_type', 50);
$table->string('type', 50);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('activities');
}
}
та модель:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Activity extends Model
{
protected $guarded = [];
/**
* Get the subject for the activity
*
* @return MorphTo
*/
public function subject(): MorphTo
{
return $this->morphTo();
}
}
не забудемо додати визначення відносин в модель користувача (app/User.php
):
<?php
...
class User extends Authenticatable
{
...
/**
* Get the activities for the user
*
* @return HasMany
*/
public function activities(): HasMany
{
return $this->hasMany(Activity::class);
}
}
Як і коли записувати дії
Коли користувач робить будь-яку дію з тим або іншим контентом, laravel викликає відповідну подія, як-то created
, updated
, deleted
- скористаємося цим, і запишемо в бд, що і з чим було зроблено. Знову ж таки, ми не хочемо дублювати код в кожній моделі, тому просто напишемо трейт:
<?php
namespace App\Traits;
use App\Activity;
use ReflectionClass;
use ReflectionException;
trait RecordsActivity
{
/**
* Boot records activity
*
* @return void
*/
protected static function bootRecordsActivity(): void
{
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();
});
}
/**
* Get events to record for the current model
*
* @return array
*/
public static function getActivitiesToRecord(): array
{
return ['created'];
}
/**
* Get the activities for the given model
*
* @return mixed
*/
public function activity()
{
return $this->morphMany(Activity::class, 'subject');
}
/**
* Record event data to db
*
* @param string $event
*
* @throws ReflectionException
*
* @return void
*/
protected function recordActivity(string $event): void
{
$this->activity()->create([
'user_id' => auth()->id(),
'type' => $this->getActivityType($event),
]);
}
/**
* Get activity type dynamically
*
* @param string $event
*
* @throws ReflectionException
*
* @return string
*/
protected function getActivityType(string $event): string
{
return $event . '_' . strtolower((new ReflectionClass($this))->getShortName());
}
}
Пояснення:
- метод
bootRecordsActivity
буде викликатися при завантаженні моделі, аналогічно методуboot
. При цьому потрібно дотримуватися угоди про іменування. Як нескладно здогадатися, даний метод має починатися зі словаboot
за яким слідує назва трейту - ми не знаємо, які моделі задіють трейт, а тому використаємо пізнє статичне зв'язування
- для запису нам потрібен
id
користувача, тому не будемо реагувати на дії "гостей» (не аутентифікованих) - зверніть увагу на окрему обробку події
deleting
: при видаленні основного запису видаляються і записи про дії. В іншому випадку в таблиціactivities
залишаться посилання на контент, якого не існує. Це найпростіший варіант. Ніхто не заважає прикрутити якусь іншу логіку - метод
getActivitiesToRecord
містить події, на які ми реагуватимемо. Якщо, наприклад, хочете відстежувати оновлення/редагування, просто додайте в масивupdated
Тепер можна підключити трейт в будь-які моделі. Наприклад:
<?php
namespace App;
use App\Traits\RecordsActivity;
class Comment extends Model
{
use RecordsActivity;
...
}
Як виводити записану інформацію
Повернемося в модель Activity
і додамо статичний метод feed
. Для кращого сприйняття відсортуємо дії від останніх до перших, також згрупуємо записи за днями (як в Slack, Telegram і т.д.), і додамо кількість записів на запит:
/**
* Get the activity feed for the given user
*
* @param User $user
* @param int $take
*
* @return mixed
*/
public static function feed(User $user, int $take = 24)
{
return static::where('user_id', $user->id)
->latest()
->with('subject')
->take($take)
->get()
->groupBy(function ($activity) {
return $activity->created_at->format('Y-m-d');
});
}
Приклад використання в котролері:
<?php
namespace App\Http\Controllers;
use App\Activity;
use App\User;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Show a user's profile
*
* @param User $user
*
* @return View
*/
public function show(User $user): View
{
return view('profiles.show', [
'profileUser' => $user,
'activities' => Activity::feed($user)
]);
}
}
І останнє. Цілком ймовірно, що контент в історії буде найрізноманітніший, і створити якийсь універсальний в'ю не вийде. Буде потрібно окреме в'ю для кожного типу контенту. Але не робіть "простирадло" if-else
а-ля "якщо це стаття, то ..., якщо це коментар, то .." і т.д. - такі речі погано пахнуть. У нашому випадку з сортуванням за датами можна зробити, наприклад, так:
@foreach ($activities as $date => $activity)
<h3 class="page-header">{{ $date }}</h3>
@foreach ($activity as $record)
@if (view()->exists('profiles.activities.'.$record->type))
@include("profiles.activities.{$record->type}", ['activity' => $record])
@endif
@endforeach
@endforeach
що, як говорилося вище, передбачає наявність в директорії resources/views/profiles/activities
відповідних в'ю (created_post.blade.php
, created_comment.blade.php
і т.д.)
На цьому все. Успіхів!