Retour au cours

backend / php-laravel

Blade : templates et composants

Leçon 141 exercice

Explication

Ce que vous allez apprendre

  • Structurer un layout partagé avec @extends, @section et @yield
  • Comprendre pourquoi {{ }} échappe automatiquement le HTML, et quand {!! !!} devient dangereux
  • Créer des composants Blade réutilisables avec des props typées via @props
  • Utiliser les variables spéciales $loop dans une boucle @foreach
  • Injecter du contenu paramétré dans un composant grâce aux slots

Dans quel contexte ?

Une équipe frontend Laravel doit afficher une grille de produits identique sur plusieurs pages (accueil, résultats de recherche, page catégorie), avec un badge visuel différent selon que le produit est en rupture de stock ou non. Plutôt que de dupliquer le HTML de la carte produit à chaque endroit, un composant Blade réutilisable centralise cette logique d'affichage en un seul fichier.

D'abord, un système d'héritage de layout simple mais puissant

Un layout de base (layouts/app.blade.php) définit la structure commune à toutes les pages, avec des zones ouvertes via @yield('contenu'). Chaque vue enfant hérite de ce layout avec @extends('layouts.app') puis remplit ces zones avec @section('contenu') ... @endsection.

Cette approche évite de dupliquer le <head>, la navigation et le pied de page sur chaque vue de l'application — un seul endroit à modifier si le layout global doit changer.

Prérequis

Cette leçon suppose que les contrôleurs et la validation (leçons 9 et 13) sont acquis : Blade affiche généralement des données déjà préparées par un contrôleur.

Ensuite, une protection de sécurité activée par défaut

{{ $variable }} échappe automatiquement tout caractère HTML spécial avant affichage, ce qui protège contre les attaques XSS (injection de scripts malveillants) si la donnée provient d'un utilisateur. {!! $variable !!}, à l'inverse, affiche le contenu brut sans échappement.

Piège dangereux

Utiliser {!! $commentaire !!} pour afficher un commentaire saisi par un utilisateur ouvre directement la porte à une injection XSS stockée : un attaquant pourrait soumettre <script> dans son commentaire, exécuté ensuite dans le navigateur de chaque visiteur qui consulte la page. Réserve {!! !!} exclusivement à du contenu explicitement nettoyé et de confiance.

Il reste à éviter la duplication de blocs HTML répétitifs

Un simple @include('partials.navigation') réutilise un fragment de vue, mais ne permet pas facilement de recevoir des données paramétrées de façon structurée. Les composants Blade (<x-produit-carte>) vont plus loin : ils déclarent des @props typées, se comportent comme de vrais éléments HTML personnalisés, et acceptent du contenu injecté via des slots.

MécanismeReçoit des données ?Recommandé pour
@includeVariables du contexte courantFragments simples sans logique
Composant Blade <x-nom>Props explicites (@props)Éléments réutilisables avec logique
Slot ($slot)Contenu HTML arbitraireContenu variable injecté par l'appelant

Maintenant, un détail qui simplifie beaucoup l'affichage de listes

Dans une boucle @foreach, la variable spéciale $loop donne accès à $loop->first, $loop->last, $loop->iteration sans avoir à gérer un compteur manuel. Utile pour styliser différemment le premier ou le dernier élément d'une liste, ou afficher une numérotation.

Astuce

$attributes->merge(['class' => 'carte-produit']) dans un composant permet à l'appelant d'ajouter ses propres classes CSS sans écraser celles définies par défaut dans le composant — un pattern à connaître pour des composants vraiment réutilisables.

Maintenant que l'affichage est maîtrisé, il reste une question transversale : comment intercepter une requête avant ou après qu'elle atteigne le contrôleur, par exemple pour vérifier une authentification ? C'est exactement le rôle des middlewares, sujet de la prochaine leçon.

Commandes & code

Blade : templates et composants

blade
{{-- resources/views/layouts/app.blade.php : layout de base --}}
<!DOCTYPE html>
<html lang="fr">
<head>
    <title>@yield('titre', 'Boutique')</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <nav>@include('partials.navigation')</nav>

    <main>
        @yield('contenu')
    </main>

    @stack('scripts') {{-- permet aux vues filles d'injecter du JS spécifique --}}
</body>
</html>
blade
{{-- resources/views/produits/index.blade.php --}}
@extends('layouts.app')

@section('titre', 'Nos produits')

@section('contenu')
    <h1>Produits disponibles</h1>

    {{-- {{ }} échappe automatiquement le HTML (protection XSS) --}}
    <p>{{ $produits->total() }} résultats</p>

    @if ($produits->isEmpty())
        <p>Aucun produit trouvé.</p>
    @else
        <ul>
            @foreach ($produits as $produit)
                <li class="@if($produit->stock === 0) epuise @endif">
                    {{-- x-composant : composant Blade réutilisable, voir plus bas --}}
                    <x-produit-carte :produit="$produit" />
                </li>
            @endforeach
        </ul>

        {{-- pagination automatique avec les liens Bootstrap/Tailwind --}}
        {{ $produits->links() }}
    @endif

    @auth
        <p>Connecté en tant que {{ auth()->user()->nom }}</p>
    @else
        <a href="{{ route('login') }}">Se connecter</a>
    @endauth
@endsection

@push('scripts')
    <script src="{{ asset('js/produits.js') }}"></script>
@endpush
blade
{{-- resources/views/components/produit-carte.blade.php --}}
{{-- Composant Blade avec props typées via la classe correspondante --}}
@props(['produit', 'compact' => false])

<div {{ $attributes->merge(['class' => 'carte-produit']) }}>
    <h3>{{ $produit->nom_affiche }}</h3>
    <p>{{ number_format($produit->prix, 2) }} €</p>

    @unless($compact)
        <p>{{ Str::limit($produit->description, 100) }}</p>
    @endunless

    {{ $slot }} {{-- contenu injecté entre les balises ouvrante/fermante du composant --}}
</div>
blade
{{-- Utilisation du composant avec un slot --}}
<x-produit-carte :produit="$produit" :compact="true">
    <button wire:click="ajouterAuPanier({{ $produit->id }})">Ajouter au panier</button>
</x-produit-carte>

{{-- Boucle avec variables spéciales $loop --}}
@foreach ($categories as $categorie)
    <div class="{{ $loop->first ? 'premiere' : '' }} {{ $loop->last ? 'derniere' : '' }}">
        {{ $loop->iteration }}/{{ $loop->count }} — {{ $categorie->nom }}
    </div>
@endforeach

Résumé

  • {{ }} échappe le HTML par défaut (protection XSS) ; {!! !!} ne le fait pas (à éviter sauf contenu de confiance).
  • @extends / @section / @yield structurent l'héritage de layout.
  • Les composants Blade (<x-nom-composant>) avec @props remplacent avantageusement les @include paramétrés.
  • $loop->first, $loop->last, $loop->iteration simplifient les logiques de boucle dans les vues.

Exercices pratiques

1 disponible
1

Mission : colmater une faille XSS dans les commentaires produits

Objectif : Corriger une vue Blade qui expose une faille XSS et remplacer un fragment duplique par un composant reutilisable.

Contexte

La page produit affiche les commentaires des clients avec {!! $commentaire->texte !!}, decision prise a l'origine pour permettre aux clients de mettre du texte en gras avec des balises HTML simples. Un client a recemment soumis un commentaire contenant une balise <script> qui s'execute desormais dans le navigateur de chaque visiteur de la page produit. Par ailleurs, la carte produit (nom, prix, description tronquee) est copiee-collee dans trois vues differentes (accueil, recherche, categorie), avec de petites divergences qui commencent a apparaitre entre les trois copies.

Résoudre l’exercice →