Выводим историю действий пользователей

Выводим историю действий пользователей

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

 

На этом всё. Удачи!