Module 2Programmer avec Laravel
Chapitre 9 sur 17
Manipulation des vues et du moteur Blade
Vues, données, compositeurs, directives Blade, layouts, inclusions et composants.
- ≈ 205 min
- 9 leçons
- 3 vidéos
- 4 exercices
Module 2Programmer avec Laravel
Chapitre 9 sur 17
Vues, données, compositeurs, directives Blade, layouts, inclusions et composants.
Objectifs
Avec ce chapitre, tu sauras…
:::savoir Ajouté au résumé Le résumé présente les vues (création, données, compositeurs, optimisation) mais renvoie à la documentation pour le moteur Blade lui-même. Les sections 5 à 8 de ce chapitre couvrent Blade en entier : c'est l'outil que tu utiliseras dans toutes les pages de ton projet de fin de module. :::
Leçon 1
Renvoyer tout le HTML d'une page depuis une route ou un contrôleur n'est pas pratique. Les vues placent le HTML dans des fichiers séparés : elles séparent la logique de l'application de la présentation (rôle V de MVC). Elles sont rangées dans resources/views et portent l'extension .blade.php, qui indique qu'elles utilisent le moteur de gabarits Blade.
<!DOCTYPE html>
<html lang="fr">
<body>
<h1>Bonjour, {{ $nom }}</h1>
</body>
</html>Route::get('/bienvenue', function () {
return view('bienvenue', ['nom' => 'Fatima']);
});Le premier argument de view() est le nom de la vue : le chemin du fichier dans resources/views, sans l'extension .blade.php. Le second est un tableau de données : chaque clé devient une variable dans la vue. On peut aussi utiliser la façade View : View::make('bienvenue', ['nom' => 'Fatima']).
Les sous-dossiers
Les vues peuvent être rangées dans des sous-dossiers, désignés avec la notation point :
return view('admin.livres.index', $donnees); // resources/views/admin/livres/index.blade.phpUn nom de dossier ne doit donc jamais contenir de point.
Première vue disponible et existence d'une vue
use Illuminate\Support\Facades\View;
return View::first(['themes.ramadan.accueil', 'accueil'], $donnees); // la première qui existe
if (View::exists('emails.bienvenue')) {
// ...
}View::first est utile quand une vue peut être personnalisée : on cherche d'abord la version spéciale, sinon la version standard.
Leçon 2
// 1. un tableau associatif (le plus courant)
return view('catalogue.show', ['livre' => $livre, 'similaires' => $similaires]);
// 2. la méthode with(), chaînable
return view('catalogue.show')
->with('livre', $livre)
->with('similaires', $similaires);
// 3. compact() : crée le tableau à partir des noms de variables
return view('catalogue.show', compact('livre', 'similaires'));Dans la vue, chaque clé est une variable : {{ $livre->titre }}. Ce que Blade compile correspond à <?php echo e($livre->titre); ?>.
Partager des données avec toutes les vues
Certaines données sont utiles partout (le nom de la boutique, le nombre d'articles du panier). View::share, appelée dans la méthode boot d'un fournisseur de services, les rend disponibles dans toutes les vues :
use Illuminate\Support\Facades\View;
public function boot(): void
{
View::share('boutique', 'Kitab');
View::share('telephone', '05 22 00 00 00');
}Leçon 3
Un compositeur de vue (view composer) est une fonction ou une classe appelée chaque fois qu'une vue est rendue. Il ajoute des données à cette vue. C'est la bonne solution quand plusieurs routes affichent une même vue qui a toujours besoin des mêmes données, par exemple le menu des catégories de la barre latérale.
use App\Models\Categorie;
use App\View\Composers\PanierComposer;
use Illuminate\Support\Facades\View;
public function boot(): void
{
// compositeur sous forme de closure
View::composer('partials.menu-categories', function ($view) {
$view->with('categories', Categorie::orderBy('libelle')->get());
});
// compositeur sous forme de classe
View::composer('layouts.app', PanierComposer::class);
}Laravel ne fournit pas de dossier par défaut pour les compositeurs-classes : on peut créer app/View/Composers.
<?php
namespace App\View\Composers;
use App\Services\PanierService;
use Illuminate\View\View;
class PanierComposer
{
public function __construct(
protected PanierService $panier, // injecté par le conteneur de services
) {}
public function compose(View $view): void
{
$view->with('nbArticles', $this->panier->nombreArticles());
}
}Les compositeurs sont résolus par le conteneur de services : on peut typer n'importe quelle dépendance dans leur constructeur.
View::composer(['catalogue.index', 'catalogue.show'], PanierComposer::class);
View::composer('admin.*', function ($view) { /* toutes les vues du dossier admin */ });
View::composer('*', function ($view) { /* toutes les vues */ });Les créateurs de vues
Un créateur de vue (view creator) ressemble à un compositeur, mais il s'exécute dès que la vue est instanciée, au lieu d'attendre le moment où elle va être affichée. Les données qu'il ajoute peuvent donc être remplacées ensuite par le contrôleur.
use App\View\Creators\ProfilCreator;
View::creator('profil', ProfilCreator::class);Leçon 4
Leçon 5
Leçon 6
| Directive | Affiche le contenu si… |
|---|---|
@auth … @endauth | un utilisateur est connecté |
@guest … @endguest | personne n'est connecté |
@can('update', $livre) … @endcan | l'utilisateur a l'autorisation (chapitre 12) |
@env('local') … @endenv | l'application est dans cet environnement |
@production … @endproduction | l'application est en production |
Leçon 7
| Directive | Rôle |
|---|---|
@yield('nom', 'défaut') | dans le layout : affiche le contenu de la section nom (ou la valeur par défaut) |
@extends('layouts.app') | dans la page : hérite du layout (première ligne de la vue) |
@section('nom') … @endsection | remplit une zone avec un bloc |
@section('nom', 'texte') | remplit une zone avec un texte court |
@parent | dans une section : garde le contenu de la section du parent et ajoute le sien |
Leçon 8
| Élément | Rôle |
|---|---|
@props(['type' => 'info']) | déclare les propriétés du composant et leurs valeurs par défaut |
{{ $slot }} | le contenu placé entre les balises ouvrante et fermante |
$attributes | les autres attributs passés au composant (class, id…) ; merge les fusionne avec ceux du composant |
:prop="$variable" | passer une expression PHP (avec : devant) au lieu d'une chaîne : <x-carte-livre :livre="$livre" /> |
Leçon 9
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
1view('admin.livres.index') charge le fichier…
2{{ $avis->texte }} contenant <script> …
3Pour fournir les catégories à la barre latérale de toutes les pages du catalogue, on utilise…
4Dans une boucle, $loop->iteration vaut au premier tour…
5La directive qui gère une liste vide dans la même boucle est…
6Dans un layout, la zone remplie par les pages se déclare avec…
7Dans un composant, le contenu placé entre les balises est dans…
8<x-carte-livre :livre="$livre" /> : le : sert à…
L'essentiel du chapitre
resources/views, extension .blade.php ; view('dossier.fichier', $donnees) ou View::make ; notation point pour les sous-dossiers (pas de point dans les noms de dossiers) ; View::first, View::exists.->with(), compact() ; pour toutes les vues : View::share dans AppServiceProvider::boot().View::composer('vue', Classe::class ou closure) (méthode compose(View $view), dépendances injectées), plusieurs vues ou * ; créateurs : View::creator (dès l'instanciation). Production : view:cache / view:clear.{{ }} échappe (anti-XSS), {!! !!} n'échappe pas (jamais pour une saisie utilisateur), {{-- --}}, @{{ }}, @json.@if/@elseif/@else, @unless, @isset, @empty, @auth, @guest, @can, @switch ; @foreach, @forelse/@empty, @for, @while, @continue, @break, $loop (index, iteration, first, last, count) ; @class, @style, @checked, @selected, @disabled ; @php.@yield, page avec @extends + @section (+ @parent), @stack/@push ; @include (+ données), @includeIf, @includeWhen, @each.@switch($x) @case(1) … @break @default @endswitch | équivalent du switch PHP |
| Propriété | Valeur |
|---|---|
$loop->index | l'indice, à partir de 0 |
$loop->iteration | le numéro, à partir de 1 |
$loop->first / $loop->last | true au premier / au dernier tour |
$loop->count | le nombre total d'éléments |
$loop->remaining | le nombre de tours restants |
$loop->even / $loop->odd | tour pair / impair |
$loop->parent | la variable $loop de la boucle englobante |
@stack('nom') / @push('nom') … @endpush | une pile : chaque page peut ajouter des scripts ou des styles |
| Écriture | Résultat |
|---|---|
@vite(['resources/css/app.css', 'resources/js/app.js']) | les balises <link> et <script> du CSS et du JS compilés par Vite (npm run dev ou npm run build) |
{{ asset('images/logo.svg') }} | l'URL d'un fichier de public/ : http://kitab.test/images/logo.svg |
{{ url('/livres') }} | une URL absolue de l'application |
{{ route('livres.show', $livre) }} | l'URL d'une route nommée |
Héritage (@extends) | Composants (<x-...>) |
|---|
un layout avec des @yield, des pages avec @section | un composant layout avec {{ $slot }} et des slots nommés |
| syntaxe historique, présente dans beaucoup de projets et d'examens | syntaxe moderne, utilisée par les kits de démarrage |
| un seul layout parent par page | les composants s'imbriquent et se combinent librement |
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
Écris : (1) un layout layouts/app.blade.php avec un titre de page variable (« Kitab » par défaut), un menu (Accueil, Catalogue, et « Connexion » pour les invités ou « Mon compte » pour les connectés), une zone de contenu et un pied de page avec l'année courante ; (2) la page pages/a-propos.blade.php qui hérite du layout avec le titre « À propos » ; (3) la même page écrite avec un composant <x-layout>.
Crée un composant anonyme <x-etoiles :note="4" /> qui affiche 5 étoiles (★ pleines selon la note, ☆ vides pour le reste), accepte une propriété max (5 par défaut) et transmet les autres attributs (classe, titre) à la balise <span> principale. Ajoute un attribut aria-label « Note : 4 sur 5 ».
resources/views/components (@props, {{ $slot }}, $attributes->merge, slots nommés <x-slot:nom>), avec classe (make:component, propriétés et méthodes publiques) ; :prop="$var" pour une expression.@vite([...]), asset(), url(), route().Par défaut, les gabarits Blade sont compilés à la demande en PHP pur, dans storage/framework/views. À chaque requête, Laravel vérifie si la version compilée existe et si le gabarit a été modifié depuis ; si oui, il recompile. Cette vérification a un petit coût. En production, on précompile toutes les vues au déploiement :
php artisan view:cache # précompile toutes les vues
php artisan view:clear # vide le cache des vuesL'affichage sécurisé
<h1>{{ $livre->titre }}</h1>
<p>Prix : {{ number_format($livre->prix, 2, ',', ' ') }} DH</p>
<p>Il y a {{ count($livres) }} livres. Nous sommes le {{ now()->format('d/m/Y') }}.</p>{{ ... }} affiche le résultat d'une expression PHP en l'échappant avec htmlspecialchars : les caractères <, >, ", & sont transformés en entités HTML. Si un utilisateur écrit <script>alert(1)</script> dans un avis, le texte s'affiche tel quel au lieu d'être exécuté : c'est la protection contre le XSS (module M108).
{!! $article->contenu_html !!} {{-- affiche SANS échappement --}}{!! ... !!} affiche le HTML tel quel. Ne l'utilise jamais avec une donnée saisie par un utilisateur (nom, avis, message). Réserve-le à du HTML que tu produis toi-même ou que tu as nettoyé (avec une bibliothèque comme HTML Purifier).
Commentaires, accolades et JSON
{{-- Ce commentaire n'apparaît pas dans le HTML envoyé au navigateur --}}
<p>@{{ message }}</p> {{-- le @ laisse {{ message }} intact pour Vue.js ou Alpine --}}
@verbatim
<div>{{ texte }} et {{ autre }}</div> {{-- tout le bloc est laissé intact --}}
@endverbatim
<script>
const livres = @json($livres); // convertit en JSON pour JavaScript
</script>Les directives commencent par @. Elles remplacent les balises <?php if (...): ?> et rendent les gabarits lisibles.
Les conditions
@if ($livre->stock === 0)
<span class="badge rupture">Épuisé</span>
@elseif ($livre->stock < 5)
<span class="badge alerte">Plus que {{ $livre->stock }} exemplaires</span>
@else
<span class="badge dispo">En stock</span>
@endif
@unless ($livre->disponible) {{-- « si non » --}}
<p>Ce livre n'est pas disponible.</p>
@endunless
@isset($promotion) {{-- la variable existe et n'est pas null --}}
<p>Promotion : -{{ $promotion }} %</p>
@endisset
@empty($livres) {{-- la variable est vide --}}
<p>Aucun livre.</p>
@endemptyInclure une vue
@include insère une autre vue (un « morceau » réutilisable). La vue incluse reçoit toutes les variables de la vue parente, plus celles qu'on lui passe :
@include('partials.menu')
@include('partials.carte-livre', ['livre' => $livre, 'compact' => true])
@includeIf('partials.promo-ramadan') {{-- seulement si la vue existe --}}
@includeWhen($livre->nouveaute, 'partials.badge-nouveau')
@each('partials.carte-livre', $livres, 'livre', 'partials.aucun-livre') {{-- une vue par élément --}}L'héritage de gabarits
Toutes les pages d'un site partagent la même structure (en-tête, menu, pied de page). On écrit cette structure une fois dans un layout, qui définit des zones avec @yield ; chaque page hérite du layout avec @extends et remplit les zones avec @section.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('titre', 'Kitab') · Librairie en ligne</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('styles')
</head>
<body>
@include('partials.navigation')
<main class="conteneur">
@if (session('status'))
<div class="alerte succes">{{ session('status') }}</div>
@endif
@yield('contenu')
</main>
<footer>© {{ date('Y') }} Kitab · Casablanca</footer>
@stack('scripts')
</body>
</html>@extends('layouts.app')
@section('titre', 'Catalogue')
@section('contenu')
<h1>Notre catalogue</h1>
<div class="grille">
@foreach ($livres as $livre)
@include('partials.carte-livre', ['livre' => $livre])
@endforeach
</div>
@endsection
@push('scripts')
<script src="{{ asset('js/filtres.js') }}"></script>
@endpushLes composants sont l'autre façon (plus moderne) de construire des morceaux réutilisables et des layouts. Un composant s'utilise comme une balise HTML qui commence par x-.
Les composants anonymes
Un fichier dans resources/views/components devient un composant, sans classe PHP :
@props(['type' => 'info'])
<div {{ $attributes->merge(['class' => 'alerte alerte-' . $type]) }} role="alert">
{{ $slot }}
</div><x-alerte type="succes">Votre commande a été enregistrée.</x-alerte>
<x-alerte type="erreur" class="mb-4" id="erreur-paiement">Le paiement a été refusé.</x-alerte>
{{-- <div class="alerte alerte-erreur mb-4" id="erreur-paiement" role="alert">… --}}Un composant dans un sous-dossier s'appelle avec un point : components/form/champ.blade.php → <x-form.champ />.
Un layout sous forme de composant
@props(['titre' => 'Kitab'])
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{{ $titre }} · Kitab</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<header>{{ $entete ?? '' }}</header>
<main>{{ $slot }}</main>
</body>
</html><x-layout :titre="$livre->titre">
<x-slot:entete>
<a href="{{ route('livres.index') }}">← Retour au catalogue</a>
</x-slot:entete>
<h1>{{ $livre->titre }}</h1>
<p>{{ $livre->resume }}</p>
</x-layout>Un slot nommé (<x-slot:entete>) remplit une zone précise du composant, disponible dans la variable $entete.
Les composants avec classe
Pour un composant qui a besoin de logique (calculer des données), Artisan crée une classe et une vue :
php artisan make:component Prix
# app/View/Components/Prix.php et resources/views/components/prix.blade.php<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Prix extends Component
{
public function __construct(
public float $montant,
public ?float $ancien = null,
) {}
public function format(float $valeur): string
{
return number_format($valeur, 2, ',', ' ') . ' DH';
}
public function render(): View|Closure|string
{
return view('components.prix');
}
}<span class="prix">
@if ($ancien)
<del>{{ $format($ancien) }}</del>
@endif
<strong>{{ $format($montant) }}</strong>
</span><x-prix :montant="$livre->prix" :ancien="$livre->prix_avant_promo" />Les propriétés publiques et les méthodes publiques de la classe sont disponibles dans la vue du composant ($montant, $format(...)).
Créer un layout avec les composants Blade ({{ $slot }}).
(1) Crée le layout layouts/app.blade.php de Kitab (avec @vite, la balise CSRF et le message flashé). (2) Crée le partiel partials/carte-livre.blade.php (titre, auteur, prix formaté, badge de stock). (3) Modifie CatalogueController pour renvoyer les vues catalogue.index (grille de cartes, message si aucun livre) et catalogue.show. (4) Ajoute un compositeur qui partage la liste des catégories (un tableau pour l'instant) avec le partiel partials/menu-categories. (5) Crée le composant <x-prix>. (6) Vérifie qu'un titre contenant <b> s'affiche tel quel.
Réécris ce gabarit en Blade, sans aucune balise <?php :
<?php if (count($livres) > 0): ?>
<ul>
<?php foreach ($livres as $i => $livre): ?>
<li class="<?= $i % 2 == 0 ? 'pair' : '' ?>">
<?= $i + 1 ?>. <?= htmlspecialchars($livre->titre) ?>
<?php if ($livre->stock == 0): ?><em>épuisé</em><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p>Aucun livre.</p>
<?php endif; ?>@auth
<a href="{{ route('compte') }}">Bonjour {{ auth()->user()->name }}</a>
@endauth
@guest
<a href="{{ route('login') }}">Se connecter</a>
@endguestLes boucles
@foreach ($livres as $livre)
<li>{{ $livre->titre }}</li>
@endforeach
@forelse ($livres as $livre) {{-- foreach + cas « liste vide » --}}
<li>{{ $livre->titre }}</li>
@empty
<li>Aucun livre ne correspond à votre recherche.</li>
@endforelse
@for ($i = 1; $i <= 5; $i++)
<span>{{ $i <= $note ? '★' : '☆' }}</span>
@endfor
@while ($condition)
...
@endwhileDans une boucle, @continue passe à l'élément suivant et @break arrête la boucle ; on peut leur donner une condition : @continue($livre->stock === 0).
La variable $loop
Dans un @foreach ou un @forelse, la variable $loop donne des informations sur l'itération en cours :
<table>
@foreach ($commandes as $commande)
<tr @class(['ligne-paire' => $loop->even])>
<td>{{ $loop->iteration }}</td>
<td>{{ $commande->numero }}</td>
</tr>
@endforeach
</table>Les attributs conditionnels
<a href="{{ route('livres.index') }}" @class(['lien', 'actif' => request()->routeIs('livres.*')])>Catalogue</a>
<div @style(['background: #fdeeea' => $livre->nouveaute])>...</div>
<input type="checkbox" name="en_stock" @checked(old('en_stock', $filtre->en_stock))>
<option value="fr" @selected($langue === 'fr')>Français</option>
<button type="submit" @disabled($livre->stock === 0)>Ajouter au panier</button>@class construit l'attribut class à partir d'un tableau : les classes sans clé sont toujours ajoutées, celles avec une condition seulement si elle est vraie.
Du PHP brut
@php
$total = $lignes->sum(fn ($l) => $l->prix * $l->quantite);
@endphp
<p>Total : {{ $total }} DH</p>À utiliser avec modération : les calculs appartiennent au contrôleur ou au modèle, pas à la vue.
Tableau: Vite, les assets et les URL
Les deux syntaxes fonctionnent et se combinent : sache lire et écrire les deux.
Mettre en valeur le lien actif du menu : props et attributs de composants.
Techniques Blade et Tailwind pour les vues d'un vrai projet.