← Toutes les fiches

Fiche 6 Parcours : les fondamentaux Lecture : 20 minutes

La programmation orientée objet

Des objets qui protègent leurs règles, et du code qu'on étend sans tout réécrire.

Écrire des classes ne suffit pas à faire de la programmation orientée objet. Un code avec des classes pleines de getters et de setters, et toute la logique dans des contrôleurs de 300 lignes, reste du code procédural déguisé. La POO devient utile quand les objets portent leurs propres règles, et quand on peut ajouter un comportement sans modifier ce qui marche déjà.

Tu utilises déjà ces idées tous les jours, parfois sans les nommer : Symfony est construit dessus. Cette fiche leur donne un nom, montre le problème que chacune résout, et dit quand elle coûte plus qu'elle ne rapporte.

1.L'encapsulation : un objet protège ses règles

Une commande ne doit pas être expédiée avant d'être payée. Où écrire cette règle ? Si l'état de la commande est public, chaque contrôleur, chaque commande console, chaque service doit penser à la vérifier. Il suffit d'un oubli.

exemples/06/encapsulation.php
<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
}

// ✗ Tout est public : n'importe quel code peut mettre la commande dans n'importe quel état
final class OpenOrder
{
    public OrderStatus $status = OrderStatus::Pending;
}

$open = new OpenOrder();
$open->status = OrderStatus::Shipped;   // expédiée sans avoir été payée, et rien ne l'empêche
echo 'Commande ouverte : ', $open->status->value, "\n";

// ✓ L'état se lit, mais ne change qu'à travers des méthodes qui portent les règles
final class Order
{
    public private(set) OrderStatus $status = OrderStatus::Pending;
    public private(set) ?string $trackingNumber = null;

    public function pay(): void
    {
        if ($this->status !== OrderStatus::Pending) {
            throw new LogicException('Seule une commande en attente peut être payée');
        }
        $this->status = OrderStatus::Paid;
    }

    public function ship(string $trackingNumber): void
    {
        if ($this->status !== OrderStatus::Paid) {
            throw new LogicException('Une commande doit être payée avant d\'être expédiée');
        }
        $this->status = OrderStatus::Shipped;
        $this->trackingNumber = $trackingNumber;
    }
}

$order = new Order();
try {
    $order->ship('FR123');
} catch (LogicException $error) {
    echo 'Refusé : ', $error->getMessage(), "\n";
}
$order->pay();
$order->ship('FR123');
echo 'Commande protégée : ', $order->status->value, ', suivi ', $order->trackingNumber, "\n";
Sortie
Commande ouverte : shipped
Refusé : Une commande doit être payée avant d'être expédiée
Commande protégée : shipped, suivi FR123

L'encapsulation consiste à cacher l'état interne d'un objet et à ne le laisser changer qu'à travers des méthodes qui vérifient les règles. La règle est écrite une seule fois, à l'endroit où elle ne peut pas être contournée.

  1. Des méthodes métier plutôt que des setters : pay() et ship() disent ce qui se passe, setStatus() permet n'importe quoi.
  2. « Dis, ne demande pas » (tell, don't ask) : plutôt que de lire l'état d'un objet pour décider à sa place, demande-lui de faire l'action. Il sait mieux que toi s'il le peut.
  3. La visibilité asymétrique de PHP 8.4 (fiche 5) rend l'état lisible sans le rendre modifiable.
Sécurité : l'affectation de masse

Un formulaire ou une API qui copie tous les champs reçus dans une entité, avec ses setters, laisse un attaquant ajouter un champ que tu n'avais pas prévu : isAdmin, status, price. C'est l'affectation de masse (mass assignment). Ne lie jamais aveuglément une requête à une entité : déclare seulement les champs autorisés dans le formulaire Symfony, ou passe par un DTO (un petit objet qui ne contient que les champs attendus), puis appelle les méthodes métier.

Quand ne pas s'acharner

Un objet qui ne fait que transporter des données (un DTO, une ligne de rapport) n'a pas de règle à protéger : des propriétés publiques readonly suffisent. L'encapsulation sert là où il y a des règles métier.

2.L'abstraction : garder l'essentiel

Quand tu appelles orderTotalInCents($items), tu ne relis pas le calcul : le nom de la fonction te suffit. Tu sais ce qu'elle fait, sans avoir besoin de savoir comment. C'est une abstraction.

L'abstraction consiste à ne garder que l'essentiel d'une chose et à cacher ses détails, pour pouvoir l'utiliser sans y penser. L'encapsulation protège l'intérieur d'un objet. L'abstraction simplifie la façon de s'en servir. Les deux vont ensemble, mais ce n'est pas la même idée.

  1. Un bon nom de fonction ou de méthode est déjà une abstraction (fiche 2).
  2. Une interface va plus loin : elle décrit ce que savent faire plusieurs classes différentes. Dans la section 3, le calculateur sait qu'un mode de livraison « a des frais », et ignore tout de leur calcul.
  3. Une classe abstraite est une classe parente incomplète. On ne peut pas l'instancier : elle sert de base à des classes enfants, leur fournit du code commun, et les oblige à écrire certaines méthodes.

Voici une classe abstraite qui fixe le déroulement d'un rapport de ventes, et laisse aux enfants le seul détail qui change, le format :

exemples/06/abstract_class.php
<?php

declare(strict_types=1);

// La classe abstraite fixe le déroulement commun, et laisse un seul détail aux enfants
abstract class SalesReport
{
    /** @param list<array{product: string, quantity: int}> $rows */
    final public function render(array $rows): string
    {
        $lines = [$this->header()];
        foreach ($rows as $row) {
            $lines[] = $this->formatRow($row['product'], $row['quantity']);
        }
        $lines[] = 'Total : ' . array_sum(array_column($rows, 'quantity')) . ' article(s)';

        return implode("\n", $lines) . "\n";
    }

    // Les enfants DOIVENT écrire ces deux méthodes
    abstract protected function header(): string;

    abstract protected function formatRow(string $product, int $quantity): string;
}

final class CsvSalesReport extends SalesReport
{
    protected function header(): string
    {
        return 'product;quantity';
    }

    protected function formatRow(string $product, int $quantity): string
    {
        return "$product;$quantity";
    }
}

final class MarkdownSalesReport extends SalesReport
{
    protected function header(): string
    {
        return "| Produit | Quantité |\n|---|---|";
    }

    protected function formatRow(string $product, int $quantity): string
    {
        return "| $product | $quantity |";
    }
}

$rows = [
    ['product' => 'Clavier', 'quantity' => 3],
    ['product' => 'Souris', 'quantity' => 5],
];
echo (new CsvSalesReport())->render($rows), "\n";
echo (new MarkdownSalesReport())->render($rows), "\n";

try {
    new SalesReport();   // une classe abstraite ne s'instancie pas
} catch (Error $error) {
    echo $error->getMessage(), "\n";
}
Sortie
product;quantity
Clavier;3
Souris;5
Total : 8 article(s)

| Produit | Quantité |
|---|---|
| Clavier | 3 |
| Souris | 5 |
Total : 8 article(s)

Cannot instantiate abstract class SalesReport
InterfaceClasse abstraite
Ce qu'elle contientUn contrat : des signatures de méthodes, sans codeDu code commun, de l'état, et des méthodes à compléter
Combien par classeUne classe peut en implémenter plusieursUne classe n'a qu'une seule parente
Le lien créé« sait faire »« est un »
Exemple dans SymfonyEventSubscriberInterface, UserInterfaceAbstractController, Voter
À choisirPar défaut, pour relier des composantsQuand il y a vraiment du code commun à partager, dans une relation stable
Quand ne pas abstraire

Une mauvaise abstraction coûte plus cher que pas d'abstraction du tout. Deux morceaux de code qui se ressemblent aujourd'hui peuvent évoluer différemment demain. Si tu les as fusionnés trop tôt, chaque nouvelle différence ajoute un paramètre ou un if dans le code commun, jusqu'à ce que plus personne ne le comprenne. Sandi Metz le résume ainsi : « duplication is far cheaper than the wrong abstraction » (la duplication coûte bien moins cher que la mauvaise abstraction).

3.Interfaces et polymorphisme

Le calcul des frais de port dépend du mode de livraison : domicile, point relais, retrait en magasin. La première idée est un grand if/elseif. À chaque nouveau mode, il faut rouvrir la même méthode, la relire en entier, et risquer de casser les autres cas.

Une interface est un contrat : elle dit ce qu'un objet sait faire, sans dire comment. Plusieurs classes peuvent l'implémenter, chacune à sa façon. Le code qui l'utilise ne connaît que le contrat : c'est le polymorphisme (des objets différents utilisés de la même manière).

exemples/06/strategy.php
<?php

declare(strict_types=1);

// Le contrat : ce que tout mode de livraison sait faire
interface ShippingMethod
{
    public function supports(string $code): bool;

    public function feeInCents(int $weightInGrams): int;
}

// Les implémentations : chacune sa règle, chacune dans sa classe
final class HomeDelivery implements ShippingMethod
{
    public function supports(string $code): bool
    {
        return $code === 'home';
    }

    public function feeInCents(int $weightInGrams): int
    {
        // 4,99 €, plus 1,50 € par kilo complet
        return 499 + intdiv($weightInGrams, 1000) * 150;
    }
}

final class PickupPoint implements ShippingMethod
{
    public function supports(string $code): bool
    {
        return $code === 'pickup';
    }

    public function feeInCents(int $weightInGrams): int
    {
        return 299;
    }
}

final class StorePickup implements ShippingMethod
{
    public function supports(string $code): bool
    {
        return $code === 'store';
    }

    public function feeInCents(int $weightInGrams): int
    {
        return 0;
    }
}

// Le code qui utilise : il ne connaît que l'interface, jamais les classes concrètes
final class ShippingCalculator
{
    /** @param iterable<ShippingMethod> $methods */
    public function __construct(private iterable $methods) {}

    public function feeInCents(string $code, int $weightInGrams): int
    {
        foreach ($this->methods as $method) {
            if ($method->supports($code)) {
                return $method->feeInCents($weightInGrams);
            }
        }

        throw new InvalidArgumentException("Mode de livraison inconnu : $code");
    }
}

$calculator = new ShippingCalculator([
    new HomeDelivery(),
    new PickupPoint(),
    new StorePickup(),
]);
foreach (['home', 'pickup', 'store'] as $code) {
    printf("%-6s 2,5 kg : %5.2f €\n", $code, $calculator->feeInCents($code, 2500) / 100);
}
Sortie
home   2,5 kg :  7.99 €
pickup 2,5 kg :  2.99 €
store  2,5 kg :  0.00 €

Ce montage porte un nom : le pattern Strategy (stratégie). Une famille d'algorithmes interchangeables, choisie au moment de l'exécution. Ajouter un mode de livraison, c'est ajouter une classe, sans toucher au calculateur ni aux autres modes.

Dans Symfony, le conteneur de services fait l'assemblage tout seul :

exemples/06/strategy_symfony.php · illustration, non exécuté
<?php

// Dans Symfony, le conteneur de services fait l'assemblage à ta place

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

// 1. Toute classe qui implémente l'interface reçoit automatiquement l'étiquette
#[AutoconfigureTag('app.shipping_method')]
interface ShippingMethod
{
    public function supports(string $code): bool;

    public function feeInCents(int $weightInGrams): int;
}

// 2. Le calculateur reçoit tous les services qui portent cette étiquette
final class ShippingCalculator
{
    /** @param iterable<ShippingMethod> $methods */
    public function __construct(
        #[AutowireIterator('app.shipping_method')]
        private iterable $methods,
    ) {}
}

// 3. Ajouter un mode de livraison = créer une classe. Aucune autre ligne à modifier.

Tu utilises déjà ce pattern sans le savoir : les Voters de la sécurité, les normalizers du Serializer et les value resolvers des contrôleurs fonctionnent exactement ainsi.

Quand ne pas l'utiliser

Deux cas qui ne changeront probablement jamais tiennent très bien dans un match (fiche 5). Le Strategy devient rentable quand les cas se multiplient, quand chacun a une logique non triviale, ou quand d'autres équipes doivent pouvoir en ajouter.

4.Composition plutôt qu'héritage

L'héritage (extends) semble pratique pour réutiliser du code. Mais dès qu'on combine plusieurs comportements, le nombre de classes explose :

exemples/06/inheritance_problem.php · illustration, non exécuté
<?php

// Avec l'héritage, chaque combinaison de comportements demande sa propre classe…

class EmailNotifier {}
class LoggingEmailNotifier extends EmailNotifier {}
class SmsNotifier {}
class LoggingSmsNotifier extends SmsNotifier {}
class RetryingLoggingEmailNotifier extends LoggingEmailNotifier {}
class RetryingLoggingSmsNotifier extends LoggingSmsNotifier {}
// … et un nouveau canal (Slack) ou un nouveau comportement (retry) double le nombre de classes.

La composition consiste à construire un objet en lui donnant d'autres objets, plutôt qu'en héritant d'eux. Ici, un objet qui en enveloppe un autre pour lui ajouter un comportement s'appelle un décorateur :

exemples/06/composition.php
<?php

declare(strict_types=1);

interface Notifier
{
    public function send(string $to, string $message): void;
}

final class EmailNotifier implements Notifier
{
    public function send(string $to, string $message): void
    {
        echo "  [e-mail] à $to : $message\n";
    }
}

final class SmsNotifier implements Notifier
{
    public function send(string $to, string $message): void
    {
        echo "  [SMS] à $to : $message\n";
    }
}

// Un « décorateur » : il enveloppe N'IMPORTE QUEL Notifier et lui ajoute un comportement
final class LoggingNotifier implements Notifier
{
    public function __construct(private Notifier $inner) {}

    public function send(string $to, string $message): void
    {
        echo "  [log] envoi vers $to\n";
        $this->inner->send($to, $message);
    }
}

final class DisabledInTestNotifier implements Notifier
{
    public function __construct(private Notifier $inner, private bool $enabled) {}

    public function send(string $to, string $message): void
    {
        if ($this->enabled) {
            $this->inner->send($to, $message);
        }
    }
}

// On combine les morceaux comme des briques, sans créer de sous-classe
echo "E-mail journalisé :\n";
(new LoggingNotifier(new EmailNotifier()))->send('alice@example.com', 'Commande expédiée');

echo "SMS journalisé :\n";
(new LoggingNotifier(new SmsNotifier()))->send('+33600000000', 'Colis disponible');

echo "SMS désactivé (environnement de test) :\n";
(new DisabledInTestNotifier(new SmsNotifier(), enabled: false))->send('+33600000000', 'Rien');
echo "  (aucun envoi)\n";
Sortie
E-mail journalisé :
  [log] envoi vers alice@example.com
  [e-mail] à alice@example.com : Commande expédiée
SMS journalisé :
  [log] envoi vers +33600000000
  [SMS] à +33600000000 : Colis disponible
SMS désactivé (environnement de test) :
  (aucun envoi)
  1. Chaque brique fait une seule chose : envoyer, journaliser, désactiver. On les teste séparément.
  2. On les combine librement, y compris au moment de l'exécution selon la configuration.
  3. Un nouveau canal ou un nouveau comportement ajoute une classe, au lieu de doubler la hiérarchie.

Symfony l'utilise partout, et propose l'attribut #[AsDecorator] pour décorer un service existant sans le modifier. Marquer les classes final par défaut, comme dans ces exemples, pousse aussi vers la composition : on ne peut plus en hériter par facilité.

Quand l'héritage reste le bon choix

Quand la relation est vraiment « est un » et stable : AbstractController, WebTestCase ou une classe d'exception de base sont des usages sains. Le signal d'alarme, c'est une hiérarchie de plus de deux niveaux, ou une classe enfant qui désactive ce que fait sa parente.

5.SOLID, sans dogme

SOLID regroupe cinq principes, formulés par Robert C. Martin. Ce sont des repères pour reconnaître un code qui va devenir pénible à modifier, pas des lois à appliquer partout. Tu en as déjà vu la plupart dans cette fiche.

PrincipeEn une phraseOù tu l'as vuQuand ça devient excessif
Single responsibilityUne classe n'a qu'une raison de changer.Chaque mode de livraison dans sa classeDes dizaines de classes de trois lignes, impossibles à suivre
Open/closedOn étend le comportement sans modifier le code existant.Ajouter un mode de livraison sans toucher au calculateurPrévoir des points d'extension pour des besoins qui n'arriveront jamais
Liskov substitutionUne sous-classe doit pouvoir remplacer sa parente sans surprise.Tout Notifier peut être décoréRarement excessif : c'est le principe à ne jamais violer
Interface segregationPlusieurs petites interfaces valent mieux qu'une grosse.ShippingMethod n'a que deux méthodesUne interface par méthode, sans raison
Dependency inversionDépendre de contrats, pas de classes concrètes.ShippingCalculator ne connaît que l'interfaceUne interface pour une classe qui n'aura jamais qu'une implémentation

Le principe de Liskov mérite un exemple. Imagine une classe ReadOnlyStorage qui hérite de Storage et dont la méthode save() lève une exception. Tout code qui reçoit un Storage et appelle save() peut planter selon l'objet qu'on lui passe. L'héritage promettait quelque chose que l'enfant ne tient pas. La bonne réponse est souvent deux interfaces séparées : une pour lire, une pour écrire.

6.Les factories : bien construire un objet

Quand construire un objet demande une conversion, une validation ou un choix, on confie ce travail à une factory (fabrique). La forme la plus simple est un constructeur nommé, une méthode statique qui dit d'où vient la valeur :

exemples/06/factory.php
<?php

declare(strict_types=1);

final readonly class Money
{
    // Constructeur privé : on passe forcément par une méthode
    // qui dit d'où vient la valeur
    private function __construct(public int $amountInCents) {}

    public static function fromCents(int $cents): self
    {
        return new self($cents);
    }

    // « 19,90 » ou « 19.90 », tel qu'on le saisit dans un formulaire
    public static function fromUserInput(string $input): self
    {
        if (preg_match('/^(\d{1,7})(?:[.,](\d{1,2}))?$/', trim($input), $parts) !== 1) {
            throw new InvalidArgumentException("Montant invalide : « $input »");
        }
        $cents = str_pad($parts[2] ?? '0', 2, '0');

        return new self((int) $parts[1] * 100 + (int) $cents);
    }
}

echo Money::fromCents(1990)->amountInCents, "\n";
echo Money::fromUserInput('19,9')->amountInCents, "\n";
echo Money::fromUserInput(' 7.05 ')->amountInCents, "\n";

try {
    Money::fromUserInput('12,345');
} catch (InvalidArgumentException $error) {
    echo $error->getMessage(), "\n";
}
Sortie
1990
1990
705
Montant invalide : « 12,345 »
Quand ne pas en créer

Si new Product($name, $price) suffit, une factory n'ajoute qu'une indirection. Elle se justifie quand il y a plusieurs façons de construire l'objet, ou une logique de construction à cacher.

7.Les événements : le pattern Observer

Quand une commande est passée, il faut envoyer un e-mail, réserver le stock, prévenir la comptabilité, et demain peut-être autre chose. Si le service de commande appelle tout ça directement, il grossit à chaque nouvelle demande et dépend de tous les autres services.

Le pattern Observer inverse la relation : le service annonce ce qui s'est passé (un événement), et ceux que ça intéresse (des listeners, ou « écouteurs ») réagissent. Voici le principe, sans Symfony :

exemples/06/observer.php
<?php

declare(strict_types=1);

// L'événement : un simple objet qui décrit ce qui s'est passé
final readonly class OrderPlaced
{
    public function __construct(public int $orderId, public string $customerEmail) {}
}

// Un mini « dispatcher », pour voir le principe sans Symfony
final class EventDispatcher
{
    /** @var array<class-string, list<callable>> */
    private array $listeners = [];

    public function listen(string $eventClass, callable $listener): void
    {
        $this->listeners[$eventClass][] = $listener;
    }

    public function dispatch(object $event): void
    {
        foreach ($this->listeners[$event::class] ?? [] as $listener) {
            $listener($event);
        }
    }
}

$dispatcher = new EventDispatcher();

// Chaque réaction est indépendante, et la commande ne les connaît pas
$dispatcher->listen(OrderPlaced::class, function (OrderPlaced $event): void {
    echo "E-mail de confirmation à {$event->customerEmail}\n";
});
$dispatcher->listen(OrderPlaced::class, function (OrderPlaced $event): void {
    echo "Stock réservé pour la commande {$event->orderId}\n";
});

// Le code qui passe la commande se contente d'annoncer ce qui s'est passé
$dispatcher->dispatch(new OrderPlaced(1042, 'alice@example.com'));
Sortie
E-mail de confirmation à alice@example.com
Stock réservé pour la commande 1042

Avec le composant EventDispatcher de Symfony, c'est la même chose, avec la découverte automatique des listeners :

exemples/06/listener_symfony.php · illustration, non exécuté
<?php

// La même chose avec le composant EventDispatcher de Symfony

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class CheckoutService
{
    public function __construct(private EventDispatcherInterface $dispatcher) {}

    public function placeOrder(Order $order): void
    {
        // … enregistrement de la commande …
        $this->dispatcher->dispatch(new OrderPlaced($order->getId(), $order->getEmail()));
    }
}

// Symfony trouve ce listener tout seul grâce à l'attribut
#[AsEventListener]
final class SendOrderConfirmation
{
    public function __invoke(OrderPlaced $event): void
    {
        // … envoi de l'e-mail …
    }
}

Symfony lui-même fonctionne ainsi : les événements kernel.request, kernel.exception ou ceux de Doctrine permettent de réagir à chaque étape d'une requête sans modifier le framework.

Quand ne pas l'utiliser

Les événements cachent le déroulement : en lisant placeOrder(), on ne voit plus ce qui se passe ensuite. La commande bin/console debug:event-dispatcher aide, mais pour deux actions toujours liées, un appel direct est plus honnête. Attention aussi : les listeners s'exécutent l'un après l'autre, dans la même requête. Une exception dans l'un arrête les suivants, et un traitement lent ralentit la page. Pour un travail lourd, le composant Messenger le fait en arrière-plan.

8.Quand la POO en fait trop

Chaque abstraction (une interface, une factory, un décorateur) a un coût : un fichier de plus, un saut de plus pour suivre le code, une notion de plus à comprendre pour le prochain développeur. Elle doit rapporter plus qu'elle ne coûte.

  1. YAGNI (You Aren't Gonna Need It, « tu n'en auras pas besoin ») : n'ajoute pas un point d'extension pour un besoin hypothétique. Ajoute-le le jour où le deuxième cas arrive.
  2. Une interface pour une seule implémentation est souvent inutile. Symfony injecte très bien une classe concrète, et tu pourras extraire l'interface plus tard, quand un deuxième besoin apparaîtra. Exception : quand l'interface sert de frontière avec l'extérieur (une API de paiement à remplacer dans les tests).
  3. Une fonction peut rester une fonction. Un calcul pur, sans état, n'a pas besoin d'une classe, d'une interface et d'une factory.

Pièges fréquents

PiègeCe qui arriveLa parade
Un setter pour chaque propriétéLes règles métier sont éparpillées dans les contrôleursDes méthodes métier qui vérifient les règles
Lier une requête directement à une entitéUn attaquant modifie un champ non prévuUn formulaire limité aux champs autorisés, ou un DTO
Un if/elseif qui grossit à chaque besoinChaque ajout risque de casser les autres casUne interface et une classe par cas (Strategy)
Hériter pour réutiliser du codeUne hiérarchie qui explose, des parents qu'on n'ose plus modifierLa composition, et des classes final par défaut
Une sous-classe qui désactive une méthodeLe code qui reçoit la classe parente plante au hasardDes interfaces séparées (Liskov)
Fusionner deux codes dès qu'ils se ressemblentUn code commun rempli de paramètres et de ifLa règle de trois
Une classe abstraite pour partager deux méthodes utilitairesUne hiérarchie rigide, pour un gain minimeUn service injecté (composition)
Une interface pour chaque classeDeux fichiers à maintenir pour rienExtraire l'interface quand le deuxième besoin arrive
Un listener qui fait un travail lourdLa page attend la fin de l'envoi des e-mailsMessenger en arrière-plan
Appliquer SOLID à la lettreUne architecture complexe pour un problème simpleDes repères, pas des lois : mesurer le coût

Checklist avant de proposer ton code

Exercice : remplacer un if par des stratégies

Le fichier exemples/06/exercice.php calcule des frais de paiement avec une suite de elseif. Aujourd'hui, il affiche :

Sortie
card     sur 100 € : 165 centimes
paypal   sur 100 € : 325 centimes
transfer sur 100 € : 0 centimes
  1. Crée une interface PaymentMethod et une classe par moyen de paiement.
  2. Réécris PaymentFeeCalculator pour qu'il reçoive la liste des moyens de paiement, comme le calculateur de livraison de la section 3. La sortie doit rester exactement la même.
  3. Ajoute un moyen de paiement « bon d'achat » (voucher), sans frais. Combien de fichiers existants as-tu dû modifier ?
  4. Ajoute un décorateur qui plafonne les frais à 5 € pour tous les moyens de paiement. Pourquoi est-ce plus simple qu'avec l'héritage ?
  5. Question de recul : pour trois moyens de paiement qui ne changent jamais, cette architecture valait-elle le coup ? À partir de quand devient-elle rentable ?
docker compose run --rm php php exemples/06/exercice.php

Conseils

  1. Pars du problème, pas du pattern. On ne se dit pas « je vais faire un Strategy », on constate qu'un if grossit, et le Strategy est une réponse possible.
  2. Lis le code de Symfony. Ouvre un Voter, un listener du noyau ou un décorateur : ce sont des exemples concrets et soignés de chaque pattern de cette fiche.
  3. Le test révèle la conception. Une classe difficile à tester (fiche 3) dépend souvent de trop de choses concrètes. Une dépendance passée au constructeur se remplace facilement par une doublure.
  4. Refactore vers un pattern, pas depuis. Commence simple, et introduis l'abstraction quand le deuxième ou le troisième cas arrive. Elle sera alors taillée pour un vrai besoin.

Documentation officielle