Skip to Content
SymfonyPartie 2 — PHP moderne pour dev JS/TS2.7 — Erreurs, générateurs, Fibers

Chapitre 2.7 — Erreurs, générateurs & Fibers

Partie 2 : PHP moderne — Chapitre 7/8 Version de référence : PHP 8.4 pour Symfony 7.4.

⏱️ TL;DR

  • Les erreurs se gèrent par exceptions : try / catch / finally, throw. Tout ce qui se lance implémente \Throwable, avec deux branches : Error (erreurs moteur : TypeError, DivisionByZeroError) et Exception (erreurs applicatives).
  • throw est une expression (8.0) : $x = $a ?? throw new \RuntimeException();.
  • Un générateur (yield) produit des valeurs paresseusement, une à une, sans tout charger en mémoire — parfait pour parcourir des millions de lignes.
  • Fibers (8.1) est la primitive de concurrence coopérative de PHP. Vous n’en écrirez presque jamais directement : PHP reste synchrone par requête, et la concurrence « réelle » passe par Messenger/workers (Partie 12), pas par async/await.
  • Pour un dev Next.js : oubliez async/await — en PHP, une requête = du code synchrone de haut en bas. Le travail « en arrière-plan » se délègue à des workers, pas à une event loop.

🎯 Objectifs

  • Gérer les erreurs avec try/catch/finally et créer ses exceptions métier.
  • Distinguer Error et Exception (tous deux Throwable).
  • Écrire et consommer un générateur (yield) et comprendre son intérêt mémoire.
  • Comprendre pourquoi PHP n’a pas d’async/await et ce qui joue ce rôle (workers).

1. Les exceptions

1.1 try / catch / finally

try { $booking = $this->bookingService->confirm($id); } catch (BookingNotFoundException $e) { // erreur spécifique d'abord return new Response('Réservation introuvable', 404); } catch (\Throwable $e) { // filet général en dernier $this->logger->error($e->getMessage()); return new Response('Erreur interne', 500); } finally { // toujours exécuté (succès comme échec) : nettoyage, libération… $this->lock->release(); }

L’ordre des catch compte : du plus spécifique au plus général. finally s’exécute dans tous les cas.

1.2 throw, y compris comme expression

throw new \InvalidArgumentException('Le créneau est complet.'); // depuis PHP 8.0, throw est une EXPRESSION → utilisable dans ?? ou ?: $email = $data['email'] ?? throw new \InvalidArgumentException('Email requis'); $status = BookingStatus::tryFrom($raw) ?? throw new \DomainException('Statut invalide');

1.3 La hiérarchie Throwable

  • Error : défaillances au niveau du moteurTypeError (mauvais type), DivisionByZeroError, UnhandledMatchError (un match sans cas correspondant). En général on ne les rattrape pas (ce sont des bugs à corriger).
  • Exception : erreurs applicatives prévues — dont RuntimeException (à l’exécution) et LogicException (erreur de logique/programmation). C’est de celles-ci qu’on hérite pour ses exceptions métier.
  • Attraper \Throwable capture les deux (à réserver au filet général en dernier recours).

⚠️ Piège : catch (\Exception $e) ne capture pas les Error (comme TypeError). Si vous voulez un filet vraiment attrape-tout (par ex. pour logger avant de renvoyer une 500), attrapez \Throwable, pas \Exception. Beaucoup de bugs « invisibles » viennent d’un catch (\Exception) qui laisse passer un TypeError.

1.4 Vos exceptions métier

Créez des exceptions nommées par intention — elles rendent le catch lisible et permettent à Symfony d’y associer un code HTTP :

declare(strict_types=1); namespace App\Exception; final class SlotFullException extends \RuntimeException { public static function forSlot(int $slotId): self { return new self(sprintf('Le créneau #%d est complet.', $slotId)); } } // usage throw SlotFullException::forSlot($slot->getId());

💡 Pour un dev Next.js : try/catch/finally et throw sont identiques à TS. La grande différence : la hiérarchie Error vs Exception (JS n’a qu’Error). Et le throw comme expression dans ?? est plus expressif que ce que permet JS. Le pattern « exception métier nommée » vaut aussi en TS, mais PHP/Symfony l’exploite à fond (mapping automatique vers des réponses HTTP, Partie 4).

2. Les générateurs (yield)

Un générateur est une fonction qui, au lieu de tout calculer d’un coup et de renvoyer un array, produit ses valeurs une par une, à la demande, avec yield. Elle ne garde en mémoire qu’une valeur à la fois.

/** @return \Generator<int, string> */ function readLines(string $path): \Generator { $handle = fopen($path, 'r'); try { while (($line = fgets($handle)) !== false) { yield rtrim($line); // ← produit une ligne, puis « pause » } } finally { fclose($handle); } } // Consommation : on itère, une ligne chargée à la fois foreach (readLines('bookings.csv') as $line) { // traiter $line — même si le fichier fait 10 Go, la mémoire reste minime }

Sans générateur, file('bookings.csv') chargerait tout le fichier en mémoire. Avec yield, on traite un flux paresseusement. C’est crucial pour les imports massifs, les exports, la pagination de gros jeux de données — des cas très concrets côté Symfony (commandes de traitement par lot, Partie 12).

Autres formes utiles :

yield $key => $value; // produire avec une clé yield from $autreIterable; // déléguer à un autre générateur/iterable

💡 Pour un dev Next.js : c’est exactement le function* / yield de JS (les generators). Même intérêt : itération paresseuse, mémoire constante. Le type iterable (vu au chapitre 2.2) accepte aussi bien un array qu’un \Generator, ce qui permet d’écrire des fonctions agnostiques.

3. Fibers, et l’absence d’async/await

C’est le point qui déroute le plus un dev JS. PHP n’a pas d’async/await, et n’en a pas besoin dans 99 % des cas. Voici pourquoi.

3.1 Le modèle d’exécution de PHP

Une requête HTTP en PHP s’exécute de façon synchrone, de haut en bas, puis le processus meurt (ou, en mode worker, se réinitialise). Il n’y a pas d’event loop persistante par défaut. Un appel « bloquant » (base de données, HTTP sortant) attend son résultat, tout simplement — et comme chaque requête a son propre processus/worker, le serveur traite plusieurs requêtes en parallèle au niveau… des processus, pas de l’async intra-requête.

Conséquence : vous écrivez du code linéaire, sans await. Pas de callback hell, pas de couleur de fonction (« async » qui contamine tout l’appel). Le code est plus simple à lire et à raisonner.

3.2 Alors, à quoi servent les Fibers ?

Fibers (8.1) fournit une primitive de concurrence coopérative : une fonction peut se suspendre (Fiber::suspend()) et être reprise plus tard. C’est le socle de bas niveau qu’utilisent des bibliothèques comme ReactPHP, Amp/Revolt pour offrir de l’asynchrone quand on en a vraiment besoin (serveurs temps réel, clients HTTP massivement concurrents). Vous n’écrirez quasiment jamais de Fiber à la main — c’est de la plomberie de framework.

3.3 Comment fait-on « du travail en arrière-plan » ?

Le besoin réel (« envoyer un e-mail sans faire attendre l’utilisateur », « traiter un webhook », « générer un PDF lourd ») ne se résout pas par async/await en PHP, mais par la délégation à des workers via Messenger (Partie 12) : le contrôleur poste un message dans une file et rend la réponse immédiatement ; un processus worker séparé consomme la file et fait le travail.

// Dans le contrôleur : on délègue, on n'attend pas $this->messageBus->dispatch(new SendBookingConfirmation($booking->getId())); return new Response('OK'); // renvoyé tout de suite ; l'e-mail part en arrière-plan

⚠️ Piège du dev JS : ne cherchez pas à « rendre asynchrone » un contrôleur Symfony avec des promesses. Le bon réflexe est Messenger : tout ce qui est lent ou différable devient un message traité par un worker. C’est plus robuste que l’async intra-requête (reprise sur échec, montée en charge, priorités) — on le construit en détail pour BookLab en Partie 12.

📚 Aller plus loin : si un jour vous avez un besoin réellement asynchrone (un serveur SSE maison, un scraper massivement concurrent), regardez Amp et ReactPHP — mais c’est l’exception, pas la règle.

✏️ Exercices

Exercice 1. Pourquoi ce filet d’erreur laisse-t-il « fuir » certains bugs, et comment le corriger ?

try { $this->process($data); } catch (\Exception $e) { $this->logger->error($e->getMessage()); }

✅ Solution

catch (\Exception $e) ne capture pas les Error (ex. TypeError, DivisionByZeroError, UnhandledMatchError), qui héritent de Error, pas d’Exception. Ces bugs remontent donc sans être loggés. Pour un filet attrape-tout, capturez \Throwable :

} catch (\Throwable $e) { $this->logger->error($e->getMessage()); }

Exercice 2. Écrivez un générateur range10k() qui produit les entiers de 1 à 10000 sans construire de tableau, et expliquez le gain.

✅ Solution

/** @return \Generator<int, int> */ function range10k(): \Generator { for ($i = 1; $i <= 10000; $i++) { yield $i; } } foreach (range10k() as $n) { /* … */ }

Gain : aucun tableau de 10000 entiers n’est alloué en mémoire ; chaque valeur est produite à la demande et « oubliée » après usage. Sur de gros volumes (fichiers, résultats de base), la mémoire reste constante au lieu de croître avec la taille des données.

Exercice 3. Un dev JS veut « envoyer l’e-mail de confirmation en await dans le contrôleur pour ne pas bloquer ». Expliquez le bon modèle en PHP/Symfony.

✅ Solution

En PHP, le contrôleur est synchrone : il n’y a pas d’await qui « libère » le thread. Le bon modèle est la délégation à un worker via Messenger : le contrôleur dispatch un message (SendBookingConfirmation) et renvoie immédiatement la réponse ; un processus worker distinct consomme la file et envoie l’e-mail en arrière-plan. On gagne en robustesse (retries, priorités, montée en charge) par rapport à un async intra-requête. (Détails en Partie 12.)

🧠 Quiz de révision

1. Quelle interface implémentent toutes les erreurs lançables, et quelles sont ses deux branches ?

\Throwable, avec Error (défaillances moteur : TypeError…) et Exception (erreurs applicatives).

2. catch (\Exception $e) attrape-t-il un TypeError ?

Non : TypeError hérite d’Error, pas d’Exception. Il faut catch (\Throwable $e) pour l’attraper.

3. Quel est l’intérêt principal d’un générateur ?

Produire des valeurs paresseusement, une à la fois, avec une mémoire constante — idéal pour de gros volumes (fichiers, exports, lots) sans tout charger en mémoire.

4. PHP a-t-il async/await ? Comment gère-t-on le travail en arrière-plan ?

Non : le code est synchrone par requête. Le travail différé/lent se délègue à des workers via Messenger (le contrôleur poste un message et rend la réponse tout de suite).

5. À quoi servent les Fibers, et les écrit-on souvent soi-même ?

Ce sont la primitive de concurrence coopérative (suspendre/reprendre une fonction), socle de bibliothèques async (ReactPHP, Amp). On ne les écrit quasiment jamais à la main.


Prochain chapitre : 2.8 — PHP ↔ TypeScript — la grande table de correspondance, les faux-amis, et le mémo de survie.