Laravel 8

Créer un blog – modifier ou supprimer un article

Nous avons dans le précédent article codé la création d’un article. On en a profité pour ajouter des composants qu’on réutilisera pour les autres entités. A présent nous allons voir comment modifier un article, ce qui sera grandement facilité par notre précédent travail puisque le formulaire est pratiquement le même que celui de la création, surtout que nous nous sommes arrangés pour préparer le terrain, de même pour la partie validation. Nous verrons ensuite la suppression d’un article, là il nous faudra un peu de Javascript parce qu’on ne va pas procéder à une suppression immédiate mais plutôt demander la confirmation au rédacteur pour éviter un regrettable accident.

On doit aussi se poser des questions concernant la sécurité. Il ne faut pas qu’un rédacteur puisse modifier ou supprimer l’article d’un autre rédacteur.

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

Modifier un article

Dans le tableau des articles on dispose d’un bouton pour la modification d’un article :

Chacun de ces boutons comporte dans l’attribut href l’url correspondant à l’article concerné.

Le repository

Dans le repository PostRepository on crée une méthode update :

public function update($post, $request)
{
    $request->merge([
        'active' => $request->has('active'),
        'image' => basename($request->image),
    ]);

    $post->update($request->all());

    $this->saveCategoriesAndTags($post, $request);
}

On se rend compte que le code est très proche de celui que nous avons écrit pour la méthode store. On bénéficie de la méthode saveCategoriesAndTags que nous avons aussi créée précédemment.

Le contrôleur

Ce sont les méthodes edit et update du contrôleur Back/PostController qui sont concernées pour cette modification.

edit

On retrouve là de code déjà utilisé dans la méthode create, d’autant qu’on utilise la même vue :

public function edit(Post $post)
{
    $categories = Category::all()->pluck('title', 'id');

    return view('back.posts.form', compact('post', 'categories'));
}

On pourrait d’ailleurs refactoriser un peu le code mais on ne gagnerait pas forcément en clarté.

update

La méthode update est elle aussi très légère grâce au repository :

public function update(PostRequest $request, PostRepository $repository, Post $post)
{
    $repository->update($post, $request);

    return back()->with('ok', __('The post has been successfully updated'));
}

La vue

On a déjà prévu le cas de la modification dans la vue (back.posts.form) :

<form 
    method="post" 
    action="{{ Route::currentRouteName() === 'posts.edit' ? route('posts.update', $post->id) : route('posts.store') }}">

    @if(Route::currentRouteName() === 'posts.edit')
        @method('PUT')
    @endif

Comme on a déjà créé les routes, les titres et les données du menu tout devrait fonctionner :

On n’a pas d’item spécifique dans le menu alors on se contente de mettre en valeur Articles.

Si la modification se passe bien on a une alerte :

Supprimer un article

Dans le tableau des articles on dispose d’un bouton pour la suppression d’un article :

Chacun de ces boutons comporte dans l’attribut href l’url correspondant à l’article concerné.

Le contrôleur

Dans le contrôleur c’est la méthode destroy qui est concernée :

public function destroy(Post $post)
{
    $post->delete();

    return response()->json();
}

Comme on va faire cette suppression en Ajax on renvoie une réponse JSON.

Le Javascript

On doit ajouter du Javascript dans la vue back/shared/index.blade.php pour :

  • réagir au clic sur un bouton de suppression
  • demander confirmation de suppression
  • envoyer la requête à Laravel
  • supprimer la ligne du tableau en cas de retour positif de Laravel

On place le nouveau code à la fin de celui qu’on a déjà prévu :

  {{ $dataTable->scripts() }}

  <script>
    (() => {

        // Variables
        const headers = {
            'X-CSRF-TOKEN': '{{ csrf_token() }}', 
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
   
        // Delete 
        const deleteElement = async e => {              
            e.preventDefault();
            Swal.fire({
              title: e.target.dataset.name,
              icon: 'warning',
              showCancelButton: true,
              confirmButtonColor: '#DD6B55',
              confirmButtonText: '@lang('Yes')',
              cancelButtonText: '@lang('No')',
              preConfirm: () => {
                  return fetch(e.target.getAttribute('href'), { 
                      method: 'DELETE',
                      headers: headers
                  })
                  .then(response => {
                      if (response.ok) {
                          e.target.parentNode.parentNode.remove();
                      } else {
                        Swal.fire({
                            icon: 'error',
                            title: '@lang('Whoops!')',
                            text: '@lang('Something went wrong!')'
                        });  
                      }
                  });
              }
            });
        }

        // 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('table', 'click', deleteElement, "e.target.matches('.btn-danger')");
        });

    })()

  </script> 

@endsection

J’ai remis la fonction universelle wrapper, que j’ai déjà utilisé pour le frontend, pour la mise en place de l’écoute, de même que la constante pour les headers. On se passe de JQuery pour avoir du code plus propre.

Comme on a potentiellement (et sûrement) plusieurs boutons de suppression dans le tableau on écoute le click au niveau de ce tableau. On regarde ensuite si l’élément déclenchant possède la classe .btn-danger.

Si c’est le cas on utilise SweetAlert pour afficher un message :

Si on répond « Oui » alors on utilise fetch pour envoyer la requête. AU niveau de chaque bouton l’attribut href comporte l’url correcte qu’on récupère :

fetch(e.target.getAttribute('href'), {

Cette façon de procéder rend le code utilisable pour toutes les entités que nous aurons à traiter et pas seulement les articles.

Si la réponse est OK on supprime la ligne du tableau :

.then(response => {
    if (response.ok) {
        e.target.parentNode.parentNode.remove();

Sinon on affiche une erreur :

La sécurité

Comme je le disais dans l’introduction il ne faudrait pas qu’un rédacteur modifie ou supprime l’article d’un autre rédacteur. Bien que ce soit peu probable on doit prendre des dispositions pour l’éviter.

On crée une classe d’autorisation pour les articles :

php artisan make:policy PostPolicy --model=Post

Le fait d’utiliser l’option –model prépare les méthodes de la classe pour les actions viewAny, view, create, update, delete, restore et forceDelete. On n’a évidemment pas besoin de tout ça. Voilà le code adapté à nos besoins :

<?php

namespace App\Policies;

use App\Models\{ Post, User };
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    protected function manage(User $user, Post $post)
    {
        return $user->isAdmin() ?: $user->id === $post->user_id;
    }

    public function viewAny(User $user)
    {
        return true;
    }

    public function view(User $user, Post $post)
    {
        return $this->manage($user, $post);
    }

    public function create(User $user)
    {
        return true;
    }

    public function update(User $user, Post $post)
    {
        return $this->manage($user, $post);
    }

    public function delete(User $user, Post $post)
    {
        return $this->manage($user, $post);
    }
}

L’administrateur peut tout faire, mais pour les rédacteurs on les limite à leurs articles.

Il ne reste plus qu’à déclarer ça dans le contrôleur :

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }

Maintenant pour une action non autorisée on a cette page :

Conclusion

Nous en avons terminé avec la gestion des articles. Mais il nous reste encore plein de choses à voir dans l’administration : les commentaires, les contacts, les utilisateurs, les pages, les liens sociaux.

 

Print Friendly, PDF & Email

5 commentaires

Laisser un commentaire