Laravel 8

Créer un blog – le tableau des articles

Nous avons dans le précédent article installé notre interface d’administration avec comme choix AdminLTE. Pour se simplifier la vie on a prévu d’automatiser les titres et le menu latéral en ajoutant deux fichiers de configuration qu’on va lire pour générer les éléments correspondants. On a aussi prévu d’afficher sur le tableau de bord les nouveaux enregistrements : utilisateurs, articles, commentaires et contacts. De cette manière on a une vue globale de la vie du blog sur une même page.

Dans le présent article on va se pencher sur la gestion des articles. Pour le moment on sait juste afficher l’image et le résumé sur la page d’accueil du blog et l’article complet sur une page. Mais on doit pouvoir également créer et modifier les articles. De même il doit être possible de les supprimer et aussi les dupliquer.

Pour la gestion de toutes les entités du blog on va faire appel systématiquement à des tableaux. Comme je l’avais fait pour mon exemple de commerce en ligne je vais utiliser le package laravel-datatables qui me semble le plus performant en la matière.

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

Edit au 22/02/2021 : j’ai modifié la Datatable pour supprimer le bouton de duplication pour l’administrateur pour les articles dont il n’est pas l’auteur.

Laravel Datatables

Pour l’administration on va gérer tous les tableaux avec le package laravel-datatables, on va donc l’installer :

composer require yajra/laravel-datatables

Une fois le package installé on dispose d’une commande pour créer une datatable, on l’utilise pour les articles :

php artisan datatables:make Posts

Pour le moment on garde le code de base, on y reviendra plus loin.

Contrôleur et routes

On crée un contrôleur pour la gestion des articles :

php artisan make:controller Back\PostController --resource --model=Post

On aura besoin de toutes les méthodes sauf show puisque l’affichage d’un article se fait dans le frontend.

Dans le contrôleur on prévoit l’injection de la Datatable créée ci-dessus :

use App\DataTables\PostsDataTable;

...

public function index(PostsDataTable $dataTable)
{
    return $dataTable->render('back.shared.index');
}

On va créer la vue plus loin.

Pour les routes on peut aussi grouper :

use App\Http\Controllers\Back\{
    AdminController,
    PostController as BackPostController
};

...

Route::prefix('admin')->group(function () {

    Route::middleware('redac')->group(function () {
        ...
        Route::resource('posts', BackPostController::class)->except('show');
    });

    Route::middleware('admin')->group(function () {
        Route::name('posts.indexnew')->get('newposts', [BackPostController::class, 'index']);
    });
});

Je crée une route spéciale réservée à l’administrateur pour afficher seulement les nouveaux articles. On pointe sur la méthode index, il faudra prévoir de distinguer les deux situations : tous les articles ou juste les nouveaux, c’est le nom de la route utilisée qui nous renseignera.

Une vue partagée pour les tableaux

Le code pour l’affichage des tableaux avec Laravel Datatable va être quelque peu systématique. On ne crée donc qu’une vue qu’on adaptera si nécessaire pour les différences rencontrées :

Voici le code de base en prévoyant les textes en français :

@extends('back.layout')

@section('css')
  <link rel="stylesheet" href="https://cdn.datatables.net/1.10.23/css/dataTables.bootstrap4.min.css">
  <style>
    a > * { pointer-events: none; }
  </style>
@endsection

@section('main') 
  {{ $dataTable->table(['class' => 'table table-bordered table-hover table-sm'], true) }}
@endsection

@section('js') 
  <script src="https://cdn.datatables.net/1.10.23/js/jquery.dataTables.min.js"></script> 
  <script src="https://cdn.datatables.net/1.10.23/js/dataTables.bootstrap4.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>

  @if(config('app.locale') == 'fr')
    <script>
      (($, DataTable) => {
        $.extend(true, DataTable.defaults, {
          language: {
            "sEmptyTable":     "Aucune donnée disponible dans le tableau",
            "sInfo":           "Affichage des éléments _START_ à _END_ sur _TOTAL_ éléments",
            "sInfoEmpty":      "Affichage de l'élément 0 à 0 sur 0 élément",
            "sInfoFiltered":   "(filtré à partir de _MAX_ éléments au total)",
            "sInfoPostFix":    "",
            "sInfoThousands":  ",",
            "sLengthMenu":     "Afficher _MENU_ éléments",
            "sLoadingRecords": "Chargement...",
            "sProcessing":     "Traitement...",
            "sSearch":         "Rechercher :",
            "sZeroRecords":    "Aucun élément correspondant trouvé",
            "oPaginate": {
              "sFirst":    "Premier",
              "sLast":     "Dernier",
              "sNext":     "Suivant",
              "sPrevious": "Précédent"
            },
            "oAria": {
              "sSortAscending":  ": activer pour trier la colonne par ordre croissant",
              "sSortDescending": ": activer pour trier la colonne par ordre décroissant"
            },
            "select": {
              "rows": {
                "_": "%d lignes sélectionnées",
                "0": "Aucune ligne sélectionnée",
                "1": "1 ligne sélectionnée"
              }  
            }
          }
        });
      })(jQuery, jQuery.fn.dataTable);
    </script>
  @endif

  {{ $dataTable->scripts() }}

@endsection

Comme on n’a pas encore créé de section CSS et pour le javascript dans back.layout on va le faire. On en profite pour mettre à jour le titre de la page :

<title>@lang('Administration')</title>

...

<!-- Theme style -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.5/css/adminlte.min.css" />
@yield('css')

...

<!-- AdminLTE App -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.5/js/adminlte.min.js"></script>
@yield('js')
</body>

Le tableau des articles

Revenons-en maintenant à notre Datatable.

Les données

Quelles sont les données qu’on va afficher dans le tableau ?

  • le titre
  • l’auteur
  • les catégories
  • le nombre de commentaires
  • la date de publication ou de dernière modification
  • le statut : publié ou pas
  • des boutons d’action :
    • voir l’article
    • le modifier
    • le cloner
    • le supprimer

Dans le Datatable c’est la méthode query qui s’occupe de récupérer les données. Par défaut on a juste une amorce de requête :

public function query(Post $model)
{
    return $model->newQuery();
}

On complète pour aller chercher toutes les données nécessaires :

use Illuminate\Support\Facades\Route;

...

public function query(Post $post)
{
    $query = isRole('redac') ? auth()->user()->posts() : $post->newQuery();

    if(Route::currentRouteNamed('posts.indexnew')) {
        $query->has('unreadNotifications');
    }

    return $query->select(
                    'posts.id',
                    'slug',
                    'title',
                    'active',
                    'posts.created_at',
                    'posts.updated_at',
                    'user_id')
                ->with(
                    'user:id,name',
                    'categories:title')
                ->withCount('comments');
}

On doit tenir compte du fait que :

  • les rédacteurs ne doivent voir que leurs articles
  • si on a la route posts.indexnew on doit avoir seulement les nouveaux articles
  • on doit éviter de charger trop de données en utilisant un SELECT (en particulier on évite de charger le corps des articles)
  • on doit charger les relations
  • on doit compter les commentaires

Le colonnes

On va un peu épurer la méthode html :

public function html()
{
    return $this->builder()
                ->setTableId('posts-table')
                ->columns($this->getColumns())
                ->minifiedAjax()
                ->dom('Blfrtip')
                ->lengthMenu();
}

Les colonnes sont définies dans la méthode getColumns, on ajoute les colonnes en fonction des données qu’on a définies ci-dessus :

protected function getColumns()
{
    $columns = [
        Column::make('title')->title(__('Title'))
    ];
    
    if(auth()->user()->role === 'admin') {
        array_push($columns, 
            Column::make('user.name')->title(__('Author'))
        );
    }

    array_push($columns,
        Column::computed('categories')->title(__('Categories')),
        Column::computed('comments_count')->title(__('Comments'))->addClass('text-center align-middle'),
        Column::make('created_at')->title(__('Date')),
        Column::computed('action')->title(__('Action'))->addClass('align-middle text-center')
    );

    return $columns;
}

On n’ajoute la colonne de l’auteur que si c’est l’administrateur, sinon ce n’est pas la peine.

On dispose de la méthode computed pour une colonne calculée. C’est le cas par exemple des catégories parce qu’on va mettre toutes les catégories auxquelles apartient l’article.

Un trait

Comme on aura à afficher des badges et boutons dans plusieurs tableaux on crée un trait :

<?php

namespace App\DataTables;

trait DataTableTrait
{
    public function badge($text, $type, $margin = 0)
    {
        return '<span class="badge badge-' . $type . ' ml-' . $margin . '">' . __($text) . '</span>';
    }

    public function button($route, $param, $type, $title, $icon, $name = '', $target = '_self')
    {
        return '<a 
                    title="'. $title . '" 
                    data-name="' . $name . '" 
                    href="' . route($route, $param) . '" 
                    class="px-3 btn btn-xs btn-' . $type . '" 
                    target="' . $target . '">
                    <i class="far fa-' . $icon . '"></i>
                </a>';
    }
}

Et on l’ajoute dans le Datatable :

class PostsDataTable extends DataTable
{
    use DataTableTrait;

Un helper

On a besoin de mettre en forme l’heure, on l’avait déjà fait pour la date dans le fichier app/helpers, on ajoute la fonction pour l’heure :

if (!function_exists('formatHour')) {
    function formatHour($date)
    {
        return ucfirst(utf8_encode ($date->formatLocalized('%Hh%M')));
    }
}

On va d’ailleurs fixer la locale dans AppServiceProvider :

public function boot()
{
    setlocale(LC_TIME, config('app.locale'));
    ...

La génération

La génération du Datatable se fait dans la méthode dataTable :

public function dataTable($query)
{
    return datatables()
        ->eloquent($query)
        ->editColumn('categories', function ($post) {
            return $this->getCategories($post);
        })
        ->editColumn('created_at', function ($post) {
            return $this->getDate($post);
        })
        ->editColumn('comments_count', function ($post) {
            return $this->badge($post->comments_count, 'secondary');
        })
        ->editColumn('action', function ($post) {

            $buttons = $this->button(
                            'posts.display', 
                            $post->slug, 
                            'success', 
                            __('Show'), 
                            'eye', 
                            '',
                            '_blank'
                        );

            if(Route::currentRouteName() === 'posts.indexnew') {
                return $buttons;
            }

            $buttons .= $this->button(
                'posts.edit', 
                $post->id, 
                'warning', 
                __('Edit'), 
                'edit'
            );

            if($post->user_id === auth()->id()) {
                $buttons .= $this->button(
                    'posts.create', 
                    $post->id, 
                    'info', 
                    __('Clone'), 
                    'clone'
                );
            }
            
            return $buttons . $this->button(
                        'posts.destroy', 
                        $post->id, 
                        'danger', 
                        __('Delete'), 
                        'trash-alt', 
                        __('Really delete this post?')
                    );
        })
        ->rawColumns(['categories', 'comments_count', 'action', 'created_at']);
}

Le traitement des publications, dates et catégories se fait dans deux fonctions distinctes :

protected function getDate($post)
{
    if(!$post->active) {
        return $this->badge('Not published', 'warning');
    }

    $updated = $post->updated_at > $post->created_at;
    $html = $this->badge($updated ? 'Last update' : 'Published', 'success');

    $html .= '<br>' . formatDate($updated ? $post->updated_at : $post->created_at) . __(' at ') . formatHour($updated ? $post->updated_at : $post->created_at);

    return $html;
}

protected function getCategories($post)
{
    $html = '';

    foreach($post->categories as $category) {
        $html .= $category->title . '<br>';
    }

    return $html;
}

Pour la date je regarde s’il y a eu une modification après la création et j’affiche cette date là.

Maintenant avec l’url monblog.ext/admin/posts (et newposts pour uniquement les nouveaux) on obtient le tableau :

Pour le moment au niveau des boutons on n’a que le premier qui fonctionne pour voir l’article.

Menu et titres

Pour les titres on complète le fichier app/config/titles.php :

<?php

return [

    'admin' => 'Dashboard',
    'posts' => [
        'index'    => 'Posts',
        'create'   => 'Post Creation',
        'edit'     => 'Post Edit',
        'indexnew' => 'New Posts',
    ],
];

Pour le menu latéral ça se passe dans app/config/menu.php :

<?php

return [

    'Dashboard' => [
        'role'   => 'redac',
        'route'  => 'admin',
        'icon'   => 'tachometer-alt',
    ],
    'Posts' => [
        'icon' => 'file-alt',
        'role'   => 'redac',
        'children' => [
            [
                'name'  => 'All posts',
                'role'  => 'redac',
                'route' => 'posts.index',
            ],
            [
                'name'  => 'New posts',
                'role'  => 'admin',
                'route' => 'posts.indexnew',
            ],
            [
                'name'  => 'Add',
                'role'  => 'redac',
                'route' => 'posts.create',
            ],
            [
                'name'  => 'fake',
                'role'  => 'redac',
                'route' => 'posts.edit',
            ],
        ],
    ],
];

Si tout se passe bien vous devez avoir menu et titre pour les articles :

Pour les nouveaux articles :

 

Conclusion

On a obtenu l’affichage des articles sous forme de tableau dans l’administration avec les renseignements essentiels. Le code de base nous servira aussi pour les autres entités. Mais on a encore du travail avec les articles : création, modification, duplication, suppression. On continuera dans le prochain article.

Print Friendly, PDF & Email

6 commentaires

Laisser un commentaire