Les commentaires sont un élément essentiel d'un système de gestion de contenu (CMS) moderne. Ils permettent d'établir un dialogue entre l'auteur et les lecteurs, et enrichissent ainsi l'expérience de lecture tout en favorisant l'engagement de la communauté.
Pour implémenter un système de commentaires robuste et sécurisé, voici les principales fonctionnalités à considérer :
- Visibilité des commentaires : tous les visiteurs du site pourront lire les commentaires, assurant ainsi une transparence totale des discussions.
- Authentification requise : seuls les utilisateurs authentifiés auront la possibilité de poster des commentaires. Cette mesure permet de réduire le spam et d'encourager des discussions de qualité.
- Modération du premier commentaire : pour chaque nouvel utilisateur, son premier commentaire sera soumis à l'approbation d'un administrateur. Cette étape supplémentaire permet de prévenir le spam et de maintenir la qualité des échanges.
- Structure hiérarchique :
- Les utilisateurs pourront répondre directement à un commentaire existant.
- Il sera également possible de répondre aux réponses, créant ainsi une conversation structurée.
- Pour maintenir une lisibilité optimale, la profondeur des réponses sera limitée à quatre niveaux.
- Gestion des notifications : mettre en place un système permettant aux utilisateurs de recevoir des notifications lorsque quelqu'un répond à leur commentaire.
- Modération continue : prévoir des outils pour que les administrateurs puissent modérer les commentaires après leur publication si nécessaire (suppression, édition, etc.).
En implémentant ces fonctionnalités, votre CMS offrira une expérience de commentaires riche et interactive, tout en maintenant un niveau de sécurité et de qualité élevé. Cela encouragera l'engagement des lecteurs et contribuera à créer une communauté active autour de votre contenu.
Les données
La migration
On crée la migration et le modèle avec la même commande :
php artisan make:model Comment --migration
Pour la migration :
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->text('body');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->unsignedBigInteger('parent_id')->nullable()->default(null);
$table->foreign('parent_id')
->references('id')
->on('comments')
->onDelete('cascade');
});
}
Ce code crée une table comments avec plusieurs colonnes :
id : une clé primaire auto-incrémentée.
created_at et updated_at : des colonnes de type timestamp pour suivre les créations et mises à jour.
body : une colonne de type texte pour stocker le contenu du commentaire.
user_id : une clé étrangère référençant la table users.
post_id : une clé étrangère référençant la table posts.
parent_id : une clé étrangère référençant la même table comments pour les relations hiérarchiques entre commentaires. Cette colonne peut être nulle (nullable()) et sa valeur par défaut est null. Si elle est nulle ça signifie que c'est un commentaire racine, sinon elle référence le commentaire parent.Les contraintes onDelete('cascade') assurent que les commentaires sont supprimés en cascade lorsque les utilisateurs, les posts ou les commentaires parents sont supprimés.
La factory
On crée la factory :
php artisan make:factory CommentFactory
On va juste renseigner le body :
public function definition(): array
{
return [
'body' => fake()->paragraph($nbSentences = 4, $variableNbSentences = true),
];
}
Le modèle Comment
Dans le modèle Comment, on renseigne la propriété $fillable et on crée les relations nécessaires :
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany};
class Comment extends Model
{
use HasFactory, Notifiable;
protected $fillable = [
'body',
'post_id',
'user_id',
'parent_id',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
public function parent(): BelongsTo
{
return $this->belongsTo(Comment::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(Comment::class, 'parent_id');
}
}
Explication des relations :
public function user(): BelongsTo
Cette méthode définit une relation belongsTo entre le modèle Comment et le modèle User. Cela signifie qu'un commentaire appartient à un utilisateur. La clé étrangère utilisée pour cette relation est user_id.
Exemple d'utilisation : $comment->user renverra l'instance de l'utilisateur qui a créé le commentaire.public function post(): BelongsTo
Cette méthode définit une relation belongsTo entre le modèle Comment et le modèle Post. Cela signifie qu'un commentaire appartient à un post. La clé étrangère utilisée pour cette relation est post_id.
Exemple d'utilisation : $comment->post renverra l'instance du post auquel le commentaire est associé.public function parent(): BelongsTo
Cette méthode définit une relation belongsTo entre le modèle Comment et lui-même (Comment). Cela signifie qu'un commentaire peut appartenir à un autre commentaire (parent). La clé étrangère utilisée pour cette relation est parent_id.
Exemple d'utilisation : $comment->parent renverra l'instance du commentaire parent, si elle existe.public function children(): HasMany
Cette méthode définit une relation hasMany entre le modèle Comment et lui-même (Comment). Cela signifie qu'un commentaire peut avoir plusieurs commentaires enfants. La clé étrangère utilisée pour cette relation est parent_id.
Exemple d'utilisation : $comment->children renverra une collection de commentaires enfants associés à ce commentaire.
Une petite visualisation ne fait jamais de mal :
Les relations dans le modèle Post
À présent que des commentaires vont exister pour les articles, il faut pouvoir les récupérer à partir du modèle Post :
use Illuminate\Database\Eloquent\Relations\{ HasMany, BelongsTo};
class Post extends Model
{
...
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function validComments(): HasMany
{
return $this->comments()->whereHas('user', function ($query) {
$query->whereValid(true);
});
}
}
On a deux accès à la relation :
- on récupère tous les commentaires sans distinction avec comments
- on ne s'intéresse qu'aux commentaires pour lesquels l'utilisateur a été validé par un administrateur avec validComments
La relation dans le modèle User
Un utilisateur peut avoir plusieurs commentaires, donc dans le modèle User :
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
Le seeder
On prévoit aussi un seeder :
php artisan make:seeder CommentSeeder
Voici le code :
<?php
namespace Database\Seeders;
use App\Models\Comment;
use Illuminate\Database\Seeder;
class CommentSeeder extends Seeder
{
public function run()
{
$nbrPosts = 9;
$nbrUsers = 3;
foreach (range(1, $nbrPosts - 1) as $i) {
$this->createComment($i, rand(1, $nbrUsers));
}
$comment = $this->createComment(2, 3);
$this->createComment(2, 2, $comment->id);
$comment = $this->createComment(2, 2);
$this->createComment(2, 3, $comment->id);
$comment = $this->createComment(2, 3, $comment->id);
$comment = $this->createComment(2, 1, $comment->id);
$this->createComment(2, 3, $comment->id);
$comment = $this->createComment(4, 1);
$comment = $this->createComment(4, 3, $comment->id);
$this->createComment(4, 2, $comment->id);
$this->createComment(4, 1, $comment->id);
}
protected function createComment($post_id, $user_id, $id = null)
{
return Comment::factory()->create([
'post_id' => $post_id,
'user_id' => $user_id,
'parent_id' => $id,
]);
}
}
On crée quelques commentaires et aussi des enfants de commentaires.
Enfin, dans DatabaseSeeder, on prévoit l'appel du nouveau seeder :
public function run(): void
{
$this->call([
...
CommentSeeder::class,
]);
}
Il ne reste plus qu'à rafraîchir la base
php artisan migrate:fresh --seed
Vérifiez que vous avez les commentaires dans votre base.
Livewire et les composants imbriqués
Livewire (et son rejeton Volt) est un framework JavaScript pour Laravel qui permet de créer des interfaces utilisateur dynamiques et interactives sans avoir besoin de beaucoup de JavaScript, et même souvent pas du tout. L'une des fonctionnalités les plus puissantes de Livewire est la possibilité de nicher des composants Livewire supplémentaires à l'intérieur d'un composant parent. Cette capacité est extrêmement utile, car elle permet de réutiliser et d'encapsuler des comportements au sein de composants Livewire partagés à travers votre application.
Le nesting de composants Livewire signifie que vous pouvez inclure un ou plusieurs composants Livewire à l'intérieur d'un autre composant Livewire. Cela permet de créer des structures de composants hiérarchiques, où chaque composant peut gérer sa propre logique et son propre état, tout en étant capable de communiquer avec les composants enfants ou parents.
Pourquoi Utiliser le Nesting de Composants ?
Réutilisabilité : En encapsulant des comportements spécifiques dans des composants réutilisables, vous pouvez éviter la duplication de code et rendre votre application plus maintenable.
Encapsulation : Chaque composant peut gérer sa propre logique et son propre état, ce qui rend le code plus modulaire et plus facile à comprendre.
Communication : Les composants parents et enfants peuvent communiquer entre eux, permettant de créer des interactions complexes sans avoir besoin de beaucoup de JavaScript.
Vous pouvez trouver une documentation complète sur le sujet ici.
Cette possibilité se prête idéalement à nos besoins en matière de gestion des commentaires, et nous allons l'utiliser parce que nous aurons du code itératif. En effet, la possibilité d'emboiter des composants Livewire permet de structurer notre application de manière modulaire et réutilisable. Cela est particulièrement utile pour des fonctionnalités comme les commentaires, où chaque commentaire peut être représenté par un composant Livewire (en fait Volt dans notre cas) distinct.
Nous allons avoir besoin de trois composants :
- Composant Volt de base : ce composant sera placé en fin d'article et doit être présent même s'il n'y a encore aucun commentaire, afin d'afficher le formulaire de saisie. Il doit également être présent pour tout commentaire racine qui sera ajouté.
- Composant Volt pour chaque commentaire existant : ce composant encapsulera chaque commentaire existant et offrira les fonctionnalités de suppression et de modification pour l'auteur du commentaire, ainsi que la possibilité de répondre pour tous les utilisateurs authentifiés.
- Composant classique pour le formulaire : ce composant sera utilisé pour afficher le formulaire de saisie des commentaires, permettant aux utilisateurs de soumettre de nouveaux commentaires ou de répondre à des commentaires existants.
Livewire est vraiment performant, mais il faut se méfier de certains pièges. En particulier au niveau des données tolérées dans les propriétés. Je n'avais pas suffisamment lu la documentation et j'ai eu quelques soucis avec les composants imbriqués avant de comprendre les limitations imposées. Il est très judicieux de lire cette partie de la documentation. Il faut se méfier des relations que vous chargez, vous ne les retrouvez pas forcément et vous pouvez passer de longs moments à chercher votre bug. J'ai dû remanier mes composants pour avoir quelque chose qui fonctionne correctement.
Comme cette partie est délicate et mérite d'être détaillée, je lui consacrerai un article spécifique dans la foulée de celui-ci.
Préparer le terrain
Le repository PostRepository
Les commentaires trouveront naturellement leur place à la fin de chaque article. Dans un premier temps, il faut donc ajouter des informations. La stratégie que j'ai adoptée est de ne pas immédiatement afficher les commentaires, mais plutôt prévoir un bouton s'il y en a. Dans le repository PostRepository on complète la fonction getPostBySlug :
public function getPostBySlug(string $slug): Post
{
return Post::with('user:id,name', 'category')
->withCount('validComments')
->whereSlug($slug)->firstOrFail();
}
La méthode withCount d'Eloquent permet de connaître le nombre d'enregistrements dans une table en relation sans charger les enregistrements correspondants.
Le composant posts.show
Le composant posts.show est celui qui affiche un article. Dans la partie PHP, on doit récupérer le nombre de commentaires :
new class extends Component {
...
public int $commentsCount;
public function mount($slug): void
{
$postRepository = new PostRepository();
$this->post = $postRepository->getPostBySlug($slug);
$this->commentsCount = $this->post->valid_comments_count;
}
}; ?>
Et pour l'affichage :
...
<div class="flex justify-between">
<p>@lang('By ') {{ $post->user->name }}</p>
<em>
@if ($commentsCount > 0)
@lang('Number of comments: ') {{ $commentsCount }}
@else
@lang('No comments')
@endif
</em>
</div>
<div id="bottom" class="relative items-center w-full py-5 mx-auto md:px-12 max-w-7xl">
@if ($commentsCount > 0)
<div class="flex justify-center">
<x-button label="{{ $commentsCount > 1 ? __('View comments') : __('View comment') }}" class="btn-outline" spinner />
</div>
@endif
</div>
</div>
On complète les traductions :
"Number of comments: ": "Nombre de commentaires : ",
"View comments": "Voir les commentaires",
"No comments": "Aucun commentaire"
Le bouton est prêt à l'action, mais pour le moment, il ne fonctionne pas. Nous n'avons pas encore créé les composants pour gérer les commentaires, mais nous pouvons encore préparer le terrain en allant chercher toutes les informations dans la base.
On ajoute une fonction showComments pour charger les commentaires de premier niveau (c'est-à-dire ceux qui n'ont aucun parent) :
<?php
...
use Illuminate\Support\Collection;
new class extends Component {
...
public Collection $comments;
public bool $listComments = false;
...
public function showComments(): void
{
$this->listComments = true;
$this->comments = $this->post
->validComments()
->where('parent_id', null)
->withCount([
'children' => function ($query) {
$query->whereHas('user', function ($q) {
$q->where('valid', true);
});
},
])
->with([
'user' => function ($query) {
$query->select('id', 'name', 'email', 'role')->withCount('comments');
},
])
->latest()
->get();
}
}; ?>
$this->comments = $this->post->validComments()
$this->post : Cela fait référence à l'objet post qui contient les informations de l'article à afficher.
validComments() : C'est une méthode définie dans le modèle Post qui retourne une collection de commentaires avec un utilisateur valide.->where('parent_id', null)
Cette méthode filtre les commentaires pour ne garder que ceux dont le parent_id est null. Cela signifie qu'on ne garde que les commentaires de premier niveau (pas les réponses à d'autres commentaires).->withCount([...])
Cette méthode ajoute un compteur de relations liées. Ici, elle compte le nombre de children (commentaires enfants) pour chaque commentaire.
'children' => function ($query) { ... } : Cela permet de filtrer les commentaires enfants pour ne compter que ceux dont l'utilisateur est valide (valid est true).->with([...])
Cette méthode charge les relations spécifiées avec les commentaires.
'user' => function ($query) { ... } : Cela charge les informations de l'utilisateur associé à chaque commentaire.
$query->select('id', 'name', 'email', 'role')->withCount('comments') : Cela sélectionne uniquement les colonnes id, name, email, et role de l'utilisateur et ajoute un compteur de commentaires pour chaque utilisateur.->latest()
Cette méthode trie les commentaires par ordre décroissant de la date de création (les plus récents en premier).->get()
Cette méthode exécute la requête et retourne une collection de commentaires.
Il suffit maintenant de compléter le bouton pour appeler la fonction au clic :
<x-button label="{{ $commentsCount > 1 ? __('View comments') : __('View comment') }}"
wire:click="showComments" class="btn-outline" spinner />
Pour le moment, vous ne verrez pas grand-chose en cliquant sur le bouton, mais vous pouvez vérifier que ça fonctionne. Il suffit de prévoir une interception :
dd($this->comments);
Et de constater le résultat :
Illuminate\Database\Eloquent\Collection {#609 ▼ // resources\views\livewire\posts\show.blade.php:44
#items: array:3 [▼
0 => App\Models\Comment {#612 ▶}
1 => App\Models\Comment {#606 ▶}
2 => App\Models\Comment {#616 ▶}
]
#escapeWhenCastingToString: false
}
Conclusion
Nous avons toutes les données et les relations pour les commentaires. La page des articles est prête à les recevoir. Dans la prochaine étape, on créera les trois composants pour les gérer.
Pour vous simplifier la vie, vous pouvez charger le projet dans son état à l’issue de ce chapitre.
Par bestmomo
Aucun commentaire