Design Patterns · PHP

SOLID

Les 5 principes fondamentaux de la programmation orientée objet pour écrire du code PHP propre, maintenable et évolutif.

scroll
S

Single Responsibility

Principe de responsabilité unique

Une classe ne doit avoir qu'une seule raison de changer. Chaque classe doit encapsuler une seule responsabilité et la remplir entièrement.

UserManager.php Violation
// Cette classe fait TOUT : validation,
// persistance ET envoi d'emails

class UserManager
{
    public function register(string $email, string $password): void
    {
        // Validation
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Email invalide');
        }

        // Sauvegarde en base
        $pdo = new \PDO('mysql:host=localhost');
        $stmt = $pdo->prepare('INSERT INTO users ...');
        $stmt->execute([$email, $password]);

        // Envoi d'email
        mail($email, 'Bienvenue!', '...');
    }
}
UserService.php Correct
// Chaque classe = une responsabilité

class UserValidator
{
    public function validate(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

class UserRepository
{
    public function save(User $user): void { /* ... */ }
}

class WelcomeMailer
{
    public function send(User $user): void { /* ... */ }
}
💡

Astuce : Si vous décrivez votre classe avec « ET » (elle valide ET sauvegarde ET envoie), c'est un signe qu'elle a trop de responsabilités.

O

Open / Closed

Principe ouvert / fermé

Une classe doit être ouverte à l'extension mais fermée à la modification. On ajoute du comportement sans toucher au code existant.

DiscountCalculator.php Violation
// Chaque nouveau type de remise
// oblige à modifier cette classe

class DiscountCalculator
{
    public function calculate(
        Order $order,
        string $type
    ): float {
        return match($type) {
            'percentage' => $order->total * 0.1,
            'fixed'      => $order->total - 5,
            'vip'        => $order->total * 0.2,
            // ... encore et encore
        };
    }
}
Discount.php Correct
interface Discount
{
    public function apply(Order $order): float;
}

class PercentageDiscount implements Discount
{
    public function __construct(
        private readonly float $rate
    ) {}

    public function apply(Order $order): float
    {
        return $order->total * $this->rate;
    }
}

// Ajouter un nouveau type ? Créez une classe !
class VipDiscount implements Discount { /* ... */ }

Pattern : Utilisez des interfaces et le Strategy Pattern pour permettre l'extension sans modification du code existant.

L

Liskov Substitution

Principe de substitution de Liskov

Une sous-classe doit pouvoir remplacer sa classe parente sans casser le comportement du programme. Les enfants doivent respecter le « contrat » du parent.

ReadOnlyRepository.php Violation
class FileRepository
{
    public function read(string $path): string
    {
        return file_get_contents($path);
    }

    public function write(string $path, string $data): void
    {
        file_put_contents($path, $data);
    }
}

// Casse le contrat du parent !
class ReadOnlyFileRepo extends FileRepository
{
    public function write(string $path, string $data): void
    {
        throw new \RuntimeException('Interdit !');
    }
}
FileInterfaces.php Correct
interface Readable
{
    public function read(string $path): string;
}

interface Writable
{
    public function write(string $path, string $data): void;
}

// Chaque classe respecte son contrat
class ReadOnlyFileRepo implements Readable
{
    public function read(string $path): string
    {
        return file_get_contents($path);
    }
}

class FullFileRepo implements Readable, Writable
{
    /* read() + write() */
}
🔎

Règle d'or : Si une sous-classe lève une exception là où le parent ne le fait pas, ou ignore une méthode héritée, vous violez Liskov.

I

Interface Segregation

Principe de ségrégation des interfaces

Aucun client ne devrait être forcé de dépendre de méthodes qu'il n'utilise pas. Préférez plusieurs petites interfaces spécifiques à une seule interface générale.

Worker.php Violation
// Interface trop large ("fat interface")

interface Worker
{
    public function work(): void;
    public function eat(): void;
    public function sleep(): void;
}

// Un robot ne mange pas et ne dort pas !
class Robot implements Worker
{
    public function work(): void  { /* OK */ }
    public function eat(): void   { /* ??? */ }
    public function sleep(): void { /* ??? */ }
}
SmallInterfaces.php Correct
interface Workable
{
    public function work(): void;
}

interface Feedable
{
    public function eat(): void;
}

interface Sleepable
{
    public function sleep(): void;
}

// Chaque classe choisit ce dont elle a besoin
class Human implements Workable, Feedable, Sleepable
{ /* ... */ }

class Robot implements Workable
{ /* Seulement work() */ }

Signal d'alerte : Si vous implémentez une interface et laissez des méthodes vides ou avec throw new \Exception, l'interface est trop grosse.

D

Dependency Inversion

Principe d'inversion des dépendances

Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions (interfaces).

OrderService.php Violation
// Couplage fort à une implémentation

class OrderService
{
    private MySqlDatabase $db;
    private StripePayment $payment;

    public function __construct()
    {
        // Instanciation directe = couplage
        $this->db      = new MySqlDatabase();
        $this->payment = new StripePayment();
    }

    public function place(Order $order): void
    {
        $this->db->save($order);
        $this->payment->charge($order->total);
    }
}
OrderService.php Correct
interface Database
{
    public function save(object $entity): void;
}

interface PaymentGateway
{
    public function charge(float $amount): bool;
}

// Dépend d'abstractions, pas d'implémentations
class OrderService
{
    public function __construct(
        private readonly Database $db,
        private readonly PaymentGateway $payment,
    ) {}

    public function place(Order $order): void
    {
        $this->db->save($order);
        $this->payment->charge($order->total);
    }
}
📚

En pratique : Utilisez l'injection de dépendances via le constructeur et le Service Container de votre framework (Laravel, Symfony) pour câbler les implémentations.