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

47 commentaires

Laisser un commentaire