Laravel 8

Créer un blog – les commentaires 1/2

Nous avons dans le précédent article codé l’affichage des articles. On a aussi prévu l’affichage des articles par catégorie, par étiquette et par auteur. On a aussi ajouté la possibilité de faire une recherche sur les titres, les contenus et les résumés. Nous allons maintenant voir nous intéresser aux commentaires.

J’ai fait le choix de ne pas les présenter immédiatement au chargement d’un article pour accélérer l’affichage, et puis on n’est pas forcément intéressé par les commentaires dans un premier temps. On va donc prévoir un bouton de chargement de ces commentaires s’il en existe. Il faudra aussi ajouter un formulaire de création. L’auteur d’un commentaire devra aussi pouvoir le supprimer s’il le désire. Pour fluidifier l’affichage on va utiliser Ajax et on va donc avoir pas mal de Javascript. On dispose de JQuery avec notre thème Calvin mais je préfère coder en pur Javascript, JQuery est appelé à décliner dans les prochaines années alors autant s’habituer à s’en passer, d’autant que les API des navigateurs sont désormais bien équipées.

Je vais aborder ce traitement des commentaires de façon progressive pour bien montrer la réflexion sous-jacente.

Vous pouvez télécharger le code final de cet article ici.

Le contrôleur

On a créé un contrôleur pour les commentaires en même temps que la migration et le modèle :

Comme pour le contrôleur des articles on va déplacer celui-ci dans le dossier Front (attention à l’espace de noms) :

De quoi allons-nous avoir besoin ?

  • on doit pouvoir créer un commentaire (nouveau ou réponse), donc une fonction store
  • on doit pouvoir supprimer un commentaire, donc une fonction destroy
  • on doit pouvoir récupérer tous les commentaires d’un article, donc une fonction comments

D’autre part toutes ces opérations devront passer en Ajax.

On va préparer le code du contrôleur :

<?php

namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;

class CommentController extends Controller
{
    public function __construct()
    {
        if(!app()->runningInConsole() && !request()->ajax()) {
            abort(403);
        }
    }

    public function store()
    {        
        
    }

    public function destroy()
    {

    }

    public function comments()
    {

    }
}

J’ai déjà prévu un constructeur pour vérifier qu’on est bien en Ajax, sinon on renvoie une erreur 403. On vérifie aussi qu’on n’est pas en mode console pour éviter d’avoir un souci avec les commandes d’Artisan, en particulier le listing des routes.

Afficher les commentaires

Le contrôleur et la route

On va commencer par coder le chargement des commentaires pour un article. On code la fonction dans le contrôleur :

use App\Models\Post;

...

public function comments(Post $post)
{
    $comments = $post->validComments()
                      ->withDepth()
                      ->latest()
                      ->get()
                      ->toTree();

    return [
        'html' => view('front/comments', compact('comments'))->render(),
    ];
}

Pour l’article concerné on va récupérer :

  • les commentaires valides (validComments)
  • avec la profondeur (withDepth)
  • classés par date (latest)
  • sous forme de tableau (toTree)

On ajoute la route :

use App\Http\Controllers\Front\{
    PostController as FrontPostController,
    CommentController as FrontCommentController
};

...

Route::prefix('posts')->group(function () {
    ...
    Route::name('posts.comments')->get('{post}/comments', [FrontCommentController::class, 'comments']);
});

Les vues pour les commentaires

Pour les vues on va devoir coder un système itératif parce que pour chaque commentaire il faut vérifier s’il y a des commentaires enfants et ainsi de suite. Ce genre de codage récursif n’est pas toujours très intuitif. On commence par prévoir une vue de base :

Avec un code très simple :

<h3>
    @lang('Comments')
    @if(Auth::guest())
        <span>@lang('You must be connected to add a comment or reply.')</span>
    @endif
</h3>
<!-- Commentlist -->
<ol class="commentlist">
    <x-front.comments :comments="$comments"/>
</ol>

On a un message pour les utilisateurs non connectés pour les avertir qu’ils ne peuvent pas laisser de commentaire.

Ensuite on ouvre une liste non ordonnée et on appelle un composant :

Dans ce composant on va itérer tous les commentaires :

@props(['comments'])

@foreach($comments as $comment)
    <x-front.comments-base :comment="$comment"/>
@endforeach

Donc ce composant sera appelé pour chaque commentaire et on ira ensuite explorer tous les enfants et les enfants des enfants…

On crée enfin le composant qui va effectivement créer le commentaire :

Avec ce code :

@props(['comment'])

<li class="comment">

  <div class="comment__avatar">
      <img class="avatar" src="{{ Gravatar::get($comment->user->email) }}">
  </div>

  <div class="comment__content">

      <div class="comment__info">
          <div class="comment__author">{{ $comment->user->name }}</div>

          <div class="comment__meta">
              <div class="comment__time">{{ formatDate($comment->created_at) }}</div>
              @if(Auth::check())
                  <div class="comment__reply">
                      @if($comment->depth < config('app.commentsNestedLevel'))
                          <a 
                              class="comment-reply-link replycomment" 
                              href="#" 
                              data-name="{{ $comment->user->name }}" 
                              data-id="{{ $comment->id }}">
                              @lang('Reply')
                          </a>
                      @endif
                      @if(Auth::user()->name == $comment->user->name)
                          <a 
                              href="#" 
                              class="comment-reply-link deletecomment" 
                              style="color:red">
                              @lang('Delete')
                          </a>
                      @endif 
                  </div>
              @endif
          </div>
      </div>

      <div class="comment__text">
          <p>{{ $comment->body }}</p>
      </div>

      <ul class="children">
          <x-front.comments :comments="$comment->children"/>
      </ul>

  </div>

</li>

Vous remarquez que dans ce composant on appelle le composant précédent à chaque fois, pour l’itération.

On utilise aussi une valeur dans la configuration (commentsNestedLevel) pour savoir s’il faut faire apparaître le bouton de réponse qui ne doit effectivement pas être présent si on atteint la profondeur maximale. On crée cette information dans config.app :

/*
|--------------------------------------------------------------------------
| Comments
|--------------------------------------------------------------------------
*/

'commentsNestedLevel' => 4,

Pour comprendre comment les commentaires sont explorés voilà une petite illustration :

La vue post

On va maintenant compléter la vue des articles (front.post) pour afficher les commentaires.

On commence par ajouter un bouton pour commander l’affichage des commentaires s’il y en a (on ajoute ce code en bas de la page) :

<!-- comments
================================================== -->
<div class="comments-wrap">

  <div id="comments" class="row">
      <div id="commentsList" class="column large-12">      

          @if($post->valid_comments_count > 0)
              <div id="forShow">
                  <p id="showbutton" class="text-center">
                      <a id="showcomments" href="{{ route('posts.comments', $post->id) }}" class="btn h-full-width">@lang('Show comments')</a>
                  </p>
                  <p id="showicon" class="h-text-center" hidden>
                      <span class="fa fa-spinner fa-pulse fa-3x fa-fw"></span>
                  </p>
              </div>
          @endif

      </div>
  </div>
</div>

Maintenant on a un bouton si des commentaires existent pour l’article :

On écritt du Javascript pour lancer la requête, récupérer et afficher les commentaires. Comme je l’ai déjà dit bien qu’on dispose de JQuery on va plutôt coder en simple Javascript :

@section('scripts')
    <script>
      (() => {

          // Variables
          const headers = {
              'X-CSRF-TOKEN': '{{ csrf_token() }}', 
              'Content-Type': 'application/json',
              'Accept': 'application/json',
              'X-Requested-With': 'XMLHttpRequest'
          }

          // Prepare show comments
          const prepareShowComments = e => {
              e.preventDefault();

              document.getElementById('showbutton').toggleAttribute('hidden');
              document.getElementById('showicon').toggleAttribute('hidden');
              showComments(); 
          }

          // Show comments
          const showComments = async () => {

              // Send request
              const response = await fetch('{{ route('posts.comments', $post->id) }}', { 
                  method: 'GET',
                  headers: headers
              });

              // Wait for response
              const data = await response.json();

              document.getElementById('commentsList').innerHTML = data.html;
          }

          // Listener wrapper
          const wrapper = (selector, type, callback, condition = 'true', capture = false) => {
              const element = document.querySelector(selector);
              if(element) {
                  document.querySelector(selector).addEventListener(type, e => { 
                      if(eval(condition)) {
                          callback(e);
                      }
                  }, capture);
              }
          };

          // Set listeners
          window.addEventListener('DOMContentLoaded', () => {
              wrapper('#showcomments', 'click', prepareShowComments);              
          })

      })()

    </script> 
@endsection

Pour la mise en place des écoutes pour les événements j’utilise une fonction universelle wrapper. Il suffit de lui transmettre toutes les informations. Dans notre cas on a un bouton avec un identifiant showcomments. On va détecter le clic sur ce bouton et diriger vers la fonction prepareShowComments.

Comme son nom l’indique cette fonction prépare le chargement des commentaires. Dans un premier temps on empêche l’événement de suivre son cours normal (preventDefault), ensuite on cache le bouton et on fait apparaître une icône animée d’attente. On appelle alors la fonction showComments qui va lancer la requête.

On a les headers dans la variable headers parce qu’ils nous resserviront. Dans la fonction showComments on utilise fetch pour lancer la requête. Au retour on envoie tout le paquet à son emplacement :

document.getElementById('commentsList').innerHTML = data.html;

Si tout se passe bien vous devriez avoir les commentaires qui s’affichent :

Conclusion

On a commencé à voir le traitement des commentaires dans cet article. On a maintenant l’affichage correct avec la hiérarchisation. Pour ne pas trop alourdir je traiterai les deux autres fonctionnalités, ajout et suppression, dans le prochain article. On aura ainsi codé la partie la plus importante du frontend. On complètera ensuite avec l’authentification pour laquelle il nous faut adapter les vues, le formulaire de contact et d’autres petites choses…

Print Friendly, PDF & Email

23 commentaires

  • Thibaut

    Bjr, un grd merci pour tes tutos tjr aussi explicite et complet, je suis entrain de refaire mon blog et j’ai ton approche surtt celui de l’utlisation des repositorys, jusqu’a present je m y interessait pas, mais grace a toi je vois tout son interet.
    neanmoins je rencontre un probleme avec l’affichage des commentaires, j’ai suivi à la lettre ton tutos bien qu’utilisant mon propre template que j’ai codé avec tailwindcss.
    j’ai bien la function validComments dans mon model post, mais lorsque je fais un de dd($post); dans mon PostController(la function show) je vois que les commentaires ne sont validés.
    pour voir le button show comment j’ai du commenté le condition @if($post->valid_comments_count > 0) et une fois commenté j’ai mon button show comments et lorsque je click j’ai le message me demandant d’etre connectet pour poster un commentaire, mais je n’ai pas les commentaires, j’ai inspecté et il n y a pas d’erreur sur la console, et j’ai verifie j’ai un status 200 lors de la requete ajax.
    je ne comprends d’où vient le probleme, merci pour ton aide!

          • Thibaut

            bjr
            dans la relation validComments est vide:
            #relations: array:1 [▼
            « validComments » => Kalnoy\Nestedset\Collection {#1494 ▼
            #items: []
            }
            ]

          • bestmomo

            Il faudrait voir la cohérence des données. Mettre tous les users valides et vérifier que les articles sotn bien liés à des commentaires.

          • Thibaut

            ok j’ai mis tt les users valides et en faisant :
            $testPost = Post::with(‘validComments’)->find(1);
            dd($testPost);
            j’ai bien:
            #relations: array:1 [▼
            « validComments » => Kalnoy\Nestedset\Collection {#1525 ▶}

            je recupere bien les commentaires, mais la j’ai un pb d’autorisation qui n »etait pas la lorsque je click sur le bouton show comments, j’ai une page 403

          • bestmomo

            Au niveau de l’affichage des commentaires on n’a prévu aucune autorisation (pas de middleware sur la route et pas de policy)…

          • Thibaut

            ok cool , merci les commentaires fonctionnent tres bien , tout s’affiches et j’ai pu integre avec mon design sans pb , reste plus qu ‘a gerer les alertes vu que j’ai deja un system d’alert avec alpine js, je vais un peu bidouille et voir si je vais y arrivé et sinon je verrais comment le faire avec sweetalert, meme au font j’aimerai utilisé que alpine js comme librairy js. un grd merci pour ta reactivité

  • bensa

    Salut!

    C’est un plaisir de suivre votre tuto 🙂
    svp j’ai pas compris cette déclaration dans post.blade.php: @if($post->valid_comments_count > 0)
    en fait dans quelle partie on a déclaré ce champ ou bien cette fonction: valid_comments_count

    Merci beaucoup.

    • bestmomo

      Salut,

      Dans le modèle Post il y a la méthode validComments qui renvoie tous les commentaires validés. Dans les requêtes on prévoit ->withCount(‘validComments’) qui donne donc le nombre de ces commentaires valides. Et par définition on retrouve ce nombre dans la propriété valid_comments_count.

  • ronald169

    je sais pas pour vous mais la méthodes ->withDepth() (aller en profondeur) ne marche pas chez moi et lorsque je check cela sur la doc de laravel j’y voie rien. mais lorque je commente cette methode tous donne bien.
    « laravel/framework »: « ^8.12 »,

  • ronald169

    Salut le best c’est toujour un plasir de te lire et d’apprendre avec toi jusqu’ici tout marche comme sur des roulettes. Mais j’ai une demande a faire si je peux me le permettre, etant donnée que nous somme en Laravel 8, pourquoi encore rentrer dans ce bon vieux Jquery ? ne serait-il pas préférable de gérer les comment avec Livewire ou encore Vuejs ?
    ca sera vraiment benefique pour nous nous permettra dans la meme occasion d’en apprendre un peu plus sur le sujet.
    Merci

    • bestmomo

      Salut,
      C’est une bonne question et il n’est pas facile de répondre rapidement. Tu as pu remarquer que j’ai écrit le Javascript pour les commentaires sans utiliser de librairie, même pas JQuery qui est pourtant disponible avec le thème choisi. Concernant Vue.js j’ai écrit un cours sur le sujet et je l’ai utilisé un certain temps. Pour Livewire j’ai rédigé une introduction et je l’ai bien exploré.
      Personnellement je distingue deux cas : soit on a une SPA avec une prépondérance du frontend, soit une application classique avec une prépondérance de backend. Dans le premier cas une librairie Javascript s’impose, quelle qu’elle soit, et pourquoi pas Vue.js, et Laravel se pose juste en pourvoyeur d’API (et autant utiliser Lumen). Mais dans le second cas c’est franchement inutile.
      En ce qui concerne Livewire, après l’avoir décortiqué, je trouve que le concept peut sembler séduisant mais au final je trouve que c’est un monstre inutile qui complique plus la vie qu’autre chose. Ce mélange des genres est source de confusion. Mais ce n’est que mon avis.
      Enfin je me place dans une perspective didactique et je trouve plus pertinent d’en revenir chaque fois au plus près des technologies de base du web. Quand on maîtrise HTML, CSS, Javascript et PHP on peut utiliser n’importe quelle fantaisie à la mode, ou pas…
      Dans la gestion des commentaires j’ai justement épuré la partie Javascript en l’isolant de toute librairie pour en revenir à plus de simplicité et montrer que les API des navigateurs permettent désormais de gérer facilement l’interactivité côté client, et il me semble que c’est bien plus instructif.
      Mais encore une fois ce n’est que mon point de vue.

  • Josuke

    Salut! Merci a toi pour ces tutos! J’ai un soucis quand je clique sur show comments ça ne montre pas les commentaires.
    Et en voulant mettre un commentaire j’ai le « Something went wrong » si jamais tu peu m’aider.
    Merci a toi!

Laisser un commentaire