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

2 commentaires

    • 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