Історія дій користувачів

Історія дій користувачів

Реалізуємо на 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 і т.д.)

 

На цьому все. Успіхів!