Module 19 – Performance & optimisation
Niveau 4 – Laravel avancé (PRO)
Objectifs
Au programme :
- Éviter le problème N+1 avec l’eager loading (with, load)
- Utiliser le cache (Cache facade, cache de requêtes, cache de vues)
- Mettre en place des queues et des jobs pour les tâches longues
- Utiliser Events et Listeners pour découpler la logique métier
- Choisir la bonne stratégie selon le contexte (requêtes lourdes, envoi d’emails, logs)
Théorie
Eager loading (éviter le N+1)
Problème N+1 : dans une boucle, on charge un modèle (ex. Article) puis on accède à une relation (ex. $article->user). Laravel exécute alors une requête par article pour charger l’utilisateur → 1 + N requêtes (N = nombre d’articles).
Solution : charger les relations en une fois avec with() (ou load() sur une collection déjà chargée).
// Mauvais : N+1
$articles = Article::all();
foreach ($articles as $article) {
echo $article->user->name; // 1 requête par article
}
// Bon : 2 requêtes au total
$articles = Article::with('user')->get();
foreach ($articles as $article) {
echo $article->user->name;
}
Relations imbriquées :
Article::with('user', 'tags')->get();
Article::with('user.profile')->get();
load() : si vous avez déjà une collection, vous pouvez charger a posteriori :
$articles->load('user');
withCount(), withSum(), etc. : pour des agrégats sans charger les modèles liés.
User::withCount('articles')->get();
// $user->articles_count
Cache
Laravel fournit la facade Cache avec des drivers (file, redis, memcached, database). Config dans config/cache.php.
Méthodes de base :
use Illuminate\Support\Facades\Cache;
Cache::put('cle', $valeur, $secondes); // ou now()->addMinutes(10)
$v = Cache::get('cle');
$v = Cache::get('cle', 'defaut');
Cache::forget('cle');
Cache::remember('cle', $secondes, function () {
return Article::all(); // exécuté seulement si la clé est absente
});
Cache de requêtes (pour des résultats de requêtes coûteuses) :
$articles = Cache::remember('articles_populaires', 3600, function () {
return Article::orderBy('views', 'desc')->limit(10)->get();
});
Cache de vues (Blade compilé) : en production, exécuter php artisan view:cache pour pré-compiler les vues (réduit la charge au runtime).
Cache de config : php artisan config:cache (évite de relire tous les fichiers de config à chaque requête). En dev, utiliser php artisan config:clear pour voir les changements.
Tags (Redis/Memcached) : invalider un groupe de clés.
Cache::tags(['articles'])->put('liste', $data, 3600);
Cache::tags(['articles'])->flush();
Queues et Jobs
Les queues permettent d’exécuter des tâches en arrière-plan (envoi d’email, génération de PDF, appel API externe) au lieu de bloquer la réponse HTTP.
Driver : config config/queue.php (database, redis, sync pour exécution immédiate en dev). Pour la queue « database », créer la table : php artisan queue:table puis migrate. Lancer le worker : php artisan queue:work.
Créer un job :
php artisan make:job SendOrderNotification
SendOrderNotification.php :
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendOrderNotification implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Order $order,
) {}
public function handle(): void
{
Mail::to($this->order->user)->send(new OrderShipped($this->order));
}
}
Dispatch (mise en file) :
SendOrderNotification::dispatch($order);
SendOrderNotification::dispatch($order)->delay(now()->addMinutes(5));
SendOrderNotification::dispatch($order)->onQueue('emails');
Worker : php artisan queue:work (ou queue:work --queue=emails). En production : Supervisor ou systemd pour garder le worker actif.
Échec : les jobs en échec peuvent être relus avec queue:failed et relancés avec queue:retry.
Events et Listeners
Events = quelque chose s’est passé (OrderPlaced, UserRegistered). Listeners = réactions à cet événement (envoyer un email, mettre à jour des stats, notifier un service). Cela découple la logique : le contrôleur ou le service qui place la commande n’a pas besoin de connaître tous les side-effects.
Créer un event et des listeners :
php artisan make:event OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced
OrderPlaced.php :
class OrderPlaced
{
public function __construct(public Order $order) {}
}
SendOrderConfirmation.php :
class SendOrderConfirmation
{
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->user)->send(new OrderConfirmation($event->order));
}
}
Enregistrement : dans EventServiceProvider (ou avec attributs PHP 8), lier l’event aux listeners.
Déclencher l’event : event(new OrderPlaced($order)); ou OrderPlaced::dispatch($order);.
Queued listeners : si un listener implémente ShouldQueue, il sera exécuté en file d’attente (comme un job).
Exemples
Eager loading avec contrainte :
Article::with(['user' => fn ($q) => $q->select('id', 'name')])->get();
Cache remember avec invalidation :
Cache::remember("article.{$id}", 600, fn () => Article::with('user')->findOrFail($id));
// Lors d’une mise à jour de l’article : Cache::forget("article.{$id}");
Job avec tentatives et délai :
public $tries = 3;
public $backoff = [60, 300, 900];
Bonnes pratiques
- Toujours vérifier les requêtes (Debugbar, Laravel Telescope) et utiliser with() dès qu’une relation est utilisée en boucle.
- Cache : définir une durée et une stratégie d’invalidation (suppression de clé à la mise à jour).
- Jobs : pour tout ce qui dépasse ~100 ms (emails, exports, appels externes) ; garder les jobs idempotents si possible (relançables sans effet de bord).
- Events : pour découpler « ce qui s’est passé » de « ce qu’on fait après » ; éviter de mettre trop de logique dans un listener (déléguer à un service).
Quiz – Module 19
Q1. Qu’est-ce que le problème N+1 et comment le résoudre ?
Q2. À quoi sert Cache::remember() ?
Q3. Pourquoi utiliser une queue (job) au lieu d’exécuter une tâche dans le contrôleur ?
Q4. Quelle commande lance le worker qui exécute les jobs en file ?
Q5. Quelle est la différence entre un Event et un Listener ?
Réponses
R1. C’est l’exécution de 1 + N requêtes quand on accède à une relation dans une boucle (une requête par élément). On le résout en chargeant la relation en avance avec with('relation') (eager loading).
R2. À récupérer une valeur depuis le cache, ou à la calculer et la mettre en cache si la clé n’existe pas (callback exécuté uniquement en cas de miss). Évite de refaire un calcul coûteux à chaque requête.
R3. Pour ne pas bloquer la réponse HTTP : les tâches longues (emails, exports, API externes) sont mises en file et exécutées par un worker en arrière-plan ; l’utilisateur reçoit une réponse rapide.
R4. php artisan queue:work (le worker lit la queue et exécute les jobs).
R5. Un Event représente « quelque chose s’est passé » (objet dispatché). Un Listener est une classe qui réagit à cet event (une ou plusieurs peuvent écouter le même event). Cela découple l’émetteur des réactions.
Suite
Niveau 5 – Laravel expert : Module 20 – Architecture avancée (Services, Repositories, DTO, Clean Architecture, DDD).