Aller au contenu principal

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

  1. Toujours vérifier les requêtes (Debugbar, Laravel Telescope) et utiliser with() dès qu’une relation est utilisée en boucle.
  2. Cache : définir une durée et une stratégie d’invalidation (suppression de clé à la mise à jour).
  3. 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).
  4. 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).