Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Voir cette série
Mon CMS - Les paramètres
Mardi 15 octobre 2024 08:58

Nous en arrivons au terme de cette série sur la création d'un CMS avec Laravel. Il y aurait évidemment encore bien des choses à ajouter, mais l'essentiel a été exposé et décrit. Pour une version complète et actualisée, vous pouvez aller sur Github où je maintiens le projet. Vous êtes invités à participer au développement, laisser des commentaires, et contribuer selon la forme qui vous convient le mieux.

Pour ce dernier article de la série, on va ajouter quelques réglages à notre CMS : nombre de pages à paginer dans l'accueil, mode maintenance, titre et sous-titre, et d'autres fantaisies que nous allons voir.

Les données

Nos réglages seront enregistrés dans la base de données. On crée une nouvelle table et son modèle avec Artisan :

php artisan make:model Setting --migration

La migration

Pour la migration, on va faire simple avec un système classique clé/valeur :

public function up(): void
{
    Schema::create('settings', function (Blueprint $table) {
        $table->id();
        $table->string('key')->unique();
        $table->text('value');
    });
}

Le modèle

Pour le modèle onsignale qu'on n'a pas besoin du timestamp et on complète la propriété $fillable :

class Setting extends Model
{
    public $timestamps = false;
	protected $fillable = ['key', 'value'];
}

Le seeder

Pour remplir notre table, on ajoute un seeder :

php artisan make:seeder SettingSeeder

Avec ce code :

<?php

namespace Database\seeders;

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

class SettingSeeder extends Seeder
{
	public function run()
	{
		$settings = [
			['key' => 'pagination', 'value' => 6],
			['key' => 'excerptSize', 'value' => 45],
			['key' => 'title', 'value' => 'Mon titre'],
			['key' => 'subTitle', 'value' => 'Mon sous-titre'],
			['key' => 'newPost', 'value' => 4],
		];

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

On complète DatabaseSeeder :

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

Il ne reste plus qu'à lancer la migration en faisant un rafraîchissement :

php artisan migrate:fresh --seed

Vérifiez que vous avez la nouvelle table settings avec les valeurs prévues :

Le chargement des paramètres

Les données de paramétrage sont dans la base de données, il faut donc aller les chercher quand on charge une page pour avoir tous ces réglages. Le bon endroit pour faire ça est AppServiceProvider :

use App\Models\{Menu, Setting};

...

public function boot(): void
{
    if (!$this->app->runningInConsole()) {
        $settings = Setting::all();
        foreach ($settings as $setting) {
            config(['app.' . $setting->key => $setting->value]);
        }
    }

On dispose ainsi des valeurs dans la configuration.

Utilisation des paramètres

Maintenant qu'on a chargé les paramètres, il faut les utiliser ! Pour la pagination et excerptSize, on avait déjà prévu une valeur dans le fichier de configuration et adapté le code.

Titre et sous-titre

Pour le titre et le sous-titre, on doit compléter layouts.app :

<div class="text-center hero-content text-neutral-content">
    <div>
        <h1 class="mb-5 text-4xl font-bold sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl">
            {{ config('app.title') }}
        </h1>
        <p class="mb-5 text-lg sm:text-xl md:text-2xl lg:text-3xl xl:text-4xl">
            {{ config('app.subTitle') }}
        </p>
    </div>                
</div>

Nouvel article

La valeur newPost doit être prise en compte dans index :

@if ($post->pinned)
    <x-badge value="{{ __('Pinned') }}" class="p-3 badge-warning" />
@elseif($post->created_at->gt(now()->subWeeks(config('app.newPost'))))
    <x-badge value="{{ __('New') }}" class="p-3 badge-success" />
@endif

Un article qui qui répond au critère de nouveauté est équipé d'un badge pour le repérer :

Un composant

On a à nouveau besoin d'un composant Volt pour gérer les paramètres et faire tout le traitement :

php artisan make:volt admin/settings --class

On va ajouter la route pour l'atteindre :

Route::middleware('auth')->group(function () {
	...
	Route::middleware(IsAdminOrRedac::class)->prefix('admin')->group(function () {
		...
		Route::middleware(IsAdmin::class)->group(function () {
			...
			Volt::route('/settings', 'admin.settings')->name('settings');

Et dans la foulée un item dans la barre latérale (admin.sidebar) :

@if (Auth::user()->isAdmin())
    ...
    <x-menu-item icon="m-cog-8-tooth" title="{{ __('Settings') }}" link="{{ route('settings') }}" :class="App::isDownForMaintenance() ? 'bg-red-300' : ''" />
@endif

Pour la maintenance, on va avoir besoin de variables dans le fichier .env :

APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
APP_MAINTENANCE_SECRET=230542a-177b-4b66-ffb1-dd77g4c93515

On aura aussi besoin de traductions :

"Home pagination": "Pagination de la page d'accueil",
"Between 2 and 12.": "Entre 2 et 12.",
"Post excerpt (number of words)": "Extrait de l'article (nombre de mots)",
"Between 30 and 60.": "Entre 30 et 60.",
"Site title": "Titre du site",
"Site sub title": "Sous titre",
"Settings updated successfully!": "Paramètres bien enregistrés!",
"Number of weeks a post is marked new": "Nombre de semaines un article est marqué comme nouveau",
"Between 1 and 8.": "Entre 1 et 8",
"Go to bypass page": "Aller sur la page de bypass",

Et enfin le code du composant :

<?php

use App\Models\Setting;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Artisan;
use Livewire\Attributes\{Layout, Validate};
use Livewire\Volt\Component;
use Mary\Traits\Toast;

new #[Title('Settings')] #[Layout('components.layouts.admin')] 
class extends Component {
	use Toast;

	private const SETTINGS_KEYS = ['pagination', 'excerptSize', 'title', 'subTitle', 'newPost'];

	#[Validate('required|max:30')]
	public string $title;

	#[Validate('required|max:50')]
	public string $subTitle;

	#[Validate('required|integer|between:2,12')]
	public int $pagination;

	#[Validate('required|integer|between:30,60')]
	public int $excerptSize;

	#[Validate('required|integer|between:1,8')]
	public int $newPost;

	public bool $maintenance = false;
	public Collection $settings;

	public function mount(): void
	{
		$this->settings = Setting::all();

		$this->maintenance = App::isDownForMaintenance();

		foreach (self::SETTINGS_KEYS as $key) {
			$this->{$key} = $this->settings->where('key', $key)->first()->value ?? null;
		}
	}

	public function updatedMaintenance(): void
	{
		if ($this->maintenance) {
			Artisan::call('down', ['--secret' => env('APP_MAINTENANCE_SECRET')]);
		} else {
			Artisan::call('up');
		}
	}

	public function save()
	{
		$data = $this->validate();

		DB::transaction(function () use ($data) {
			foreach (self::SETTINGS_KEYS as $key) {
				$setting = $this->settings->where('key', $key)->first();
				if ($setting) {
					$setting->value = $data[$key];
					$setting->save();
				}
			}
		});

		$this->success(__('Settings updated successfully!'));
	}
};

?>

<div>
    <x-header title="{{ __('Settings') }}" separator progress-indicator>
        <x-slot:actions>
            <x-button icon="s-building-office-2" label="{{ __('Dashboard') }}" class="btn-outline lg:hidden"
                link="{{ route('admin') }}" />
        </x-slot:actions>
    </x-header>
    <x-card>
        <x-card separator class="mb-6 border-4 {{ $maintenance ? 'bg-red-300' : 'bg-zinc-100' }} border-zinc-950">
            <div class="flex items-center justify-between">
                <x-toggle label="{{ __('Maintenance Mode') }}" wire:model="maintenance" wire:change="$refresh" />
                @if($maintenance)
                    <x-button label="{{ __('Go to bypass page')}}" link="/{{ env('APP_MAINTENANCE_SECRET') }}"  />
                @endif
            </div>
        </x-card>
        <x-form wire:submit="save">
            <x-card separator class="border-4 bg-zinc-100 border-zinc-950">
                <x-input label="{{ __('Site title') }}" wire:model="title" />
                <br>
                <x-input label="{{ __('Site sub title') }}" wire:model="subTitle" />
            </x-card>
            <x-card separator class="border-4 bg-zinc-100 border-zinc-950">
                <x-range min="2" max="12" wire:model="pagination" label="{!! __('Home pagination') !!}"
                    hint="{{ __('Between 2 and 12.') }}" class="range-info" wire:change="$refresh" />
                <x-badge value="{{ $pagination }}" class="my-2 badge-neutral" />
            </x-card>
            <x-card separator class="border-4 bg-zinc-100 border-zinc-950">
                <x-range min="30" max="60" step="5" wire:model="excerptSize"
                    label="{!! __('Post excerpt (number of words)') !!}" hint="{{ __('Between 30 and 60.') }}" class="range-info"
                    wire:change="$refresh" />
                <x-badge value="{{ $excerptSize }}" class="my-2 badge-neutral" />
            </x-card>
            <x-card separator class="border-4 bg-zinc-100 border-zinc-950">
                <x-range min="1" max="8" step="1" wire:model="newPost"
                    label="{!! __('Number of weeks a post is marked new') !!}" hint="{{ __('Between 1 and 8.') }}" class="range-info"
                    wire:change="$refresh" />
                <x-badge value="{{ $newPost }}" class="my-2 badge-neutral" />
            </x-card>
            <x-slot:actions>
                <x-button label="{{ __('Save') }}" icon="o-paper-airplane" spinner="save" type="submit"
                    class="btn-primary" />
            </x-slot:actions>
        </x-form>

    </x-card>
</div>

Vous devriez obtenir cet aspect :

Nous allons voir un peu le fonctionnement...

Le mode maintenance

Lorsque votre application Laravel est en mode maintenance, une vue personnalisée s'affiche pour toutes les requêtes entrantes. Cette fonctionnalité permet de mettre temporairement votre application hors service, que ce soit pour effectuer des mises à jour ou des opérations de maintenance.

Le middleware par défaut de Laravel intègre une vérification du mode maintenance. Si ce mode est activé, le système génère une exception spécifique avec un code d'état 503. Cette exception, de type HttpException, provient du composant HttpKernel de Symfony.

Cette approche offre une manière élégante de gérer les périodes d'indisponibilité planifiées de votre application, tout en informant les utilisateurs de la situation de manière appropriée. Vous avez tous les détails dans la documentation de Laravel. S'il est possible de tout gérer avec des commandes Artisan, il est quand même plus judicieux de prévoir une utilisation plus sympathique, ce que j'ai prévu sur la page des paramètres avec un curseur :

L'action est gérée par cette simple fonction qui lance les commandes Artisan :

public function updatedMaintenance(): void
{
    if ($this->maintenance) {
        Artisan::call('down', ['--secret' => env('APP_MAINTENANCE_SECRET')]);
    } else {
        Artisan::call('up');
    }
}

La clé secrète a été placée dans le fichier .env. Une fois le mode maintenance activé, il faut utiliser cette clé pour accéder de nouveau au CMS. Pour éviter d'avoir à taper ça à la main, j'ai prévu un bouton dédié :

D'autre part, une bonne couleur visible rappelle l'état de maintenance activée. De la même manière, on le signale dans le menu latéral :

Gestion des paramètres

Au chargement du composant, on renseigne les propriétés avec tous les paramètres :

public function mount(): void
{
    $this->settings = Setting::all();
    $this->maintenance = App::isDownForMaintenance();
    foreach (self::SETTINGS_KEYS as $key) {
        $this->{$key} = $this->settings->where('key', $key)->first()->value ?? null;
    }
}

Une autre approche consisterait à aller les chercher en mémoire dans la configuration puisqu'on charge déjà tous ces paramètres au démarrage de l'application, mais je trouve que c'est plus "propre" comme ça.

Pour l'enregistrement des paramètres, j'ai prévu une validation et une transaction :

public function save()
{
    $data = $this->validate();

    DB::transaction(function () use ($data) {
        foreach (self::SETTINGS_KEYS as $key) {
            $setting = $this->settings->where('key', $key)->first();
            if ($setting) {
                $setting->value = $data[$key];
                $setting->save();
            }
        }
    });

    $this->success(__('Settings updated successfully!'));
}

Conclusion

Ainsi se termine cette série sur la création d'un CMS. Comme je l'ai déjà mentionné au début de cet article, pour une version complète et actualisée, vous pouvez aller sur Github où je maintiens le projet.

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 médias (partie 2)