В данной статье рассмотрим один из наиболее удобных вариантов создания функционала комментариев для сайта на 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!