Функционал комментариев для сайта на laravel

06 Марта 2023 21:16

В данной статье рассмотрим один из наиболее удобных вариантов создания функционала комментариев для сайта на laravel с использованием полиморфной связи "Один ко многим".

Для начала, что такое полиморфная связь "Один ко многим"? Лучший ответ в документации. Однако, если вкратце - это связь, при которой одна модель может иметь в подчинении много экземпляров другой модели, в нашем случае - комментариев. А полиморфная, потому что эти комментарии могут относиться к другим моделям. Т.е. комментарии могут быть как у статей, так и у новостей, видеороликов и др.

То есть, вместо того, чтобы создавать отдельные таблицы комментариев для каждой модели, мы имеем лишь одну таблицу с комментариями, в которой есть поле, указывающее на конкретную модель, которой принадлежит комментарий.

Для визуализации, схема таблиц и полей выглядит так:

articles:
  id
  title
  content
  
news:
  id
  title
  content
  
comments:
  id
  commentable_id
  commentable_type
  content
  created_at

Создаем миграцию для комментариев

Первое, с чего нужно начать - создать миграцию, которая сформирует нам таблицу для хранения комментариев.

Запускаем команду в терминале php artisan make:migration create_comments_table.

Идем в созданный файл - app/database/migrations/2022_04_23_201928_create_comments_table.php (Дата в начале файла с максимальной вероятностью будет другая).

Прописываем туда:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->text('content');
            $table->morphs('commentable');

            $table->timestamps();
            $table->softDeletes();

            $table->foreign('user_id')
                  ->references('id')
                  ->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

Что тут у нас по полям:

  • id - айдишник комментария с автоинкрементом
  • user_id - айдишник пользователя, оставившего комментарий.
  • content - поле для хранения текста комментария
  • morphs('commentable') - метод, который создаст нам 2 поля в таблице с перфиксом, который передан в параметре:
    • commentable_id - айдишник сущности, в которой оставлен комментарий
    • commentable_type - собственно, тип сущености, в которой оставлен комментарий. По умолчанию, в это поле будет сохраняться весь путь неймспейса модели, например, App\Content\Posts\News, но можно сделать алиасы.
  • timestamps() - метод, который создаст нам 2 поля с датами:
    • created_at - дата создания комментария
    • updated_at - дата обновления комментария
  • softDeletes() - метод, который создаст нам поле с датой:
    • deleted_at - дата удаления комментария

А так же:

  • foreign('user_id')->references('id')->on('users'); - референс для пользователей, чтобы связать user_id в таблице comments с id в таблице users.

Многие конструкции, из выше указанных, - по желанию! Самые основные, без которых функционал будет невозможен: id(), text('content'), morphs('commentable').

Формируем алиасы для полиморфных связей

Эти аласы будут сохраняться в базу, вместо полного пути модели.

Идем в файл app/Providers/AppServiceProvider.php.

Добавляем в метод boot() те, модели, к которым будут прикручены комментарии, по такой схеме:

public function boot()
{
    Relation::enforceMorphMap([
        'articles' => 'App\Models\Posts\Articles',
        'news' => 'App\Models\Posts\News',
    ]);
}

Теперь в поле commentable_type таблицы comments будет сохраняться не App\Models\Posts\Articles или App\Models\Posts\News, а articles или news.

Это удобно если, например, в какой-то момент может частично поменяться архитектура проекта. Тогда не придется обрабатывать все записи в базе, а нужно будет лишь поправить этот конфиг в сервис провайдере.

Создаем модель для комментариев

Запускаем команду в терминале php artisan make:model App\Models\Comment.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $fillable = [
        'user_id',
        'content',
        'updated_at',
        'deleted_at'
    ];

    public function commentable()
    {
        return $this->morphTo();
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Самое главное здесь - метод commentable(). С помощью него мы можем получать родительскую модель articles или news, к которой принадлежит комментарий. 

Создаем трейт с комментариями

Поскольку, комментарии будут в нескольких моделях, то целесообразно не прописывать методы сразу в модели, а сделать трейт.

Создаем файл по такому пути - app/Models/Traits/HasComments.php.

Добавляем в него следующее:

<?php

namespace App\Models\Traits;

use App\Models\Comment;

trait HasComments
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Метод comments() будет доступен в нужных нам моделях после подключения в них этого трейта.

Пример модели, использующей комментарии

Теперь все, что нужно - добавить трейт в нужные нам модели.

Сама модель будет выглядеть примерно так:

<?php

namespace App\Models\Posts;

use Illuminate\Database\Eloquent\Model;
use App\Models\Traits\HasComments;

class Article extends Model
{
    use HasComments;

    /*some code*/
}

Примеры

Теперь мы можем работать с комментариями.

Самое основное, что нам нужно понимать, что теперь нам в наших моделях, куда мы добавили трейт, доступен метод comments() и свойство comments.

Свойство comments - динамическое, через него мы можем сразу получить доступ к комментариям текущей модели. Например

$article = Article::find(5);

foreach($article->comments as $comment){
    // обрабатываем комментарии
}

Метод comments() может быть использован для построителя запросов, если мы хотим выбрать определенные комментарии. Или создавать комментарии через метод create(). Например

$article = Article::find(5);

$someComments = $article->comments()->where('user_id', 3)->get();

Создание комментариев для разных моделей

Что на счет создания комментариев для разных моделей? Здесь мы используем алиасы моделей которые мы прописывали в AppServiceProvider.php.

Рассмотрим последовательное движение комментария с момента нажатия кнопки "Отправить" пользователем:

1. Отправка данных.

С фронта отправляются данные вида:

{
    commentable_id: 154,
    commentable_type: 'article',
    content: 'Text of comment'
}

2. Маршрут

Данные отправляются на роут:

Route::prefix('comments')->group(function() {
    Route::post('store', [CommentController::class, 'store']);
});

3. Валидация

Из роута понятно, что обработкой запроса будет заниматься метод store() в контроллере CommentController пройдя через валидацию в Request:

<?php

namespace App\Http\Requests\Comment;

use Illuminate\Foundation\Http\FormRequest;

class StoreRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'commentable_id' => 'required|integer',
            'commentable_type' => 'required|string',
            'content' => 'required|string|max:2048'
        ];
    }
}

4. Контроллер

Собственно, сам контроллер:

<?php

namespace App\Http\Controllers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use App\Http\Requests\Comment\StoreRequest;
use App\Http\Resources\CommentResource;

class CommentController extends Controller {

    public function store(StoreRequest $request)
    {
        $data = $request->validated();

        /** @var Model $class */
        $class = Relation::getMorphedModel($data['commentable_type']);
        if (!$class) {
            throw new \Exception('Class not found');
        }

        $model = $class::find($data['commentable_id']);
        if (!$model) {
            throw new \Exception('Model not found');
        }

        $comment = $model->comments()->create([
            'user_id' => auth()->id(),
            'content' => $data['content']
        ]);

        return new CommentResource($comment);
    }
}

В вышеуказанном методе самое интересное это $class = Relation::getMorphedModel($data['commentable_type']); - тут мы получаем по алиасу связанную модель и уже потом конкретную статью или новость по айдишнику, переданному с фронта. Все остальное: проверка ошибок, возвращаемые данные и т.д. -  под ваш codestyle!