Laravel 8

Créer un blog – l’administration

Nous en avons dans le précédent article terminé avec le frontend de notre blog. Il est possible qu’on y revienne pour ajouter des fonctionnalités mais pour le moment il comporte tout ce qu’on avait prévu au départ.

Nous allons à présent ouvrir un nouveau chapitre avec la création de l’administration du blog. On va avoir pas mal de choses à gérer : les utilisateurs, les catégories et articles, les commentaires, les contacts, les pages et les liens sociaux, sans parler des notifications. On a donc du pain sur la planche.

Dans un premier temps il faut faire un choix technologique. Comme je l’aime bien j’ai opté pour AdminLTE, comme je l’avais déjà fait pour mon exemple de commerce en ligne. On va d’ailleurs suivre le même processus.

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

AdminLTE

On trouve AdminLTE sur ce site :

On va le télécharger avec le bouton DOWNLOAD. On se retrouve avec un fichier compressé et après décompression on obtient tout ça :

Au moment où j’écris cet article la dernière version est la 3.1.0-RC.

Plutôt que d’utiliser les assets proposés on va plutôt charger les librairies avec des CDN, ce qui améliorera les performances (les serveurs sont performants et on travaille en parallèle).

On va par contre utiliser la page de démarrage starter.html.

On prend le code de la page starter.html, changer son nom pour layout.blade.php et ranger le fichier dans un dossier back des vues :

On crée une route provisoire pour accéder au layout :

Route::view('admin', 'back.layout');

Pour le moment on a aucun style parce qu’on n’a pas les assets alors on va arranger ça avec des CDN :

<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" />
<!-- Theme style -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.5/css/adminlte.min.css" />

...

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/js/bootstrap.bundle.min.js" ></script>
<!-- AdminLTE App -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.5/js/adminlte.min.js"></script>

Maintenant on a quelque chose de correct mis à part les images, mais ça on en aura pas besoin :

On fait un grand ménage maintenant :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>AdminLTE 3 | Starter</title>

  <!-- Google Font: Source Sans Pro -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  <!-- Font Awesome Icons -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" />
  <!-- Theme style -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.5/css/adminlte.min.css" />
</head>
<body class="hold-transition sidebar-mini">
<div class="wrapper">

  <!-- Navbar -->
  <nav class="main-header navbar navbar-expand navbar-white navbar-light">
    <!-- Left navbar links -->
    <ul class="navbar-nav">      
      <li class="nav-item">
        <a class="nav-link" data-widget="pushmenu" href="#"><i class="fas fa-bars"></i></a>
      </li>
      <li class="nav-item d-none d-sm-inline-block">
        <a href="{{ route('home') }}" class="nav-link">@lang('Show blog')</a>
      </li>
      <li class="nav-item d-none d-sm-inline-block">
        <form action="{{ route('logout') }}" method="POST" hidden>
          @csrf                                
        </form>
        <a class="nav-link"
            href="{{ route('logout') }}"
            onclick="event.preventDefault(); this.previousElementSibling.submit();">
            @lang('Logout')
        </a>
      </li>
    </ul>

  </nav>
  <!-- /.navbar -->

  <!-- Main Sidebar Container -->
  <aside class="main-sidebar sidebar-dark-primary elevation-4">
    <!-- Sidebar -->
    <div class="sidebar">

      <!-- Sidebar Menu -->
      <nav class="mt-2">
        <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">

        </ul>
      </nav>
      <!-- /.sidebar-menu -->
    </div>
    <!-- /.sidebar -->
  </aside>

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">
    <!-- Content Header (Page header) -->
    <!-- Content Header (Page header) -->
    <div class="content-header">
      <div class="container-fluid">
        <div class="row mb-2">
          <div class="col-sm-12">
            <h1 class="m-0 text-dark">Titre</h1>
          </div><!-- /.col -->
        </div><!-- /.row -->
      </div><!-- /.container-fluid -->
    </div>
    <!-- /.content-header -->

    <!-- Main content -->
    <div class="content">
      <div class="container-fluid">
        @yield('main')
      </div><!-- /.container-fluid -->
    </div>
    <!-- /.content -->
  </div>
  <!-- /.content-wrapper -->

  <!-- Main Footer -->
  <footer class="main-footer">
    <!-- Default to the left -->
    <strong>Copyright &copy; 2021 {{ config('app.name', 'Laravel') }}.</strong>
  </footer>
</div>
<!-- ./wrapper -->

<!-- REQUIRED SCRIPTS -->

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/js/bootstrap.bundle.min.js" ></script>
<!-- AdminLTE App -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.5/js/adminlte.min.js"></script>
</body>
</html>

Maintenant qu’on a fait du vide il ne nous reste plus qu’à remplir…

On va quand même régler un petit détail avec le nom de l’application dans le fichier .env :

APP_NAME="Mon Blog"

Un composant pour le menu latéral

Comme le code va être répétitif on crée un composant anonyme pour les items du menu latéral :

@props(['route', 'sub', 'icon'])

<li class="nav-item">
  <a href="{{ route($route) }}" class="nav-link {{ currentRouteActive($route) }}">
    <i class="

      @isset($sub) 
        far fa-circle 
      @endisset

      nav-icon  

      @isset($icon) 
        fas fa-{{ $icon }} 
      @endisset
      
    "></i>
    <p>{{ $slot }}</p>
  </a>
</li>

Il suffit de lui envoyer ces informations :

  • le nom de la route
  • si c’est un sous-menu
  • s’il y a une icône

Des helpers

Pour simplifier le codage on va aussi créer des helpers. Vous avez peut-être déjà remarqué qu’il y en a un dans le composant ci-dessus.

On ajoute ces 4 helpers dans le fichier app/helpers.php :

...

if (!function_exists('currentRouteActive')) {
  function currentRouteActive(...$routes)
  {
      foreach ($routes as $route) {
          if(Route::currentRouteNamed($route)) return 'active';
      }
  }
}

if (!function_exists('currentChildActive')) {
  function currentChildActive($children)
  {
      foreach ($children as $child) {
          if(Route::currentRouteNamed($child['route'])) return 'active';
      }
  }
}

if (!function_exists('menuOpen')) {
  function menuOpen($children)
  {
      foreach ($children as $child) {
          if(Route::currentRouteNamed($child['route'])) return 'menu-open';
      }
  }
}

if (!function_exists('isRole')) {
  function isRole($role)
  {
      return auth()->user()->role === $role;
  }
}

Le but est d’ajouter les classes nécessaires pour l’apparence et de connaître facilement le rôle d’un utilisateur;

Des middlewares

On va avoir besoin plusieurs fois de savoir si l’utilisateur authentifié est administrateur ou rédacteur. Le plus simple est de créer deux middlewares :

php artisan make:middleware Admin
php artisan make:middleware Redac

class Admin
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();

        if ($user && $user->role === 'admin') {
            return $next($request);
        }

        return redirect()->route('home');
    }
}
class Redac
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();

        if ($user && ($user->role === 'admin' || $user->role === 'redac')) {
            return $next($request);
        }

        return redirect()->route('home');
    }
}

Pour le middleware Redac on admet qu’un administrateur est aussi rédacteur. On enregistre ces middlewares dans app/Http/Kernel.php :

protected $routeMiddleware = [
    ...
    'admin' => \App\Http\Middleware\Admin::class,
    'redac' => \App\Http\Middleware\Redac::class,
];

Contrôleur et route

On crée un contrôleur pour entrer dans l’administration :

php artisan make:controller Back\AdminController

On renvoie une vue avec la méthode index :

<?php

namespace App\Http\Controllers\Back;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class AdminController extends Controller
{
    public function index()
    {
        return view('back.index');
    }
}

On ajoute la route pour appeler cette méthode :

use App\Http\Controllers\Back\AdminController;

...

Route::prefix('admin')->group(function () {
    Route::middleware('redac')->group(function () {
        Route::name('admin')->get('/', [AdminController::class, 'index']);
    });
});

Toute les routes de l’administration seront préfixées admin. On protège l’accès avec le middleware redac qu’on a créé plus haut. L’administration ne doit être accessible que pour l’administrateur et les rédacteurs. J’ai créé des groupes parce qu’on aura de nombreuses routes concernées.

La vue index

On crée la vue back.index :

@extends('back.layout') 

@section('main') 
  <div class="container-fluid">
      <div class="row">

        C'est ici qu'on va mettre du contenu.

      </div>      
  </div>
@endsection

Maintenant si on se connecte avec l’administrateur ou un rédacteur on arrive dans le tableau de bord avec l’url monblog.ext/admin :

Accès à l’administration

Pour accéder à cette administration le plus simple est de prévoir un lien dans la barre de navigation pour les utilisateurs concernés, c’est à dire l’administrateur et les rédacteurs. On complète donc cette barre dans la vue front.layout :

@guest
    ...
@else
    @if(auth()->user()->role != 'user')
        <li>
            <a href="{{ url('admin') }}">@lang('Administration')</a>
        </li>
    @endif

Les titres

Pour les titres on va créer un fichier de configuration qui va faire correspondre une route avec un titre :

Pour le moment on n’a pas grand chose à y mettre :

<?php

return [
    'admin' => 'Dashboard',
];

Dans AppServiceProvider on envoie les titres avec un composeur :

use Illuminate\Support\Facades\{ Blade, View, Route };

...

public function boot()
{
    ...

    View::composer('back.layout', function ($view) {
        $title = config('titles.' . Route::currentRouteName());
        $view->with(compact('title'));
    });

Dans back.layout on peut maintenant afficher le titre :

<h1 class="m-0 text-dark">@lang($title)</h1>

Le menu latéral

On va s’intéresser maintenant au menu latéral. On a déjà créé un composant ci-dessus pour afficher chaque item. Pour bien organiser ce menu on va, comme pour les titres, créer un fichier de configuration :

On y prévoit le titre, le rôle, la route et l’icône :

<?php

return [

    'Dashboard' => [
        'role'   => 'redac',
        'route'  => 'admin',
        'icon'   => 'tachometer-alt',
    ],
];

Dans la vue back.layout on lit ce fichier pour afficher le menu :

<!-- Sidebar Menu -->
<nav class="mt-2">
  <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">

    @foreach(config('menu') as $name => $elements)
        @if($elements['role'] === 'redac' || auth()->user()->isAdmin())
            @isset($elements['children'])
                <li class="nav-item has-treeview {{ menuOpen($elements['children']) }}">
                    <a href="#" class="nav-link {{ currentChildActive($elements['children']) }}">
                        <i class="nav-icon fas fa-{{ $elements['icon'] }}"></i>
                        <p>
                            @lang($name)
                            <i class="right fas fa-angle-left"></i>
                        </p>
                    </a>
                    <ul class="nav nav-treeview">
                        @foreach($elements['children'] as $child)
                            @if(($child['role'] === 'redac' || auth()->user()->isAdmin()) && $child['name'] !== 'fake')
                                <x-back.menu-item 
                                    :route="$child['route']" 
                                    :sub=true>
                                    @lang($child['name'])
                                </x-back.menu-item>
                            @endif
                        @endforeach
                    </ul>
                </li>
            @else
                <x-back.menu-item 
                    :route="$elements['route']" 
                    :icon="$elements['icon']">
                    @lang($name)
                </x-back.menu-item>
            @endisset
        @endif
    @endforeach

  </ul>
</nav>

C’est équipé aussi pour les sous-menus. Comme ça pour la suite on aura juste à renseigner le fichier de configuration. Pour le moment on a juste l’item pour le tableau de bord :

Le tableau de bord

Que va-t-on mettre sur le tableau de bord ? On va afficher un bloc pour chacun des nouveaux éléments :

  • nouvel utilisateur
  • nouvel article
  • nouveau commentaire
  • nouveau contact

Il faudra tenir compte du fait que les rédacteurs ne sont concernés que par les nouveaux commentaires de leurs articles.

Un composant box

On crée un composant box qui sera utilisé pour chacune des catégories précitées :

@props([
  'type',
  'number',
  'title',
  'route',
  'model',
])

<div class="col-lg-3 col-6">
  <div class="small-box bg-{{ $type }}">
    <div class="inner">
      <h3>{{ $number }}</h3>
      <p>@lang($title)</p>
    </div>
    <div class="icon">
      <i class="ion ion-bag"></i>
    </div>
    <a href="#" class="small-box-footer">@lang('More info') <i class="fas fa-arrow-circle-right"></i></a>
    <form action="{{ route('purge', $model) }}" method="POST">
      @csrf
      @method('PUT')
      <button type="submit" class="btn btn-{{ $type }} btn-block text-warning">@lang('Purge')</button>
    </form>
  </div>
</div>

Il faut lui envoyer les information requises et il affiche gentiment le bloc.

Le contrôleur est les routes

On doit compléter le contrôleur AdminController pour :

  • envoyer les informations pour afficher les blocs nécessaires
  • procéder à la demande la purge des notifications lues

Voici le nouveau code complet pour le contrôleur :

<?php

namespace App\Http\Controllers\Back;

use App\Http\Controllers\Controller;
use App\Models\{ User, Post, Comment, Contact };
use Illuminate\Support\Facades\DB;

class AdminController extends Controller
{
    public function index(Post $post, User $user, Comment $comment, Contact $contact)
    {
        $users = isRole('admin') ? $this->getUnreads($user) : null;
        $contacts = isRole('admin') ? $this->getUnreads($contact) : null;
        $posts = isRole('admin') ? $this->getUnreads($post) : null;

        $comments = $this->getUnreads($comment, isRole('redac'));

        return view('back.index', compact('posts', 'users', 'contacts', 'comments'));
    }

    protected function getUnreads($model, $redac = null)
    {
        $query = $redac ? 
            $model->whereHas('post.user', function ($query) {
                $query->where('users.id', auth()->id());
            }) :
            $model->newQuery();

        return $query->has('unreadNotifications')->count();
    }

    public function purge($model)
    {
        $model = 'App\Models\\' . ucfirst($model);

        DB::table('notifications')->where('notifiable_type', $model)->delete();

        return back();
    }
}

On doit ajouter la route de la purge :

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

        Route::name('purge')->put('purge/{model}', [AdminController::class, 'purge']);
    });
});

La vue index

Il ne reste plus qu’à utiliser le composant dans la vue back.index :

@extends('back.layout') 

@section('main') 
  <div class="container-fluid">
      <div class="row">

        @if($posts)
          <x-back.box
            type='info'
            :number='$posts'
            title='New posts'
            route='posts.indexnew'
            model='post'>
          </x-back.box>
        @endif

        @if($users)
          <x-back.box
            type='success'
            :number='$users'
            title='New users'
            route='users.indexnew'
            model='user'>
          </x-back.box>
        @endif

        @if($contacts)
          <x-back.box
            type='primary'
            :number='$contacts'
            title='New contacts'
            route='contacts.indexnew'
            model='contact'>
          </x-back.box>
        @endif

        @if($comments)
          <x-back.box
            type='danger'
            :number='$comments'
            title='New comments'
            route='comments.indexnew'
            model='comment'>
          </x-back.box>
        @endif 

      </div>      
  </div>
@endsection

On envoie des routes qu’on a pas encore créées mais on règlera ça plus tard.

On a l’affichage d’un bloc pour chaque entité s’il y en a au moins une nouvelle. Je n’ai pas poussé le codage jusqu’à adapter l’orthographe du pluriel si on a un seul élément.

J’ai un peu détourné le système de notification de Laravel pour mettre ça en place mais pourquoi pas ?

Conclusion

Ca y est, notre administration est en place, on va pouvoir à présent traiter toutes les données du blog. On commencera ça dans le prochain article.

Print Friendly, PDF & Email

24 commentaires

  • zitounisd

    Merci encore une fois pour ce tutorial , à la fin j’ai eu l’erreur suivantes :
    BadMethodCallException
    Call to undefined method App\Models\Contact::unreadNotifications()

    est ce que je dois avoir une colonne unread dans la table notifications

  • Ferfalam

    Salut BESTMOMO, dis à ce niveau du tutoriel est ce que tu as essayé de créer un nouvel utilisateur ? Car moi j’ai une erreur à la création d’un utilisateur

    Error
    Class ‘App\Models\ModelCreated’ not found

  • Thibaut

    bjr
    j’ai encore un soucis, j’ai avancé jusqu’ici mais j’ai a nveau une error :ErrorException
    Undefined variable: title (View: C:\laragon\www\blog-tutoriel\resources\views\components\back\box.blade.php)
    Toutes ces variables:@props([‘type’,’number’,’title’,’route’,’model’,])ne sont pas reconnues ,sauf la variable route
    pourtant je suis exactement le tutos

          • Thibaut

            bjr
            ma version de php est 7.4.9 j’ai pas puis installer le version 8 sur laragon, j’ai pourtant suivi tout les tutos qui traite la question, ils disent la mm chose mais lorsque je veux le faire j’ai apache qui ne demarre pas

          • bestmomo

            Pas encore vraiment besoin d’utiliser PHP 8, mais si vraiment tu la veux il doit aussi falloir modifier un peu la configuration d’Apache.

            Edit : tu as toutes les explications avec une vidéo bien faite ici. Je viens d’essayer et PHP 8 fonctionne bien. Par contre je reçois des erreurs PHP sur mes projets, il doit falloir régénérer les librairies.

          • Thibaut

            j’ai finalement compris d’où venait le probleme, le soucis c’est que moi j’ai pris l’habitude de creer mes components via la ligne de commande php artisan make:components  »le nom » et lorsque je fais ça un dossier view/compenent est creer avec une class portant le nom de mon component, or je devez aussi définir les données requises du composant dans son constructeur de classe, mais je ne l’avais pas fait du tout , du coup il ne voyez pas les variables dans mon component. alors pour ne pas avoir de probleme j’ai tout simplement tout supprimé et recreer mes component manuellement et tout marche de nouveau.
            j’aurai puis declarer les variables dans les class de mes component mais la je voulais plus de complication

          • bestmomo

            Ah d’accord !
            D’ailleurs pour un composant qui nécessite un traitement complémentaire c’est utile de créer une classe, sinon je n’en vois pas l’intérêt.

          • Thibaut

            par rapport au passage a php8 sur laragon, j’ai de nveau suivi les instruction du lien que tu ma donne et jai cette cette error:service Apache can not start, Reason Unknown, Please check Apache log file; et lorque je regarde le fichier log j’ai cett erreur:
            httpd: Syntax error on line 546 of C:/laragon/bin/apache/httpd-2.4.35-win64-VC15/conf/httpd.conf: Syntax error on line 2 of C:/laragon/etc/apache2/mod_php.conf: Can’t locate API module structure `php8_module’ in file C:/laragon/bin/php/php-8.0.3-Win32-vs16-x64/php8apache2_4.dll: No error
            [Sat Mar 27 16:25:38.029378 2021] [core:warn] [pid 864:tid 568] AH00098: pid file C:/laragon/bin/apache/httpd-2.4.35-win64-VC15/logs/httpd.pid overwritten — Unclean shutdown of previous Apache run?
            PHP Warning: ‘C:\\WINDOWS\\SYSTEM32\\VCRUNTIME140.dll’ 14.15 is not compatible with this PHP build linked with 14.28 in Unknown on line 0
            [Sat Mar 27 16:25:38.556782 2021] [:emerg] [pid 864:tid 568] AH00020: Configuration Failed, exiting
            [Sat Mar 27 16:25:47.054210 2021] [core:warn] [pid 17780:tid 548] AH00098: pid file C:/laragon/bin/apache/httpd-2.4.35-win64-VC15/logs/httpd.pid overwritten — Unclean shutdown of previous Apache run?
            PHP Warning: ‘C:\\WINDOWS\\SYSTEM32\\VCRUNTIME140.dll’ 14.15 is not compatible with this PHP build linked with 14.28 in Unknown on line 0
            [Sat Mar 27 16:25:47.138180 2021] [:emerg] [pid 17780:tid 548] AH00020: Configuration Failed, exiting

          • bestmomo

            Il faudrait peut-être prendre une version d’Apache plus récente, moi j’ai la 2.4.46 et ça passe (Laragon installe par défaut la 2.4.35).

  • webwatson

    Merci pour ce tuto!

    Puis-je avoir une explication sur cette methode et ce paramètre ((…$routes)

    if (!function_exists('currentRouteActive')) {
    function currentRouteActive(...$routes)
    {
    foreach ($routes as $route) {
    if(Route::currentRouteNamed($route)) return 'active';
    }
    }
    }

  • bensa

    Bonjour,

    svp pouvez vous m’expliquer le fonctionnement de ce code dans l’index de AdminController:
    $query = $redac ?
    $model->whereHas('post.user', function ($query) {
    $query->where('users.id', auth()->id());

    j’ai pas bien compris d’ou vient ‘post.user’ et ‘users.id’

    merci beaucoup

    • bestmomo

      Salut,

      Pour les rédacteurs les seules notifications concernent les commentaires, dans ce cas on sait donc que le modèle est Comment. Dans ce modèle on a une relation avec Post et dans Post on a une relation avec User. Donc ici on demande si le commentaire en question appartient à un article (post) qui lui-même appartient à un auteur (user) qui est celui qui est actuellement authentifié (auth()->id()). De cette manière on n’affiche que les nouveaux commentaires qui concernent les articles de l’auteur connecté.

      Pour users.id on fait juste référence à la colonne id de la table users, pour éviter une ambiguité dans la requête parce que toutes les tables ont une colonne id.

    • bestmomo

      Salut,

      C’est finalement très personnel les repositories. Je commence à en créer quand je vois qu’un contrôleur risque d’être un peu trop encombré de code pour récupérer des données. Par exemple pour les articles c’était tout à fait judicieux, comme on l’a vu. Ici ça ne l’est pas, surtout qu’on va chercher des informations dans 5 tables différentes. Mais bon, c’est juste ma vision des choses.

      Bonne journée !

Laisser un commentaire