Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Voir cette série
Mon CMS - Les menus
Mardi 27 août 2024 15:56

Notre projet de développement d'un CMS (Content Management System) a considérablement progressé, particulièrement sur le plan du frontend. Jusqu'à présent, nous avons réussi à mettre en place plusieurs fonctionnalités essentielles :

    Une page d'accueil attrayante et fonctionnelle
    Un système d'authentification robuste
    Un affichage paginé des résumés d'articles
    Une fonction de recherche d'articles performante
    Un mode sombre pour améliorer l'expérience utilisateur

Cependant, pour rendre notre CMS véritablement complet et flexible, il nous manque encore un élément crucial : un système de menus dynamiques. Cette fonctionnalité permettra aux administrateurs de créer et de gérer facilement la structure de navigation du site, offrant ainsi une meilleure expérience utilisateur et une plus grande adaptabilité du contenu. Dans cet article, nous allons nous concentrer sur la conception et l'implémentation de ce système de menus. Nous explorerons les étapes suivantes :

    Conception de la structure de données pour les menus
    Développement du composant frontend pour l'affichage des menus
    Intégration des menus dans nos barres de navigation
    Création d'un pied de page avec un menu dédié

À la fin de cet article, notre CMS sera doté d'un système de navigation flexible et puissant, franchissant ainsi une étape importante vers un outil de gestion de contenu complet et professionnel. Il ne nous manquera plus que la partie administration que nous aborderons plus tard.

Les données

Pour répondre aux besoins variés de navigation d'un site web moderne, nous allons mettre en place un système de menus à deux niveaux. Cette approche nous permettra de créer une structure de navigation à la fois flexible et intuitive.

Structure des menus

    Menu principal : Situé dans la barre de navigation supérieure (mais aussi dans la barre latérale), il servira de point d'entrée principal pour la navigation sur le site.
    Sous-menus : Associés aux éléments du menu principal, ils offriront une navigation plus détaillée et spécifique.
    Menu secondaire (footer) : Placé dans le pied de page, il contiendra principalement des liens vers des pages importantes, mais moins fréquemment consultées.

Organisation des données

Pour implémenter cette structure, nous allons créer 3 tables dans notre base de données :

    Table menus :
        Stockera les éléments du menu principal
        Champs : id, label, link (nullable), order
    Table submenus :
        Contiendra les sous-menus liés aux éléments du menu principal
        Champs : id, menu_id (clé étrangère), label, link, order
    Table footers :
        Stockera les éléments du menu du pied de page
        Champs : id, label, link, order

Cette organisation nous permettra de :

    Gérer facilement la hiérarchie des menus
    Offrir une flexibilité pour l'ajout, la modification et la suppression d'éléments de menu
    Séparer clairement les menus principaux des sous-menus pour une meilleure maintenance

Particularités

    Le menu principal supportera jusqu'à deux niveaux de profondeur (menu principal + sous-menus).
    Le menu footer sera limité à un seul niveau pour maintenir une structure simple et claire.

Avantages de cette approche

    Flexibilité : Adaptable à diverses structures de site web.
    Scalabilité : Facile à étendre si besoin (par exemple, pour ajouter plus de niveaux dans le futur).
    Performance : Structure simple permettant des requêtes efficaces.
    Facilité de gestion : Interface d'administration intuitive pour les non-techniciens.

Migrations

Table menus

On crée la migration et le modèle :

php artisan make:model Menu --migration

Pour la migration :

public function up(): void
{
    Schema::create('menus', function (Blueprint $table) {
        $table->id();
        $table->string('label');
        $table->string('link')->nullable();
        $table->integer('order');
    });
}

Le champ link est nullable parce qu'on a deux cas : on a directement un lien ou on a des sous-menus (dans ce cas, on prévoit null).

On se passe du timestamp. Pour le modèle, on prévoit aussi la propriété $fillable. On va aussi en profiter pour ajouter la relation avec les sous-menus :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Menu extends Model
{
    public $timestamps = false;

	protected $fillable = [
		'label',
		'link',
		'order',
	];

	public function submenus(): HasMany
	{
		return $this->hasMany(Submenu::class);
	}
}

Table submenus

On crée aussi la migration et le modèle :

php artisan make:model Submenu --migration

Pour la migration :

public function up(): void
{
    Schema::create('submenus', function (Blueprint $table) {
        $table->id();
        $table->string('label');
        $table->integer('order');
        $table->string('link')->default('#');
        $table->foreignId('menu_id')->constrained()->onDelete('cascade');
    });
}

On se passe aussi du timestamp. On a la clé étrangère pour faire le lien avec la table menus.

Pour le modèle, on prévoit aussi la propriété $fillable :

class Submenu extends Model
{
	public $timestamps = false;

	protected $fillable = [
		'label',
		'link',
		'order',
	];
}

Table footers

On crée la migration et le modèle :

php artisan make:model Footer --migration

Pour la migration :

public function up(): void
{
	Schema::create('footers', function (Blueprint $table) {
		$table->id();
		$table->string('label');
		$table->string('link');
		$table->integer('order');
	});
}

On a exactement comme la table menus, à part que le champ link n'est pas nullable parce qu'on n'a pas de sous-menus à ce niveau.

Pour le modèle :

class Footer extends Model
{
	public $timestamps = false;

	protected $fillable = [
		'label',
		'link',
		'order',
	];
}

Population

Pour que notre CMS fonctionne, on prévoit de remplir ces tables. On ajoute ces deux seeders :

Le code pour le seeder des menus et sous-menus :

<?php

namespace database\seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class MenusSeeder extends Seeder
{
	public function run()
	{
		$menus = [
			['label' => 'Catégorie 1', 'link' => null, 'order' => 3],
			['label' => 'Catégorie 2', 'link' => '/category/category-2', 'order' => 2],
			['label' => 'Catégorie 3', 'link' => '/category/category-3', 'order' => 1],
		];

		DB::table('menus')->insert($menus);

		$submenus = [
			['label' => 'Post 1', 'order' => 1, 'link' => '/posts/post-1', 'menu_id' => 1],
			['label' => 'Tout', 'order' => 3, 'link' => '/category/category-1', 'menu_id' => 1],
		];

		DB::table('submenus')->insert($submenus);
	}
}

Et celui pour les footers :

<?php

namespace database\seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class FooterSeeder extends Seeder
{
	public function run()
	{
		// Données des éléments du pied de page
		$footers = [
			['label' => 'Accueil', 'order' => 1, 'link' => '/'],
			['label' => 'Terms', 'order' => 3, 'link' => '/pages/terms'],
			['label' => 'Policy', 'order' => 4, 'link' => '/pages/privacy-policy'],
			['label' => 'Contact', 'order' => 5, 'link' => '/contact'],
		];

		// Insérer les données dans la table footers
		DB::table('footers')->insert($footers);
	}
}

Il ne nous reste plus qu'à compléter DatabaseSeeder :

public function run(): void
{
    $this->call([
        ...
        MenusSeeder::class,
        FooterSeeder::class, 
    ]);
}

Pour terminer, on régénère la base et les données :

php artisan migrate:fresh --seed

La table menus se retrouve avec 3 enregistrements :

La table des sous-menus avec deux enregistrements :

Et pour footers, on en a quatre (les pages n'existent pas toutes) :

Mise en place des menus

À présent, on va faire apparaître les menus. Comme ceux-ci devront apparaître sur pratiquement toutes les pages, on va passer par un composeur de vues. Ouvrez le fichier AppServiceProvider et ajoutez ce code :

...

use App\Models\Menu;
use Illuminate\View\View;
use Illuminate\Support\Facades;

class AppServiceProvider extends ServiceProvider
{
    ...

    public function boot(): void
    {
        Facades\View::composer(['components.layouts.app'], function (View $view) {
			$view->with(
				'menus',
				Menu::with(['submenus' => function ($query) {
					$query->orderBy('order');
				}])->orderBy('order')->get()
			);
		});
    }
}

On utilise la méthode composer de la classe View. Elle dit à Laravel de composer (préparer) une vue spécifique (components.layouts.app) chaque fois qu'elle est rendue. La fonction anonyme (closure) passée en deuxième argument sera exécutée chaque fois que cette vue est rendue.

Menu::with est une méthode Eloquent qui charge les relations submenus pour chaque Menu.
La fonction anonyme function ($query) { $query->orderBy('order'); } est utilisée pour trier les submenus par leur colonne order.

orderBy('order') trie les Menu par leur colonne order.
get() récupère tous les Menu avec leurs submenus triés.

$view->with('menus', ...)

    Cette ligne passe les Menu triés à la vue sous le nom menus. Cela signifie que dans la vue components.layouts.app, on peut accéder à ces menus en utilisant $menus

Ce code prépare les données des menus et les passe à notre layout. Les menus sont triés par un ordre spécifique, et leurs sous-menus sont également triés par un ordre spécifique. Cela permet à la vue de toujours avoir les menus correctement triés et prêts à être affichés.

Dans notre layout (components/layouts/app.blade.php) on récupère ces menus et on les envoie aux barres de navigation :

<livewire:navigation.navbar :$menus />

...

<livewire:navigation.sidebar :$menus />

Les barres de navigation

On doit à présent récupérer les informations des menus dans les barres de navigation pour les afficher. Pour la barre de navigation principale (navigation/navbar.blade.php), on aura ce code :

    ...

use Illuminate\Support\Collection;

new class extends Component {

    public Collection $menus;

    public function mount(Collection $menus): void
    {
        $this->menus = $menus;
    }

    ...

};

    ...

<x-slot:actions>
    <span class="hidden lg:block">

        ...

        @foreach ($menus as $menu)
            @if ($menu->submenus->isNotEmpty())
                <x-dropdown>
                    <x-slot:trigger>
                        <x-button label="{{ $menu->label }}" class="btn-ghost" />
                    </x-slot:trigger>
                    @foreach ($menu->submenus as $submenu)
                        <x-menu-item title="{{ $submenu->label }}" link="{{ $submenu->link }}"
                            style="min-width: max-content;" />
                    @endforeach
                </x-dropdown>
            @else
                <x-button label="{{ $menu->label }}" link="{{ $menu->link }}" :external="Str::startsWith($menu->link, 'http')"
                    class="btn-ghost" />
            @endif
        @endforeach
    
    ...

</x-slot:actions>

On va faire la même chose pour la barre de navigation latérale (navigation/sidebar.blade.php), avec ce code :

    ...

use Illuminate\Support\Collection;

new class extends Component {

    public Collection $menus;

    public function mount(Collection $menus): void
    {
        $this->menus = $menus;
    }

    ...

};

    ...
<x-menu activate-by-route>
    
    ...

    @foreach ($menus as $menu)
        @if($menu->submenus->isNotEmpty())
            <x-menu-sub title="{{ $menu->label }}">
                @foreach ($menu->submenus as $submenu)
                    <x-menu-item title="{{ $submenu->label }}" link="{{ $submenu->link }}" />
                @endforeach
            </x-menu-sub>
        @else
            <x-menu-item title="{{ $menu->label }}" link="{{ $menu->link }}" />
        @endif
    @endforeach
</x-menu>

Le pied de page

On ne s'est pas encore beaucoup préoccupés du pied de page, là aussi on va avoir un menu, on va aussi y placer d'autres informations comme celles qui concernent les réseaux sociaux.

Ajoutez un composant de navigation ici :

Entrez ce code :

<?php

use App\Models\Footer;
use Livewire\Volt\Component;

new class() extends Component {

	public function with(): array
	{
		return [
			'footers' => Footer::orderBy('order')->get(),
		];
	}
};
?>

<footer class="p-10 rounded footer footer-center bg-base-200 text-base-content">
    <nav class="grid grid-flow-col gap-4">
        @foreach ($footers as $footer)
            <a href="{{ $footer->link }}" class="link link-hover">
                @lang($footer->label)
            </a>
        @endforeach
    </nav>
    <nav>
        <div class="grid grid-flow-col gap-4">
            <a href=" "" title="Github"
            target="_blank">
                <svg 
                    xmlns="http://www.w3.org/2000/svg" 
                    width="24" 
                    height="24" 
                    viewBox="0 0 24 24"
                    class="fill-current">
                    <path
                        d="M12 0C5.372 0 0 5.372 0 12c0 5.303 3.438 9.8 8.207 11.387.6.11.793-.26.793-.577v-2.2c-3.338.726-4.033-1.415-4.033-1.415-.546-1.387-1.333-1.757-1.333-1.757-1.089-.744.083-.729.083-.729 1.204.085 1.838 1.237 1.838 1.237 1.07 1.835 2.809 1.305 3.495.998.108-.775.419-1.305.762-1.605-2.665-.305-5.466-1.335-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.124-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.399 3-.405 1.02.006 2.043.139 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.873.118 3.176.77.84 1.233 1.91 1.233 3.22 0 4.61-2.804 5.62-5.475 5.92.43.37.815 1.1.815 2.22v3.293c0 .319.192.694.801.576C20.565 21.796 24 17.302 24 12c0-6.628-5.372-12-12-12z" />
                </svg>
            </a>
            <a href=" ""
                title="Discord"
                target="_blank">
                <svg 
                    width="25" 
                    height="28" 
                    viewBox="0 0 71 80" 
                    class="fill-current mt-[-.05rem]"
                    xmlns="http://www.w3.org/2000/svg">
                    <path
                        d="M60.1045 13.8978C55.5792 11.8214 50.7265 10.2916 45.6527 9.41542C45.5603 9.39851 45.468 9.44077 45.4204 9.52529C44.7963 10.6353 44.105 12.0834 43.6209 13.2216C38.1637 12.4046 32.7345 12.4046 27.3892 13.2216C26.905 12.0581 26.1886 10.6353 25.5617 9.52529C25.5141 9.44359 25.4218 9.40133 25.3294 9.41542C20.2584 10.2888 15.4057 11.8186 10.8776 13.8978C10.8384 13.9147 10.8048 13.9429 10.7825 13.9795C1.57795 27.7309 -0.943561 41.1443 0.293408 54.3914C0.299005 54.4562 0.335386 54.5182 0.385761 54.5576C6.45866 59.0174 12.3413 61.7249 18.1147 63.5195C18.2071 63.5477 18.305 63.5139 18.3638 63.4378C19.7295 61.5728 20.9469 59.6063 21.9907 57.5383C22.0523 57.4172 21.9935 57.2735 21.8676 57.2256C19.9366 56.4931 18.0979 55.6 16.3292 54.5858C16.1893 54.5041 16.1781 54.304 16.3068 54.2082C16.679 53.9293 17.0513 53.6391 17.4067 53.3461C17.471 53.2926 17.5606 53.2813 17.6362 53.3151C29.2558 58.6202 41.8354 58.6202 53.3179 53.3151C53.3935 53.2785 53.4831 53.2898 53.5502 53.3433C53.9057 53.6363 54.2779 53.9293 54.6529 54.2082C54.7816 54.304 54.7732 54.5041 54.6333 54.5858C52.8646 55.6197 51.0259 56.4931 49.0921 57.2228C48.9662 57.2707 48.9102 57.4172 48.9718 57.5383C50.038 59.6034 51.2554 61.5699 52.5959 63.435C52.6519 63.5139 52.7526 63.5477 52.845 63.5195C58.6464 61.7249 64.529 59.0174 70.6019 54.5576C70.6551 54.5182 70.6887 54.459 70.6943 54.3942C72.1747 39.0791 68.2147 25.7757 60.1968 13.9823C60.1772 13.9429 60.1437 13.9147 60.1045 13.8978ZM23.7259 46.3253C20.2276 46.3253 17.3451 43.1136 17.3451 39.1693C17.3451 35.225 20.1717 32.0133 23.7259 32.0133C27.308 32.0133 30.1626 35.2532 30.1066 39.1693C30.1066 43.1136 27.28 46.3253 23.7259 46.3253ZM47.3178 46.3253C43.8196 46.3253 40.9371 43.1136 40.9371 39.1693C40.9371 35.225 43.7636 32.0133 47.3178 32.0133C50.9 32.0133 53.7545 35.2532 53.6986 39.1693C53.6986 43.1136 50.9 46.3253 47.3178 46.3253Z" />
                </svg>
            </a>
        </div>
    </nav>
    <aside>
        <p>Version 0.1.0</a> © {{ date('Y') }} Moi</p>
    </aside>
</footer>

Il ne reste plus qu'à ajouter ce nouveau composant dans le layout :

    {{-- FOOTER --}}
    <hr><br>
    <livewire:navigation.footer />
    <br>

    {{--  TOAST area --}}
    <x-toast />

    <script src="{{ asset('storage/scripts/prism.js') }}"></script>
</body>
</html>

Dans ce pied de page, seulement les menus sont dynamiques, ce qui n'est pas très fonctionnel. Il faudra sans doute plus tard envisager de rendre dynamiques les autres informations.

Conclusion

Notre CMS commence à avoir une belle allure avec ses menus dynamiques et son pied de page. Nous avons bien avancé. la prochaine étape consistera à ajouter des commentaires aux articles, et ça va être un gros morceau !

Pour vous simplifier la vie, vous pouvez charger le projet dans son état à l’issue de ce chapitre.



Par bestmomo

Aucun commentaire

Article précédent : Mon CMS - Les articles
Article suivant : Mon CMS - Les commentaires (1/2)