Views
Bloc Laravel · séance 3 · ~60 min
Au chapitre MVC, la vue posts/index.blade.php affichait des articles avec @foreach et {{ }}. Un site compte le plus souvent plusieurs pages, qui répètent le même <head>, le même en-tête et le même pied de page. Dans ce chapitre, vous construisez le site d'un festival de musique inventé, « Les Nuits du Parc ». Vous apprenez à écrire ces lignes communes une seule fois, dans un fichier que toutes les pages réutilisent. Vous travaillez dans le projet premier-test-laravel, sans base de données.
À la fin de ce chapitre, vous serez capables de :
- afficher les données d'un contrôleur avec
@if,@foreach,$loopet@forelse, - créer un composant Blade, un morceau de template réutilisable, et lui transmettre des données,
- écrire le layout d'un site, le template que toutes ses pages partagent, et l'appliquer à plusieurs pages,
- convertir un layout écrit avec
@extends,@section,@yieldet@includeen composants, - trouver la cause d'une erreur de vue, de variable ou de composant introuvable, et protéger un formulaire avec
@csrf.
1. Trois pages qui se ressemblent
Commencez par la page des infos pratiques. La commande make:view crée le fichier d'une vue. Vous lui donnez le nom de la vue, avec un point à la place de la barre oblique : festival.info désigne le fichier info.blade.php du dossier festival. Si le dossier n'existe pas, la commande le crée aussi :
php artisan make:view festival.info
INFO View [resources/views/festival/info.blade.php] created successfully.
Le fichier créé contient une balise <div> et un commentaire HTML avec une citation tirée au hasard, qui change à chaque création :
<div>
<!-- When there is no desire, all things are at peace. - Laozi -->
</div>
Un commentaire HTML fait partie de la page envoyée au navigateur, et tout visiteur peut le lire en affichant le code source. Remplacez tout le contenu du fichier par cette page complète :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Infos pratiques · Les Nuits du Parc</title>
</head>
<body>
<header>
<p>Les Nuits du Parc</p>
<nav>
<a href="/concerts">Programme</a>
<a href="/infos">Infos pratiques</a>
<a href="/concerts/3">Concert de clôture</a>
</nav>
</header>
<main>
<h1>Infos pratiques</h1>
<p>Le festival a lieu au parc communal, du vendredi au dimanche.</p>
</main>
<footer>
<p>Les Nuits du Parc, édition 2026</p>
</footer>
</body>
</html>
Cette page n'affiche aucune donnée venue d'un contrôleur. Vous la reliez donc à son adresse avec Route::view, qui renvoie directement la vue, sans passer par un contrôleur. Ajoutez cette route à la fin de routes/web.php, qui contient déjà les routes des chapitres Routing et Controllers :
Route::view('/infos', 'festival.info')->name('info');
curl -s http://premier-test-laravel.test/infos
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Infos pratiques · Les Nuits du Parc</title>
…
<main>
<h1>Infos pratiques</h1>
<p>Le festival a lieu au parc communal, du vendredi au dimanche.</p>
</main>
…
</html>
Le site aura deux autres pages : le programme des concerts et la fiche d'un concert. Si vous les écriviez de la même façon, les trois fichiers répéteraient une vingtaine de lignes identiques. Seuls le <title> et le contenu de <main> changeraient d'une page à l'autre.
Le code qui sent mauvais
En binôme, cinq minutes. Le festival change de nom pour « Parc en Musique », et vous devez ajouter au menu un lien « Devenir bénévole ». Avec trois pages écrites comme festival/info.blade.php, quels fichiers ouvrez-vous, et combien de lignes modifiez-vous ?
Pour éviter de recopier ces lignes, les vues des concerts que vous écrivez maintenant ne contiennent que leur propre contenu, sans <!DOCTYPE html> ni en-tête. Le navigateur affiche tout de même un fragment de HTML qui n'a ni <html> ni <head> : il complète lui-même la structure manquante.
2. Passer des données à une vue
Vous connaissez depuis le chapitre MVC la fonction view(), son tableau de données et l'affichage échappé {{ }}. Au chapitre Routing, Route::view était réservé aux pages sans données. La page du programme des concerts, elle, affiche des données : sa route appelle donc un contrôleur. Créez-le avec Artisan :
php artisan make:controller ConcertController
INFO Controller [app/Http/Controllers/ConcertController.php] created successfully.
Artisan a écrit une classe vide qui importe Request. Remplacez tout le contenu du fichier. Les concerts, inventés, sont rangés dans une propriété, comme les relevés de WeatherController au chapitre Controllers. La méthode index() renvoie une View : l'import use Illuminate\View\View; remplace celui de Request.
<?php
namespace App\Http\Controllers;
use Illuminate\View\View;
class ConcertController extends Controller
{
private array $concerts = [
1 => [
'artist' => 'Duo Mistral',
'stage' => 'Chapiteau',
'time' => '18 h',
'duration' => 45,
'description' => 'Guitare et violon, <strong>entrée libre</strong>.',
'guests' => [],
],
2 => [
'artist' => "L'Orchestre du Lac",
'stage' => 'Grande scène',
'time' => '20 h',
'duration' => 90,
'description' => 'Un orchestre de trente musiciens, <strong>gratuit</strong> pour les moins de 12 ans.',
'guests' => ['Ana Lopes', 'Karim Benali'],
],
3 => [
'artist' => 'Orage Mécanique',
'stage' => 'Grande scène',
'time' => '22 h',
'duration' => 75,
'description' => 'Rock et percussions pour la <strong>clôture</strong>.',
'guests' => ['Les Tambours du Parc'],
],
];
public function index(): View
{
return view('concerts.index', ['concerts' => $this->concerts]);
}
}
La clé de chaque concert, de 1 à 3, lui sert de numéro. Créez la vue avec php artisan make:view concerts.index, puis remplacez son contenu par ce template :
<h1>Programme</h1>
<ul>
@foreach ($concerts as $concert)
<li>{{ $concert['artist'] }}, {{ $concert['stage'] }}, {{ $concert['time'] }}</li>
@endforeach
</ul>
Dans routes/web.php, importez ConcertController au-dessus de LatestBookController, puis ajoutez la route à la fin du fichier :
Route::get('/concerts', [ConcertController::class, 'index'])->name('concerts.index');
curl -s http://premier-test-laravel.test/concerts
<h1>Programme</h1>
<ul>
<li>Duo Mistral, Chapiteau, 18 h</li>
<li>L'Orchestre du Lac, Grande scène, 20 h</li>
<li>Orage Mécanique, Grande scène, 22 h</li>
</ul>
Un concert est un tableau, et non un objet comme $post au chapitre MVC : vous écrivez donc $concert['artist'], avec des crochets. {{ }} a remplacé l'apostrophe de « L'Orchestre » par ', le code HTML de ce caractère. C'est l'échappement vu au chapitre MVC : le navigateur affiche une apostrophe, mais ne peut pas la prendre pour du code.
Dans du code existant, vous rencontrerez aussi compact('concerts'), une fonction PHP qui fabrique le tableau ['concerts' => $concerts] à partir d'une variable $concerts, et la méthode ->with('concerts', …). Ces deux écritures produisent la même page. Dans ce cours, vous écrivez le tableau, car il montre sur une seule ligne le nom de chaque variable et sa valeur.
La vue introuvable
Si le nom donné à view() ne correspond à aucun fichier, par exemple view('concerts.program', …), Laravel renvoie une erreur 500 avec ce message :
View [concerts.program] not found.
Laravel cherche la vue dans resources/views/, en remplaçant chaque point par une barre oblique : concerts.program désigne concerts/program.blade.php, un fichier qui n'existe pas. Remettez concerts.index.
3. Les directives de tous les jours
Une directive est une instruction Blade qui commence par @, comme @foreach. Vous allez en utiliser plusieurs dans une nouvelle page, la fiche d'un concert. Dans ConcertController, ajoutez show() sous index() :
public function show(int $id): View
{
if (! isset($this->concerts[$id])) {
abort(404);
}
return view('concerts.show', ['concert' => $this->concerts[$id]]);
}
La fonction abort(404) arrête le traitement et renvoie la page 404 de Laravel. Sans cet appel, pour l'adresse /concerts/9, PHP chercherait la clé 9 dans $this->concerts, ne la trouverait pas, et Laravel renverrait une erreur 500 au lieu d'une page 404. Ajoutez la route à la fin de routes/web.php :
Route::get('/concerts/{id}', [ConcertController::class, 'show'])->whereNumber('id')->name('concerts.show');
Créez la vue avec php artisan make:view concerts.show et écrivez ce template :
{{-- Ce commentaire Blade n'arrive pas dans la page --}}
<!-- Ce commentaire HTML arrive dans la page -->
<h1>{{ $concert['artist'] }}</h1>
<p>{{ $concert['stage'] }}, {{ $concert['time'] }}</p>
@if ($concert['duration'] >= 90)
<p>Long concert : {{ $concert['duration'] }} minutes, avec entracte.</p>
@elseif ($concert['duration'] >= 60)
<p>Concert de {{ $concert['duration'] }} minutes, sans entracte.</p>
@else
<p>Concert court : {{ $concert['duration'] }} minutes.</p>
@endif
<p>{{ $concert['description'] }}</p>
<p>{!! $concert['description'] !!}</p>
<h2>Invités</h2>
<ul>
@forelse ($concert['guests'] as $guest)
<li>{{ $guest }}</li>
@empty
<li>Aucun invité annoncé</li>
@endforelse
</ul>
curl -s http://premier-test-laravel.test/concerts/2
<!-- Ce commentaire HTML arrive dans la page -->
<h1>L'Orchestre du Lac</h1>
<p>Grande scène, 20 h</p>
<p>Long concert : 90 minutes, avec entracte.</p>
<p>Un orchestre de trente musiciens, <strong>gratuit</strong> pour les moins de 12 ans.</p>
<p>Un orchestre de trente musiciens, <strong>gratuit</strong> pour les moins de 12 ans.</p>
<h2>Invités</h2>
<ul>
<li>Ana Lopes</li>
<li>Karim Benali</li>
</ul>
Lisez la réponse de haut en bas :
- Les commentaires. Blade retire le commentaire
{{-- … --}}avant d'envoyer la page : il n'apparaît pas dans la réponse. Le commentaire HTML<!-- … -->y reste, et tout visiteur peut le lire. @if,@elseif,@else,@endiffonctionnent comme leifde PHP. Pour un concert de 90 minutes, la condition de@ifest vraie : Blade n'affiche que le premier paragraphe.{!! !!}affiche sans échapper. Avec{{ }}, la balise<strong>est envoyée sous la forme<strong>, et le navigateur affiche les caractères<strong>comme du texte. Avec{!! !!}, la balise arrive intacte et le mot passe en gras.
N'utilisez {!! !!} que pour du HTML que vous avez écrit vous-même, comme ces descriptions. Affichez toujours un texte saisi par un visiteur avec {{ }}. Avec {!! !!}, un <script> qu'il aurait tapé serait exécuté dans le navigateur de chaque personne qui ouvre la page. C'est l'attaque que l'échappement empêchait déjà au chapitre MVC.
@forelse parcourt le tableau comme @foreach, et affiche le bloc placé après @empty quand le tableau est vide, ici pour le premier concert :
curl -s http://premier-test-laravel.test/concerts/1
…
<p>Concert court : 45 minutes.</p>
…
<h2>Invités</h2>
<ul>
<li>Aucun invité annoncé</li>
</ul>
La variable $loop
Dans une boucle @foreach, Blade crée une variable $loop, un objet qui décrit le tour en cours. $loop->iteration donne le numéro du tour, en commençant à 1, et $loop->count le nombre total de tours. $loop->first vaut true au premier tour, et $loop->last au dernier. La variable $loop existe aussi dans une boucle @forelse.
Remplacez le contenu de concerts/index.blade.php :
<h1>Programme</h1>
<ul>
@foreach ($concerts as $id => $concert)
<li>
{{ $loop->iteration }}/{{ $loop->count }}
<a href="{{ route('concerts.show', $id) }}">{{ $concert['artist'] }}</a>
@if ($loop->last)
(clôture)
@endif
</li>
@endforeach
</ul>
curl -s http://premier-test-laravel.test/concerts
<h1>Programme</h1>
<ul>
<li>
1/3
<a href="http://premier-test-laravel.test/concerts/1">Duo Mistral</a>
</li>
…
<li>
3/3
<a href="http://premier-test-laravel.test/concerts/3">Orage Mécanique</a>
(clôture)
</li>
</ul>
L'écriture $id => $concert donne à chaque tour la clé et la valeur, comme en PHP. route('concerts.show', $id) place cette clé dans l'adresse de la fiche, par exemple /concerts/1.
La variable oubliée
Dans show(), remplacez un instant le return par return view('concerts.show');, sans tableau. Ouvrez /concerts/2 : Laravel renvoie une erreur 500 avec ce message :
Undefined variable $concert (View: …/resources/views/concerts/show.blade.php)
Le message nomme le template, mais la cause est dans le contrôleur, qui n'a pas transmis la variable. Remettez le tableau.
4. Ce que Blade fait de votre fichier
{{ }} et @if ne font pas partie du langage PHP : PHP ne peut pas exécuter un template tel quel. La première fois qu'une page a besoin d'un template, Blade le traduit en un fichier PHP ordinaire, appelé vue compilée. Il l'enregistre dans storage/framework/views/, sous un nom de 32 caractères. Voici un extrait de celle de concerts/show.blade.php :
<h1><?php echo e($concert['artist']); ?></h1>
…
<?php if($concert['duration'] >= 90): ?>
…
<p><?php echo e($concert['description']); ?></p>
<p><?php echo $concert['description']; ?></p>
{{ }} devient un echo qui passe par la fonction e(), celle qui transforme les caractères spéciaux du HTML, comme <, > et l'apostrophe, en codes HTML. {!! !!} devient un echo sans e().
Aux requêtes suivantes, Laravel exécute directement la vue compilée. Quand vous modifiez le template, Laravel remarque que le fichier est plus récent que sa vue compilée, et il le traduit de nouveau. Si la page affiche malgré tout l'ancienne version, supprimez toutes les vues compilées avec cette commande :
php artisan view:clear
INFO Compiled views cleared successfully.
5. Un premier composant
Sur la page des infos pratiques, vous voulez mettre certains messages en évidence dans un cadre. Sur la page du programme, chaque concert s'affichera dans un petit bloc, une carte. Vous utiliserez ces deux morceaux de HTML sur plusieurs pages.
Un composant Blade est un morceau de template que vous écrivez une fois et réutilisez dans plusieurs vues. Son fichier se range dans resources/views/components/.
Pour l'afficher dans une vue, vous écrivez une balise qui porte son nom précédé de x-, par exemple <x-alert> pour le fichier alert.blade.php.
Créer le composant et son slot
Pour créer un composant, lancez make:component avec l'option --view. Artisan crée alors un seul fichier, le template du composant :
php artisan make:component alert --view
INFO View [resources/views/components/alert.blade.php] created successfully.
Remplacez le <div> et sa citation par ceci :
<div class="alert">
{{ $slot }}
</div>
Dans festival/info.blade.php, appelez le composant sous le <h1> :
<h1>Infos pratiques</h1>
<x-alert>Les portes ouvrent à 17 h 30.</x-alert>
<main>
<h1>Infos pratiques</h1>
<div class="alert">
Les portes ouvrent à 17 h 30.
</div>
<p>Le festival a lieu au parc communal, du vendredi au dimanche.</p>
</main>
Blade a remplacé la balise <x-alert> par le contenu de components/alert.blade.php, et le texte écrit entre les deux balises est arrivé à la place de {{ $slot }}. Ce contenu s'appelle le slot.
Si vous modifiez alert.blade.php, toutes les pages qui utilisent <x-alert> changent en même temps. Et en lisant <x-alert>Les portes ouvrent à 17 h 30.</x-alert> dans une vue, vous comprenez ce que la page affiche sans ouvrir le composant.
Dans ce cours, un composant est un seul fichier de template, sans classe PHP. Laravel l'appelle composant anonyme, parce qu'aucune classe ne porte son nom.
Les props et leur valeur par défaut
Une alerte qui annonce un concert complet doit avoir un autre aspect qu'une simple information, donc une autre classe CSS. La vue indique au composant de quel type d'alerte il s'agit avec un attribut écrit dans la balise, comme href dans <a href="…">. Une donnée transmise ainsi s'appelle une prop, et le composant la déclare avec la directive @props. Remplacez le contenu de alert.blade.php :
@props(['type' => 'info'])
<div class="alert alert-{{ $type }}">
{{ $slot }}
</div>
'info' est la valeur par défaut de type : Blade l'utilise quand la balise <x-alert> ne contient pas d'attribut type. Ajoutez une seconde alerte dans festival/info.blade.php :
<x-alert>Les portes ouvrent à 17 h 30.</x-alert>
<x-alert type="danger">Le concert de 22 h est complet.</x-alert>
<div class="alert alert-info">
Les portes ouvrent à 17 h 30.
</div>
<div class="alert alert-danger">
Le concert de 22 h est complet.
</div>
Les données que le composant accepte sont maintenant écrites en tête de son fichier : en l'ouvrant, vous voyez ce qu'il faut lui transmettre.
Les deux-points devant un attribut
Sur la page du programme, affichez maintenant chaque concert dans une carte. Créez le composant avec php artisan make:component concert-card --view, puis écrivez ce contenu :
@props(['artist', 'time'])
<article class="concert-card">
<h2>{{ $artist }}</h2>
<p>{{ $time }}</p>
</article>
Le nom de la balise reprend celui du fichier : pour afficher concert-card.blade.php, vous écrivez <x-concert-card>. Dans concerts/index.blade.php, remplacez la liste par un premier essai :
<h1>Programme</h1>
@foreach ($concerts as $id => $concert)
<x-concert-card artist="$concert['artist']" time="$concert['time']" />
@endforeach
<h1>Programme</h1>
<article class="concert-card">
<h2>$concert['artist']</h2>
<p>$concert['time']</p>
</article>
…
Le composant a reçu le texte $concert['artist'] tel quel. Pour passer la valeur d'une expression PHP, écrivez deux-points devant le nom de l'attribut. Corrigez l'appel :
<x-concert-card :artist="$concert['artist']" :time="$concert['time']" />
<h1>Programme</h1>
<article class="concert-card">
<h2>Duo Mistral</h2>
<p>18 h</p>
</article>
<article class="concert-card">
<h2>L'Orchestre du Lac</h2>
<p>20 h</p>
</article>
…
Deux erreurs de composant
Retirez :time="…" de l'appel. La prop time n'a pas de valeur par défaut, et Laravel renvoie une erreur 500 :
Undefined variable $time (View: …/resources/views/components/concert-card.blade.php) (View: …/resources/views/components/concert-card.blade.php)
Le message nomme deux fois le template du composant, parce que c'est lui qui affiche $time. La cause est pourtant dans la page qui l'appelle : concerts/index.blade.php ne transmet plus l'attribut time. Remettez :time="…". Une valeur par défaut dans @props ferait disparaître l'erreur, mais cacherait l'oubli.
Dans la boucle, remplacez ensuite <x-concert-card par <x-card. Blade cherche alors le fichier components/card.blade.php, qui n'existe pas, et Laravel affiche :
Unable to locate a class or view for component [card].
Remettez <x-concert-card.
Passer le tableau entier
Au lieu de transmettre chaque information dans sa propre prop, vous pouvez transmettre tout le tableau du concert dans une seule prop. Le composant choisit ensuite ce qu'il affiche. Remplacez le contenu de concert-card.blade.php et la boucle de concerts/index.blade.php :
@props(['concert'])
<article class="concert-card">
<h2>{{ $concert['artist'] }}</h2>
<p>{{ $concert['stage'] }}, {{ $concert['time'] }}</p>
</article>
<h1>Programme</h1>
@foreach ($concerts as $id => $concert)
<x-concert-card :concert="$concert" />
<a href="{{ route('concerts.show', $id) }}">Voir la fiche</a>
@endforeach
<h1>Programme</h1>
<article class="concert-card">
<h2>Duo Mistral</h2>
<p>Chapiteau, 18 h</p>
</article>
<a href="http://premier-test-laravel.test/concerts/1">Voir la fiche</a>
…
Vous retrouverez ces idées dans le cours Vue.js. Vue est une bibliothèque JavaScript qui construit l'interface d'une page dans le navigateur, et elle découpe elle aussi une page en composants. Un composant Vue déclare ses props avec defineProps, comme @props en Blade. Il reçoit son contenu à l'endroit marqué par la balise <slot />, comme {{ $slot }}. Et les deux-points devant un attribut transmettent là aussi la valeur d'une expression, écrite cette fois en JavaScript.
6. Slots nommés et attributs
Un slot nommé
Un slot nommé est un contenu supplémentaire que la vue transmet au composant dans une balise <x-slot:…>. Blade le place dans une variable du même nom : le contenu de <x-slot:note> devient la variable $note du composant. Ajoutez ces lignes à concert-card.blade.php, sous le paragraphe :
@isset($note)
<p class="note">{{ $note }}</p>
@endisset
@isset n'affiche son bloc que si la variable existe. Dans la boucle de concerts/index.blade.php, donnez une remarque aux concerts de 90 minutes ou plus :
@foreach ($concerts as $id => $concert)
<x-concert-card :concert="$concert">
@if ($concert['duration'] >= 90)
<x-slot:note>Entracte de 15 minutes</x-slot:note>
@endif
</x-concert-card>
<a href="{{ route('concerts.show', $id) }}">Voir la fiche</a>
@endforeach
…
<article class="concert-card">
<h2>L'Orchestre du Lac</h2>
<p>Grande scène, 20 h</p>
<p class="note">Entracte de 15 minutes</p>
</article>
…
Pour les deux autres cartes, $note n'existe pas, et @isset ne produit rien.
Les attributs non déclarés
Blade place dans la variable $attributes tous les attributs de la balise que @props ne déclare pas. Sa méthode merge() les combine avec les attributs que le composant écrit lui-même. Pour l'attribut class, elle écrit les classes du composant, puis celles de la balise, à la suite. Remplacez le contenu de alert.blade.php :
@props(['type' => 'info'])
<div {{ $attributes->merge(['class' => 'alert alert-' . $type]) }}>
{{ $slot }}
</div>
Dans festival/info.blade.php, complétez l'appel de la seconde alerte :
<x-alert type="danger" class="top" id="sold-out">Le concert de 22 h est complet.</x-alert>
<div class="alert alert-danger top" id="sold-out">
Le concert de 22 h est complet.
</div>
La classe top s'ajoute à celles du composant, et id passe tel quel. L'attribut type, déclaré dans @props, n'apparaît pas dans le <div>.
Sans @props, Blade crée quand même une variable pour chaque attribut, mais le garde aussi dans $attributes. Supposez que le composant contienne <div class="alert alert-{{ $type }}" {{ $attributes }}>. Pour la seconde alerte, Blade recopie alors dans le <div> tous les attributs de la balise :
<div class="alert alert-danger" type="danger" class="top" id="sold-out">
Le <div> reçoit un attribut type, qui ne correspond à aucun attribut HTML, et deux attributs class, dont le navigateur ignore le second. Pour la première alerte, écrite sans type, la variable $type n'existe pas, et Laravel affiche l'erreur Undefined variable $type.
Déclarez avec @props chaque donnée de votre composant.
7. Le layout du site
Un layout est le template qui contient ce que les pages d'un site partagent : le <head>, l'en-tête, le menu et le pied de page. Ici, le layout est un composant. Chaque page l'utilise comme une balise qui entoure tout son contenu. Blade place ce contenu dans le $slot du layout, et le titre de la page dans une prop. Créez-le dans un sous-dossier layouts :
php artisan make:component layouts.app --view
INFO View [resources/views/components/layouts/app.blade.php] created successfully.
Remplacez son contenu par le HTML de la section 1. Écrivez {{ $title }} dans la balise <title>, et {{ $slot }} à la place du contenu de <main>. Écrivez aussi l'adresse de chaque lien du menu avec route(), comme au chapitre Routing :
@props(['title' => 'Festival'])
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{{ $title }} · Les Nuits du Parc</title>
</head>
<body>
<header>
<p>Les Nuits du Parc</p>
<nav>
<a href="{{ route('concerts.index') }}">Programme</a>
<a href="{{ route('info') }}">Infos pratiques</a>
<a href="{{ route('concerts.show', 3) }}">Concert de clôture</a>
</nav>
</header>
<main>
{{ $slot }}
</main>
<footer>
<p>Les Nuits du Parc, édition 2026</p>
</footer>
</body>
</html>
Un sous-dossier se note avec un point : pour afficher components/layouts/app.blade.php, vous écrivez <x-layouts.app>. Remplacez tout festival/info.blade.php par ceci :
<x-layouts.app title="Infos pratiques">
<h1>Infos pratiques</h1>
<x-alert>Les portes ouvrent à 17 h 30.</x-alert>
<x-alert type="danger" class="top" id="sold-out">Le concert de 22 h est complet.</x-alert>
<p>Le festival a lieu au parc communal, du vendredi au dimanche.</p>
</x-layouts.app>
curl -s http://premier-test-laravel.test/infos
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Infos pratiques · Les Nuits du Parc</title>
</head>
<body>
<header>
<p>Les Nuits du Parc</p>
<nav>
<a href="http://premier-test-laravel.test/concerts">Programme</a>
<a href="http://premier-test-laravel.test/infos">Infos pratiques</a>
<a href="http://premier-test-laravel.test/concerts/3">Concert de clôture</a>
</nav>
</header>
<main>
<h1>Infos pratiques</h1>
<div class="alert alert-info">
…
<p>Le festival a lieu au parc communal, du vendredi au dimanche.</p>
</main>
<footer>
<p>Les Nuits du Parc, édition 2026</p>
</footer>
</body>
</html>
Blade a placé la valeur de l'attribut title à l'endroit de {{ $title }}, et tout ce qui se trouve entre <x-layouts.app> et </x-layouts.app> à l'endroit de {{ $slot }}. Le navigateur reçoit une page complète, et le fichier de la page ne contient que ce qui lui est propre.

Le titre dans un slot nommé
Entourez de la même façon le contenu de concerts/index.blade.php avec <x-layouts.app title="Programme">. Pour la fiche, le titre mélange un texte et une variable. Vous pourriez écrire l'attribut :title="'Concert de ' . $concert['artist']", mais il mélange des guillemets simples, des guillemets doubles et l'opérateur . de PHP. Avec un slot nommé title, vous écrivez le titre comme dans le reste du template, avec {{ }}, et le résultat est le même. Dans concerts/show.blade.php, entourez le template existant de ces lignes :
<x-layouts.app>
<x-slot:title>Concert de {{ $concert['artist'] }}</x-slot:title>
{{-- le template de la section 3, inchangé --}}
</x-layouts.app>
curl -s http://premier-test-laravel.test/concerts/2 | grep "<title>"
<title>Concert de L'Orchestre du Lac · Les Nuits du Parc</title>
$title contient la valeur de l'attribut ou le contenu du slot nommé. Si la page ne donne ni l'un ni l'autre, Blade utilise la valeur par défaut, et le titre devient Festival · Les Nuits du Parc. Gardez l'attribut pour un titre simple.
Pour renommer le festival, vous modifiez maintenant trois lignes d'un seul fichier, components/layouts/app.blade.php. De même, pour ajouter une feuille de style à tout le site, il suffit d'écrire une balise <link> dans ce seul <head>.
8. L'ancienne façon, et pourquoi préférer les composants
Avant les composants, les layouts s'écrivaient avec quatre directives, que vous croiserez dans des projets existants et des tutoriels. Pour comparer, créez ces trois fichiers : ils écrivent la même page des infos pratiques de cette façon.
Le layout, resources/views/layouts/base.blade.php :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'Festival') · Les Nuits du Parc</title>
</head>
<body>
@include('partials.header')
<main>
@yield('content')
</main>
<footer>
<p>Les Nuits du Parc, édition 2026</p>
</footer>
</body>
</html>
Créez aussi l'en-tête, resources/views/partials/header.blade.php, et copiez-y le <header> du layout de la section 7, sans rien changer. Créez enfin la page, resources/views/legacy/info.blade.php :
@extends('layouts.base')
@section('title', 'Infos pratiques')
@section('content')
<h1>Infos pratiques</h1>
<x-alert>Les portes ouvrent à 17 h 30.</x-alert>
<x-alert type="danger" class="top" id="sold-out">Le concert de 22 h est complet.</x-alert>
<p>Le festival a lieu au parc communal, du vendredi au dimanche.</p>
@endsection
@extends('layouts.base')indique quel layout la page utilise.@section('title', '…')et@section('content') … @endsectiondéfinissent deux blocs de contenu, appeléstitleetcontent.@yield('title', 'Festival')marque, dans le layout, l'endroit où Blade place le bloctitlede la page, ouFestivalsi la page n'en définit pas.@include('partials.header')insère à cet endroit le contenu d'un autre template.
Ajoutez la route Route::view('/ancien/infos', 'legacy.info'); à la fin de routes/web.php. Le HTML reçu est celui de /infos, balise pour balise. Seule l'indentation diffère.
curl -s http://premier-test-laravel.test/ancien/infos
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Infos pratiques · Les Nuits du Parc</title>
</head>
<body>
<header>
<p>Les Nuits du Parc</p>
…
</header>
<main>
<h1>Infos pratiques</h1>
…
</html>
Note
Histoire de Laravel. Jusqu'à Laravel 6, les layouts s'écrivaient avec @extends et @yield. Laravel 7, sorti le 3 mars 2020, a introduit les composants appelés par une balise <x-…>. La documentation de Laravel 13 présente d'abord les layouts en composants, et décrit l'ancienne façon comme « the primary way of building applications prior to the introduction of components », c'est-à-dire la façon principale de construire une application avant l'arrivée des composants. Pour @include, elle précise qu'un composant peut insérer le même template, et qu'il permet en plus de lui transmettre des données et des attributs.
Ce que les composants apportent
@extends fonctionne toujours en Laravel 13. Deux différences font pourtant préférer les composants.
Avec les composants, une faute de nom provoque une erreur. Dans legacy/info.blade.php, écrivez @section('contenu') au lieu de @section('content') :
curl -s -i http://premier-test-laravel.test/ancien/infos
HTTP/1.1 200 OK
…
<main>
</main>
…
La page ne définit plus de bloc content. À l'endroit de @yield('content'), Blade n'insère donc rien, et Laravel renvoie la page avec le code 200, sans aucun message. Remettez content. Avec un composant, une faute dans le nom de la balise, comme <x-layout.app>, provoque au contraire l'erreur Unable to locate a class or view for component [layout.app].
Les données sont déclarées. En lisant <x-layouts.app title="…">, vous voyez quelles données la page transmet. En lisant @props dans le layout, vous voyez lesquelles il utilise. Avec @yield, il faut lire tout le layout pour trouver les noms des blocs.

Lisez @extends dans le code existant. Écrivez des composants dans le vôtre.
Blade et Vue.js côte à côte
Si vous suivez le cours Vue.js, ce tableau met les trois écritures côte à côte. Sinon, lisez seulement les trois premières colonnes.
| Idée | Blade, ancienne façon | Blade, composants | Vue.js |
|---|---|---|---|
| Déclarer les données attendues | aucune déclaration | @props(['concert']) |
defineProps(['concert']) |
| Passer une expression | le layout voit les variables de la page | :concert="$concert" (PHP) |
:concert="concert" (JavaScript) |
| Zone de contenu principal | @yield('content') |
{{ $slot }} |
<slot /> |
| Zone nommée | @yield('title') et @section('title') |
{{ $title }} et <x-slot:title> |
<slot name="title" /> et <template #title> |
| Attributs non déclarés | aucun | {{ $attributes }}, à écrire soi-même |
ajoutés automatiquement à la balise extérieure du composant |
La comparaison a deux limites. Blade exécute un composant une seule fois, sur le serveur, et le navigateur reçoit du HTML qui ne change plus. Un composant Vue, lui, tourne dans le navigateur : il peut mettre la page à jour quand une donnée change, ou réagir à un clic avec @click. Ensuite, une prop Blade sans valeur par défaut que vous oubliez de transmettre arrête la page avec l'erreur Undefined variable. En Vue, la même prop vaut undefined, et la page s'affiche quand même.
9. Un formulaire et @csrf
Au chapitre Routing, une requête POST envoyée avec curl recevait l'erreur 419. Vous écrivez maintenant un vrai formulaire : un visiteur qui veut devenir bénévole envoie son prénom en POST. Une seule méthode suffit pour lui répondre, vous la placez donc dans un contrôleur à action unique, comme au chapitre Controllers :
php artisan make:controller VolunteerController --invokable
INFO Controller [app/Http/Controllers/VolunteerController.php] created successfully.
Remplacez le contenu du fichier. Au chapitre Controllers, $request->query() lisait un paramètre d'URL. La méthode input() de Request lit de la même façon un champ envoyé par un formulaire :
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class VolunteerController extends Controller
{
public function __invoke(Request $request): string
{
$firstName = $request->input('first_name');
return "Merci $firstName, votre candidature de bénévole est enregistrée.";
}
}
Créez la vue avec php artisan make:view volunteers.form et écrivez le formulaire, pour l'instant sans @csrf :
<x-layouts.app title="Devenir bénévole">
<h1>Devenir bénévole</h1>
<form method="POST" action="{{ route('volunteers.store') }}">
<label>Prénom <input type="text" name="first_name"></label>
<button type="submit">Envoyer</button>
</form>
</x-layouts.app>
Importez VolunteerController au-dessus de WeatherController, puis ajoutez les deux routes à la fin de routes/web.php :
Route::view('/benevoles', 'volunteers.form')->name('volunteers.create');
Route::post('/benevoles', VolunteerController::class)->name('volunteers.store');
Ouvrez http://premier-test-laravel.test/benevoles, tapez Lina et cliquez sur « Envoyer ». Le navigateur affiche Page Expired, avec le code 419. C'est la protection contre la CSRF, vue au chapitre Routing : Laravel refuse une requête POST s'il ne peut pas vérifier qu'elle vient d'une page de votre site.
Au chapitre Routing, vous avez vu que Laravel regarde d'abord l'en-tête Sec-Fetch-Site, que les navigateurs n'envoient que sur une connexion HTTPS. Votre projet est servi en HTTP : Laravel cherche alors un jeton dans le formulaire, et il n'en trouve pas.
La directive @csrf ajoute ce jeton au formulaire. Écrivez-la juste sous la balise <form> :
<form method="POST" action="{{ route('volunteers.store') }}">
@csrf
<label>Prénom <input type="text" name="first_name"></label>
curl -s http://premier-test-laravel.test/benevoles | grep _token
<input type="hidden" name="_token" value="6HjTJozqH7XAcXVJCTyVptQjZ9lRXU2dZV2gkYri" autocomplete="off"> <label>Prénom <input type="text" name="first_name"></label>
Chez vous, ce champ caché contient une autre suite de 40 caractères. Laravel garde une copie de ce jeton dans la session du visiteur, un espace de stockage que le serveur réserve à chaque visiteur. Quand le formulaire est envoyé, Laravel compare le jeton reçu avec celui de la session.
Pour savoir quelle session appartient à quel navigateur, Laravel envoie un cookie, une petite valeur que le navigateur conserve et renvoie ensuite à chaque requête vers votre site. Dans votre projet, ce cookie s'appelle premier-test-session, d'après le nom de l'application écrit dans .env.
Rechargez le formulaire, tapez de nouveau Lina et envoyez-le. Cette fois, Laravel accepte la requête, et le navigateur affiche la phrase renvoyée par le contrôleur :
Merci Lina, votre candidature de bénévole est enregistrée.
Une page piégée sur un autre site ne peut pas lire ce jeton. Le formulaire qu'elle envoie arrive sans lui, et Laravel le refuse avec le code 419. Écrivez @csrf dans chaque formulaire POST. Pour l'instant, le contrôleur ne vérifie pas le contenu des champs.
Ajoutez enfin le lien dans le menu, une ligne dans components/layouts/app.blade.php, et toutes les pages qui utilisent ce layout l'affichent :
<a href="{{ route('volunteers.create') }}">Devenir bénévole</a>
À retenir
- Un contrôleur passe les données à la vue avec un tableau, et
@if,@foreach,$loopet@forelseles affichent sans balise<?phpdans la vue. {{ }}échappe le texte avec la fonctione(), alors que{!! !!}l'affiche tel quel et ne sert qu'au HTML que vous avez écrit vous-même.- Un composant anonyme est un fichier de
resources/views/components/qui déclare ses props avec@propset reçoit son contenu dans$slot. - Un attribut précédé de deux-points transmet une expression PHP, et un attribut sans deux-points transmet un texte fixe.
- Le layout d'un site est un composant,
<x-layouts.app>, qui remplace@extends,@section,@yieldet@includedans un projet neuf.
Exercices
Comptez 115 minutes pour les exercices, en séance ou à la maison.
Faites les exercices 1 à 8 dans premier-test-laravel, pour Octet, une entreprise d'informatique inventée. Ajoutez chaque route à la fin de routes/web.php et chaque import en haut du fichier. Supprimez la citation de chaque fichier créé par make:view ou make:component.
1. Une page d'accueil (10 minutes)
Créez la vue home avec make:view, avec un titre « Bienvenue chez Octet » et un paragraphe. Affichez-la à l'adresse /accueil, route nommée home, sans contrôleur. Inspirez-vous de la section 1.
2. L'entreprise (10 minutes)
Créez CompanyController, dont la méthode index() passe ce tableau à la vue company, sous le nom info. La vue affiche une ligne par information, la clé puis la valeur. Reliez-la à l'adresse GET /entreprise, avec une route nommée company. Reprenez le $id => $concert de la section 3.
$info = [
'Nom' => 'Octet',
'Année de création' => 2012,
'Mission' => 'Réparer plutôt que remplacer',
];
<h1>L'entreprise</h1>
<ul>
<li>Nom : Octet</li>
<li>Année de création : 2012</li>
<li>Mission : Réparer plutôt que remplacer</li>
</ul>
3. Les membres du club (15 minutes)
Créez MemberController avec cette propriété. Sa méthode index(), sur GET /membres nommée members.index, passe les membres à la vue members. Chaque ligne affiche le numéro donné par $loop, le prénom, puis l'âge si le membre a 18 ans ou plus, et sinon le mot « mineur ». Une méthode empty(), sur GET /membres/vide, passe un tableau vide à la même vue, qui affiche « Aucun membre ». Inspirez-vous de la section 3.
private array $members = [
['first_name' => 'Alice', 'age' => 29],
['first_name' => 'Bob', 'age' => 34],
['first_name' => 'Chloé', 'age' => 17],
['first_name' => 'Damien', 'age' => 42],
];
4. Des cartes de services (10 minutes)
Créez le composant anonyme card, avec une prop title affichée dans un <h2> et le slot dans un <p>, le tout dans un <div class="card">. Affichez trois cartes, Design, Développement et Marketing, dans la vue services, reliée à /services par Route::view. La carte Développement reçoit en plus la classe featured. Inspirez-vous des sections 5 et 6.
<div class="card featured">
<h2>Développement</h2>
<p>Un site Laravel sur mesure.</p>
</div>
5. Les produits et leur fiche (20 minutes)
Créez ProductController avec cette propriété, et deux routes : GET /produits, nommée products.index, et GET /produits/{id}, nommée products.show. index() passe les produits à la vue products.index, qui affiche pour chacun un lien vers sa fiche écrit avec route(), son prix, et « Promo ! » s'il est en promotion. show() affiche la vue products.show d'un produit, et renvoie une page 404 si le numéro n'existe pas. Inspirez-vous de ConcertController, à la section 3.
private array $products = [
1 => ['name' => 'PC portable', 'price' => 1200, 'on_sale' => true],
2 => ['name' => 'Souris sans fil', 'price' => 80, 'on_sale' => false],
3 => ['name' => 'Clavier mécanique', 'price' => 150, 'on_sale' => true],
4 => ['name' => 'Casque audio', 'price' => 100, 'on_sale' => false],
];
curl -s http://premier-test-laravel.test/produits/3
curl -s -i http://premier-test-laravel.test/produits/9
6. Un composant produit (10 minutes)
Créez le composant anonyme product, qui reçoit un produit entier dans une prop product et affiche son nom, son prix et « Promo ! » s'il y a lieu. Utilisez-le dans la boucle de products/index.blade.php, en gardant le lien. Inspirez-vous de la carte du concert, à la fin de la section 5.
7. Le layout d'Octet (15 minutes)
Créez components/layouts/company.blade.php, avec une prop title qui vaut Octet par défaut et qui forme seule le contenu de <title>, un menu de trois liens écrits avec route() vers l'accueil, l'entreprise et les produits, {{ $slot }} dans <main> et un pied de page. Appliquez-le aux pages des exercices 1, 2, 3 et 5, chacune avec son titre. Sur la fiche d'un produit, le titre est le nom du produit. Inspirez-vous de la section 7.
curl -s http://premier-test-laravel.test/produits/3 | grep "<title>"
<title>Clavier mécanique</title>
8. Une newsletter (15 minutes)
Créez une page /newsletter, affichée par Route::view et nommée newsletter.create, avec un formulaire qui envoie un champ email en POST vers la route newsletter.store. Celle-ci appelle un contrôleur à action unique NewsletterController, qui renvoie « Inscription à la newsletter enregistrée pour » suivi de l'adresse. Envoyez le formulaire dans le navigateur sans @csrf, puis avec, et notez le code obtenu. Inspirez-vous de la section 9. Avec @csrf, la réponse est :
Inscription à la newsletter enregistrée pour lina@example.be.
9. Lire et diagnostiquer (sur papier, 10 minutes)
La vue reçoit $text = '<b>Promo</b>', $name = 'Design' et $empty = [], et card est le composant de l'exercice 4. Écrivez le HTML produit par chaque extrait, le <h2> seulement pour 3 et 4.
{{-- 1 --}} <p>{{ $text }}</p>
{{-- 2 --}} <p>{!! $text !!}</p>
{{-- 3 --}} <x-card title="$name">Texte</x-card>
{{-- 4 --}} <x-card :title="$name">Texte</x-card>
{{-- 5 --}} {{-- Réduction --}} <!-- Promo --> <p>Prix</p>
{{-- 6 --}} @forelse ($empty as $x) <p>{{ $x }}</p> @empty <p>Rien à afficher</p> @endforelse
Pour chaque message, écrivez la cause et le fichier à corriger.
View [products.list] not found.
Undefined variable $product (View: …/resources/views/products/show.blade.php)
Unable to locate a class or view for component [products].