En esta entrada, vamos a crear un sistema de comentarios anidados desde 0 con Laravel. ¿Y qué es un sistema de comentarios anidados? Pues, básicamente, es un sistema de comentarios en el que los comentarios pueden tener comentarios hijos, es decir, respuestas. Podríamos decir, que será algo parecido al sistema que tiene WordPress y que puedes ver en este mismo blog en la sección de comentarios (por ejemplo, aquí).
Preparación y migraciones
Primero de todo, debemos tener un proyecto Laravel, si no lo tienes echa un vistazo a este post. Una vez tengamos el proyecto ya listo, tenemos que crear las migraciones para generar las tablas necesarias en la base de datos. Vamos a crear una para los posts y otra para los comentarios:
1 2 |
php artisan make:migration create_posts_table php artisan make:migration create_comments_table |
Con estos comandos, se nos generarán los archivos para las migraciones. En el archivo de migración de la tabla posts podremos lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('content'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } } |
Y en el de la tabla comentarios lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateCommentsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->text('content'); $table->integer('parent_id')->unsigned()->nullable(); $table->integer('post_id')->unsigned(); $table->integer('user_id')->unsigned(); $table->timestamps(); $table->foreign('parent_id')->references('id')->on('comments'); $table->foreign('post_id')->references('id')->on('posts'); $table->foreign('user_id')->references('id')->on('users'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('comments'); } } |
Como puedes ver, le estamos indicando que un comentario tiene una relación con un post, un usuario creador y que puede tener un comentario padre (es decir, que podría ser respuesta de un comentario). Este último, no es obligatorio al haber puesto que puede ser null (nullable()).
Ejecutamos las migraciones:
1 |
php artisan migrate |
Modelos
Creamos los dos modelos, uno para los posts y otro para los comentarios. En el de los posts ponemos este código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { protected $fillable = [ 'title', 'content', ]; public function comments() { return $this->hasMany('App\Comment')->whereNull('parent_id'); } } |
Y en el modelo de los comentarios ponemos este código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model { protected $fillable = [ 'content', 'parent_id', 'post_id', 'user_id', ]; public function post() { return $this->belongsTo('App\Post'); } public function user() { return $this->belongsTo('App\User'); } public function parent() { return $this->belongsTo('App\Comment', 'parent_id'); } public function replies() { return $this->hasMany('App\Comment', 'parent_id'); } } |
El código anterior creo que es bastante entendible, pero simplemente lo que le estamos indicando a los modelos son las relaciones que hemos comentado antes.
Controladores y rutas
Ahora vamos a crear los controladores, que serán los que tendrán toda la lógica. Aunque primero de todo vamos a añadir las rutas al archivo de rutas. Si quieres saber más sobre rutas y los controladores, échale un vistazo a este post.
1 2 3 4 5 6 7 8 |
... Route::middleware(['auth'])->group(function() { Route::get('/post/{post}', 'PostController@show')->name('posts.show'); Route::post('/post/{post}/comment', 'CommentController@store')->name('comments.store'); }); ... |
Una vez tenemos las rutas, creamos los controladores, uno para los posts y otro para los comentarios. En el de los posts ponemos el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Post; class PostController extends Controller { public function show(Post $post) { return view('posts.show', compact('post')); } } |
Y en el de los comentarios éste:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Post; use App\Comment; class CommentController extends Controller { public function store(Request $request, Post $post) { $comment = new Comment(); $comment->content = $request->content; $comment->parent_id = $request->parent_id; $comment->user_id = \auth()->id(); $post->comments()->save($comment); return \redirect()->route('posts.show', $post); } } |
Lo que hace el código del método store es crear un objeto comentario con los datos que nos llegan del formulario y con el id del usuario logueado (el que hace el comentario). Después, simplemente, lo guardamos y redirigimos a la url del post.
Como mejora, estaría bien ponerle una validación de que los datos que nos llegan del formulario son los que queremos y en el formato que queremos.
Vistas
Para que funcione todo esto, aún nos falta una cosa más, las vistas. La primera vista que vamos a crear va a ser la de la ficha del post. Creamos un archivo llamado show.blade.php en el directorio resources/views/posts/ :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-12"> <h1>{{ $post->title }}</h1> <div> {{ $post->content }} </div> @include('comments.list', ['comments' => $post->comments]) @include('comments.form') </div> </div> </div> @endsection |
Al la vista 'comments.list' , que ahora crearemos, le estamos pasando los comentarios que no tienen padre, es decir, los comentario que no tienen respuestas (parent_id = null) e incluimos el formulario para añadir comentarios.
Cuando tengamos esto, aún nos seguirá sin funcionar porque nos faltan las vistas de los comentarios que son las dos vistas que incluye ( @include ) el archivo anterior y una más.
Creamos la vista del formulario de envío de comentarios en resources/views/comments/form.blade.php :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<form action="{{route('comments.store', $post)}}" method="POST"> {{ csrf_field() }} @if (isset($comment->id)) <input type="hidden" name="parent_id" value="{{$comment->id}}"> @endif <input type="hidden" name="user_id" value="{{\auth()->id()}}"> <div class="form-group"> <label for="content">Content:</label> <textarea class="form-control" name="content" id="content"></textarea> </div> <button type="submit" class="btn btn-primary">Send</button> </form> |
Creamos la de la lista de comentarios en resources/views/comments/list.blade.php:
1 2 3 |
@foreach($comments as $comment) @include('comments.item', ['comment' => $comment]) @endforeach |
Y finalmente, la del comentario en sí en resources/views/comments/item.blade.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<div class="p-4 border-left my-3"> <p class="font-weight-bold">User {{ $comment->user->name }}:</p> <p>{{ $comment->content }}</p> <button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#reply-{{$comment->id}}" aria-expanded="false" aria-controls="reply-{{$comment->id}}"> Reply </button> <div class="collapse my-3" id="reply-{{$comment->id}}"> <div class="card card-body"> @include('comments.form', ['comment' => $comment]) </div> </div> @if ($comment->replies) @include('comments.list', ['comments' => $comment->replies]) @endif </div> |
Esta última vista, será la que haga que visualmente parezca que un comentario cuelga de otro, es decir, que visualmente tenga respuestas. Se comprueba si el comentario actual del bucle tiene respuestas (replies), si tiene, se vuelve a inlcuir la lista ( 'comments.list') con las respuestas y así. Esto hará que sea recursivo y que por muchos que añadamos, siempre salgan de esta forma.
Si todo ha ido bien, el resultado final debería ser este (los usuarios están generados con seeders):
-
en la ultima parte me da un error que es crear la lista de comentarios, el error es traer la lista de los comentarios a la vista principal