🏗️ Design Patterns en PHP : Le Guide Complet avec Exemples Pratiques
Découvrez les 23 patterns de conception (GoF) adaptés au PHP moderne. Apprenez à créer des applications robustes, évolutives et maintenables grâce à des exemples concrets et un projet e-commerce complet.
⚡ Passez à la vitesse supérieure en POO PHP !
Structurez vos projets PHP comme un vrai professionnel avec les concepts avancés de programmation orientée objet.
👉 Explorez notre guide avancé : "POO Avancée en PHP : Classes Abstraites, Interfaces et Traits"
Maîtrisez les classes abstraites pour forcer des contrats, les interfaces pour plus de flexibilité, et les traits pour réutiliser du code intelligemment. Avec un exemple complet qui les réunit tous !
1️⃣ Introduction aux Design Patterns
Qu'est-ce qu'un design pattern ?
Un design pattern (ou patron de conception) est une solution générique et réutilisable à un problème récurrent de conception logicielle. C'est un modèle éprouvé qui décrit comment résoudre un problème spécifique dans un contexte donné.
Pourquoi les utiliser en PHP ?
- Maintenabilité : Code mieux organisé et plus facile à comprendre
- Réutilisabilité : Solutions éprouvées applicables à divers projets
- Évolutivité : Architecture qui supporte l'ajout de fonctionnalités
- Collaboration : Langage commun entre développeurs
- Robustesse : Solutions testées et documentées
Les 3 catégories de patterns (GoF)
📊 Classification des 23 patterns originaux :
- Créationnels (5) : Gèrent la création d'objets (Singleton, Factory, Builder...)
- Structurels (7) : Concernent la composition des objets (Adapter, Decorator, Facade...)
- Comportementaux (11) : Gèrent les interactions entre objets (Observer, Strategy, Command...)
Avantages pour la maintenabilité et l'évolutivité
Les design patterns permettent de créer des applications qui résistent à l'épreuve du temps. Ils facilitent les modifications futures et réduisent les risques de régressions.
2️⃣ Patterns de Création
a. Singleton Pattern
Problème résolu : Garantir une seule instance
Assure qu'une classe n'a qu'une seule instance et fournit un point d'accès global à cette instance.
Cas d'utilisation :
- Connexion à une base de données
- Système de logging
- Gestionnaire de configuration
- Cache partagé
<?php
class DatabaseConnection {
private static $instance = null;
private $connection;
// Constructeur privé pour empêcher l'instanciation directe
private function __construct() {
$this->connection = new PDO(
'mysql:host=localhost;dbname=test',
'username',
'password'
);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
// Empêcher le clonage
private function __clone() {}
// Empêcher la désérialisation
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
// Méthode statique pour obtenir l'instance unique
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// Méthode pour obtenir la connexion
public function getConnection() {
return $this->connection;
}
}
// UTILISATION
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();
var_dump($db1 === $db2); // bool(true) - Même instance
// Utilisation de la connexion
$stmt = $db1->getConnection()->prepare("SELECT * FROM users");
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
⚠️ Attention : Singleton = Anti-pattern ?
Le Singleton est souvent critiqué car il introduit un état global et rend les tests unitaires difficiles. À utiliser avec parcimonie !
Alternatives : Injection de dépendances, conteneur de services (Laravel).
b. Factory Pattern
Problème résolu : Création d'objets sans exposer la logique
Permet de créer des objets sans spécifier la classe exacte de l'objet à créer.
Factory Method vs Abstract Factory :
- Factory Method : Une méthode qui crée des objets d'un type spécifique
- Abstract Factory : Une interface pour créer des familles d'objets liés
<?php
// Interface commune
interface Notification {
public function send($message, $recipient);
}
// Implémentations concrètes
class EmailNotification implements Notification {
public function send($message, $recipient) {
echo "Envoi d'email à $recipient : $message<br>";
// Logique d'envoi d'email réelle
return true;
}
}
class SMSNotification implements Notification {
public function send($message, $recipient) {
echo "Envoi de SMS à $recipient : $message<br>";
// Logique d'envoi SMS réelle
return true;
}
}
class PushNotification implements Notification {
public function send($message, $recipient) {
echo "Envoi de notification push à $recipient : $message<br>";
// Logique d'envoi push réelle
return true;
}
}
// Factory pour créer les notifications
class NotificationFactory {
public static function createNotification($type) {
switch (strtolower($type)) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
case 'push':
return new PushNotification();
default:
throw new InvalidArgumentException("Type de notification non supporté: $type");
}
}
}
// UTILISATION
try {
$emailNotif = NotificationFactory::createNotification('email');
$smsNotif = NotificationFactory::createNotification('sms');
$pushNotif = NotificationFactory::createNotification('push');
$emailNotif->send("Bienvenue sur notre site!", "user@example.com");
$smsNotif->send("Votre code de vérification: 123456", "+33612345678");
$pushNotif->send("Nouveau message reçu", "device_token_abc");
} catch (Exception $e) {
echo "Erreur: " . $e->getMessage();
}
?>
c. Builder Pattern
Problème résolu : Construction d'objets complexes
Permet de construire des objets complexes étape par étape, en séparant la construction de la représentation.
<?php
class QueryBuilder {
private $select = ['*'];
private $from;
private $where = [];
private $orderBy = [];
private $limit;
private $offset;
public function select($columns) {
$this->select = is_array($columns) ? $columns : func_get_args();
return $this;
}
public function from($table, $alias = null) {
$this->from = $alias ? "$table AS $alias" : $table;
return $this;
}
public function where($column, $operator, $value = null) {
if ($value === null) {
$value = $operator;
$operator = '=';
}
$this->where[] = [$column, $operator, $value];
return $this;
}
public function orderBy($column, $direction = 'ASC') {
$this->orderBy[] = [$column, $direction];
return $this;
}
public function limit($limit) {
$this->limit = $limit;
return $this;
}
public function offset($offset) {
$this->offset = $offset;
return $this;
}
public function build() {
$sql = "SELECT " . implode(', ', $this->select);
$sql .= " FROM " . $this->from;
if (!empty($this->where)) {
$sql .= " WHERE ";
$conditions = [];
foreach ($this->where as $where) {
$conditions[] = "$where[0] $where[1] ?";
}
$sql .= implode(' AND ', $conditions);
}
if (!empty($this->orderBy)) {
$sql .= " ORDER BY ";
$orders = [];
foreach ($this->orderBy as $order) {
$orders[] = "$order[0] $order[1]";
}
$sql .= implode(', ', $orders);
}
if ($this->limit !== null) {
$sql .= " LIMIT " . $this->limit;
}
if ($this->offset !== null) {
$sql .= " OFFSET " . $this->offset;
}
return $sql;
}
public function getParams() {
$params = [];
foreach ($this->where as $where) {
$params[] = $where[2];
}
return $params;
}
}
// UTILISATION
$queryBuilder = new QueryBuilder();
$query = $queryBuilder
->select(['id', 'name', 'email'])
->from('users')
->where('status', '=', 'active')
->where('age', '>', 18)
->orderBy('name', 'ASC')
->limit(10)
->offset(0)
->build();
$params = $queryBuilder->getParams();
echo "Requête SQL: $query<br>";
echo "Paramètres: " . implode(', ', $params);
?>
3️⃣ Patterns Structurels
a. Adapter Pattern
Problème résolu : Interface incompatible
Permet à des interfaces incompatibles de travailler ensemble en créant un adaptateur.
<?php
// Interface que notre système attend
interface PaymentProcessor {
public function processPayment($amount, $currency);
}
// API PayPal externe (interface différente)
class PayPalApi {
public function sendPayment($amountInCents, $currencyCode) {
echo "PayPal: Paiement de $amountInCents cents en $currencyCode<br>";
return "PAYPAL_TRX_" . uniqid();
}
public function getTransactionStatus($transactionId) {
return "COMPLETED";
}
}
// Adaptateur pour PayPal
class PayPalAdapter implements PaymentProcessor {
private $paypal;
public function __construct(PayPalApi $paypal) {
$this->paypal = $paypal;
}
public function processPayment($amount, $currency) {
// Conversion: euros en cents pour PayPal
$amountInCents = $amount * 100;
// Conversion: code devise complet vers code ISO
$currencyCode = strtoupper(substr($currency, 0, 3));
$transactionId = $this->paypal->sendPayment($amountInCents, $currencyCode);
return [
'success' => true,
'transaction_id' => $transactionId,
'amount' => $amount,
'currency' => $currency
];
}
}
// UTILISATION
$paypalApi = new PayPalApi();
$paypalAdapter = new PayPalAdapter($paypalApi);
function processOrder(PaymentProcessor $processor, $amount, $currency) {
echo "Traitement de la commande...<br>";
$result = $processor->processPayment($amount, $currency);
echo "Résultat: " . json_encode($result) . "<br>";
}
// Notre système peut maintenant utiliser PayPal via l'adaptateur
processOrder($paypalAdapter, 99.99, 'EUR');
?>
b. Decorator Pattern
Problème résolu : Ajouter des fonctionnalités dynamiquement
Permet d'ajouter de nouvelles fonctionnalités à un objet existant sans modifier sa structure.
<?php
// Interface commune
interface DataService {
public function getData($id);
}
// Service concret (coûteux en performance)
class DatabaseService implements DataService {
public function getData($id) {
// Simulation d'une requête coûteuse
sleep(1);
echo "Requête DB pour ID: $id<br>";
return ['id' => $id, 'data' => "Données depuis DB pour $id"];
}
}
// Decorator de base (abstrait)
abstract class DataServiceDecorator implements DataService {
protected $wrappedService;
public function __construct(DataService $service) {
$this->wrappedService = $service;
}
public function getData($id) {
return $this->wrappedService->getData($id);
}
}
// Decorator de cache
class CacheDecorator extends DataServiceDecorator {
private $cache = [];
private $ttl = 3600; // 1 heure
private $cacheTimestamps = [];
public function getData($id) {
if ($this->hasValidCache($id)) {
echo "Cache HIT pour ID: $id<br>";
return $this->cache[$id];
}
echo "Cache MISS pour ID: $id<br>";
$data = parent::getData($id);
// Mettre en cache
$this->cache[$id] = $data;
$this->cacheTimestamps[$id] = time();
return $data;
}
private function hasValidCache($id) {
if (!isset($this->cache[$id])) {
return false;
}
$cacheAge = time() - $this->cacheTimestamps[$id];
return $cacheAge < $this->ttl;
}
public function clearCache() {
$this->cache = [];
$this->cacheTimestamps = [];
}
}
// Decorator de logging
class LoggingDecorator extends DataServiceDecorator {
private $logs = [];
public function getData($id) {
$startTime = microtime(true);
$this->log("Début requête pour ID: $id");
$data = parent::getData($id);
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
$this->log("Fin requête pour ID: $id - Durée: $duration ms");
return $data;
}
private function log($message) {
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] $message";
$this->logs[] = $logEntry;
echo "LOG: $logEntry<br>";
}
public function getLogs() {
return $this->logs;
}
}
// UTILISATION : Empilement de décorateurs
echo "=== Sans décorateurs ===<br>";
$simpleService = new DatabaseService();
$simpleService->getData(1);
$simpleService->getData(1); // Toujours lent
echo "<br>=== Avec cache ===<br>";
$cachedService = new CacheDecorator(new DatabaseService());
$cachedService->getData(2); // Lent (cache miss)
$cachedService->getData(2); // Rapide (cache hit)
echo "<br>=== Avec cache ET logging ===<br>";
$fullService = new LoggingDecorator(
new CacheDecorator(
new DatabaseService()
)
);
$fullService->getData(3);
$fullService->getData(3);
?>
c. Facade Pattern
Problème résolu : Simplifier des systèmes complexes
Fournit une interface simplifiée à un système complexe.
<?php
// Sous-systèmes complexes
class PaymentGateway {
public function processCreditCard($cardNumber, $amount) {
// Logique complexe de traitement de carte
echo "Processing credit card payment of $amount€<br>";
return "CC_TRX_" . uniqid();
}
public function processPayPal($email, $amount) {
echo "Processing PayPal payment of $amount€ to $email<br>";
return "PP_TRX_" . uniqid();
}
}
class InventoryManager {
public function checkStock($productId, $quantity) {
// Logique complexe de vérification de stock
echo "Checking stock for product $productId<br>";
return true; // Simulation
}
public function updateStock($productId, $quantity) {
echo "Updating stock for product $productId (-$quantity)<br>";
return true;
}
}
class ShippingService {
public function calculateShipping($address, $weight) {
// Logique complexe de calcul de frais de port
echo "Calculating shipping to $address<br>";
return 5.99; // Frais de port fixes pour l'exemple
}
public function scheduleDelivery($address, $items) {
echo "Scheduling delivery to $address<br>";
return "DELIVERY_" . uniqid();
}
}
class EmailService {
public function sendOrderConfirmation($email, $orderDetails) {
echo "Sending confirmation email to $email<br>";
return true;
}
}
// Facade : Interface simplifiée
class OrderFacade {
private $paymentGateway;
private $inventoryManager;
private $shippingService;
private $emailService;
public function __construct() {
$this->paymentGateway = new PaymentGateway();
$this->inventoryManager = new InventoryManager();
$this->shippingService = new ShippingService();
$this->emailService = new EmailService();
}
public function placeOrder($orderData) {
echo "=== DÉBUT TRAITEMENT COMMANDE ===<br>";
// 1. Vérifier le stock
foreach ($orderData['items'] as $item) {
if (!$this->inventoryManager->checkStock($item['product_id'], $item['quantity'])) {
throw new Exception("Stock insuffisant pour le produit $item[product_id]");
}
}
// 2. Calculer les frais de port
$shippingCost = $this->shippingService->calculateShipping(
$orderData['shipping_address'],
$orderData['total_weight']
);
// 3. Traiter le paiement
$totalAmount = $orderData['subtotal'] + $shippingCost;
if ($orderData['payment_method'] === 'credit_card') {
$transactionId = $this->paymentGateway->processCreditCard(
$orderData['card_number'],
$totalAmount
);
} elseif ($orderData['payment_method'] === 'paypal') {
$transactionId = $this->paymentGateway->processPayPal(
$orderData['paypal_email'],
$totalAmount
);
}
// 4. Mettre à jour le stock
foreach ($orderData['items'] as $item) {
$this->inventoryManager->updateStock($item['product_id'], $item['quantity']);
}
// 5. Planifier la livraison
$deliveryId = $this->shippingService->scheduleDelivery(
$orderData['shipping_address'],
$orderData['items']
);
// 6. Envoyer confirmation email
$this->emailService->sendOrderConfirmation(
$orderData['customer_email'],
[
'transaction_id' => $transactionId,
'delivery_id' => $deliveryId,
'total_amount' => $totalAmount
]
);
echo "=== COMMANDE TERMINÉE AVEC SUCCÈS ===<br><br>";
return [
'success' => true,
'transaction_id' => $transactionId,
'delivery_id' => $deliveryId,
'total_amount' => $totalAmount
];
}
}
// UTILISATION SIMPLIFIÉE
$orderFacade = new OrderFacade();
$orderData = [
'customer_email' => 'client@example.com',
'payment_method' => 'credit_card',
'card_number' => '4111-1111-1111-1111',
'shipping_address' => '123 Rue Exemple, Paris',
'subtotal' => 99.99,
'total_weight' => 2.5,
'items' => [
['product_id' => 101, 'quantity' => 2],
['product_id' => 202, 'quantity' => 1]
]
];
try {
$result = $orderFacade->placeOrder($orderData);
echo "Résultat: " . json_encode($result, JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo "Erreur: " . $e->getMessage();
}
?>
4️⃣ Patterns Comportementaux
a. Observer Pattern
Problème résolu : Notifications entre objets
Permet à un objet (sujet) de notifier automatiquement d'autres objets (observateurs) des changements d'état.
<?php
// Interface Observer
interface Observer {
public function update($event, $data = null);
}
// Interface Subject (Observable)
interface Subject {
public function attach(Observer $observer);
public function detach(Observer $observer);
public function notify($event, $data = null);
}
// Sujet concret : Gestionnaire d'utilisateurs
class UserManager implements Subject {
private $observers = [];
private $users = [];
public function attach(Observer $observer) {
$this->observers[] = $observer;
}
public function detach(Observer $observer) {
$key = array_search($observer, $this->observers, true);
if ($key !== false) {
unset($this->observers[$key]);
}
}
public function notify($event, $data = null) {
foreach ($this->observers as $observer) {
$observer->update($event, $data);
}
}
public function registerUser($userData) {
$userId = uniqid('user_');
$user = array_merge(['id' => $userId], $userData);
$this->users[$userId] = $user;
// Notifier les observateurs
$this->notify('user_registered', $user);
echo "Utilisateur enregistré: $user[email]<br>";
return $userId;
}
public function updateUserProfile($userId, $profileData) {
if (!isset($this->users[$userId])) {
throw new Exception("Utilisateur non trouvé");
}
$this->users[$userId] = array_merge($this->users[$userId], $profileData);
$this->notify('profile_updated', [
'user_id' => $userId,
'profile_data' => $profileData
]);
echo "Profil mis à jour pour l'utilisateur: $userId<br>";
}
public function makePurchase($userId, $amount) {
if (!isset($this->users[$userId])) {
throw new Exception("Utilisateur non trouvé");
}
$purchaseData = [
'user_id' => $userId,
'amount' => $amount,
'date' => date('Y-m-d H:i:s')
];
$this->notify('purchase_made', $purchaseData);
echo "Achat effectué par $userId : $amount€<br>";
}
}
// Observateurs concrets
class EmailNotifier implements Observer {
public function update($event, $data = null) {
switch ($event) {
case 'user_registered':
$this->sendWelcomeEmail($data['email'], $data['name']);
break;
case 'purchase_made':
$user = $this->getUserById($data['user_id']);
$this->sendPurchaseConfirmation($user['email'], $data['amount']);
break;
}
}
private function sendWelcomeEmail($email, $name) {
echo "[EMAIL] Email de bienvenue envoyé à $email (Bonjour $name!)<br>";
}
private function sendPurchaseConfirmation($email, $amount) {
echo "[EMAIL] Confirmation d'achat envoyée à $email ($amount€)<br>";
}
private function getUserById($userId) {
// Simulation - en réalité on irait chercher en DB
return ['email' => 'user@example.com'];
}
}
class NewsletterManager implements Observer {
private $subscribers = [];
public function update($event, $data = null) {
if ($event === 'user_registered') {
$this->addToNewsletter($data['email'], $data['name']);
}
}
private function addToNewsletter($email, $name) {
$this->subscribers[] = ['email' => $email, 'name' => $name];
echo "[NEWSLETTER] $email ajouté à la newsletter<br>";
}
public function getSubscribers() {
return $this->subscribers;
}
}
class ActivityLogger implements Observer {
private $logs = [];
public function update($event, $data = null) {
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] Événement: $event";
if ($data) {
$logEntry .= " - Données: " . json_encode($data);
}
$this->logs[] = $logEntry;
echo "[LOG] $logEntry<br>";
}
public function getLogs() {
return $this->logs;
}
}
// UTILISATION
$userManager = new UserManager();
// Création des observateurs
$emailNotifier = new EmailNotifier();
$newsletterManager = new NewsletterManager();
$activityLogger = new ActivityLogger();
// Attachement des observateurs
$userManager->attach($emailNotifier);
$userManager->attach($newsletterManager);
$userManager->attach($activityLogger);
echo "=== Enregistrement d'un nouvel utilisateur ===<br>";
$userId = $userManager->registerUser([
'name' => 'Alice Dupont',
'email' => 'alice@example.com'
]);
echo "<br>=== Mise à jour du profil ===<br>";
$userManager->updateUserProfile($userId, [
'phone' => '0123456789'
]);
echo "<br>=== Achat effectué ===<br>";
$userManager->makePurchase($userId, 99.99);
// Détacher un observateur
$userManager->detach($newsletterManager);
echo "<br>=== Nouvel utilisateur (sans newsletter) ===<br>";
$userManager->registerUser([
'name' => 'Bob Martin',
'email' => 'bob@example.com'
]);
?>
b. Strategy Pattern
Problème résolu : Interchangeabilité d'algorithmes
Permet de définir une famille d'algorithmes, de les encapsuler et de les rendre interchangeables.
<?php
// Interface Strategy
interface PaymentStrategy {
public function pay($amount);
public function getName();
}
// Stratégies concrètes
class CreditCardPayment implements PaymentStrategy {
private $cardNumber;
private $cardHolder;
private $cvv;
public function __construct($cardNumber, $cardHolder, $cvv) {
$this->cardNumber = $cardNumber;
$this->cardHolder = $cardHolder;
$this->cvv = $cvv;
}
public function pay($amount) {
// Validation de la carte
if (!$this->validateCard()) {
throw new Exception("Carte invalide");
}
// Logique de paiement par carte
echo "Processing credit card payment of $amount€<br>";
echo "Card: **** **** **** " . substr($this->cardNumber, -4) . "<br>";
return [
'success' => true,
'transaction_id' => 'CC_' . uniqid(),
'amount' => $amount,
'method' => 'credit_card'
];
}
public function getName() {
return "Carte Bancaire";
}
private function validateCard() {
// Validation simple pour l'exemple
return strlen($this->cardNumber) === 16
&& strlen($this->cvv) === 3;
}
}
class PayPalPayment implements PaymentStrategy {
private $email;
private $password;
public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
}
public function pay($amount) {
// Authentification PayPal
if (!$this->authenticate()) {
throw new Exception("Authentification PayPal échouée");
}
echo "Processing PayPal payment of $amount€<br>";
echo "Account: $this->email<br>";
return [
'success' => true,
'transaction_id' => 'PP_' . uniqid(),
'amount' => $amount,
'method' => 'paypal'
];
}
public function getName() {
return "PayPal";
}
private function authenticate() {
// Simulation d'authentification
return !empty($this->email) && !empty($this->password);
}
}
class BankTransferPayment implements PaymentStrategy {
private $iban;
private $bic;
public function __construct($iban, $bic) {
$this->iban = $iban;
$this->bic = $bic;
}
public function pay($amount) {
// Logique de virement bancaire
echo "Processing bank transfer of $amount€<br>";
echo "IBAN: **** **** **** " . substr($this->iban, -4) . "<br>";
echo "BIC: $this->bic<br>";
return [
'success' => true,
'transaction_id' => 'BT_' . uniqid(),
'amount' => $amount,
'method' => 'bank_transfer'
];
}
public function getName() {
return "Virement Bancaire";
}
}
// Contexte qui utilise une stratégie
class ShoppingCart {
private $items = [];
private $paymentStrategy;
public function addItem($item, $price) {
$this->items[] = ['item' => $item, 'price' => $price];
}
public function getTotal() {
$total = 0;
foreach ($this->items as $item) {
$total += $item['price'];
}
return $total;
}
public function setPaymentStrategy(PaymentStrategy $strategy) {
$this->paymentStrategy = $strategy;
}
public function checkout() {
if (empty($this->paymentStrategy)) {
throw new Exception("Aucune méthode de paiement sélectionnée");
}
$total = $this->getTotal();
if ($total <= 0) {
throw new Exception("Le panier est vide");
}
echo "=== CHECKOUT ===<br>";
echo "Méthode de paiement: " . $this->paymentStrategy->getName() . "<br>";
echo "Total: $total€<br>";
return $this->paymentStrategy->pay($total);
}
public function displayItems() {
echo "=== PANIER ===<br>";
foreach ($this->items as $item) {
echo "- $item[item] : $item[price]€<br>";
}
echo "Total: " . $this->getTotal() . "€<br><br>";
}
}
// UTILISATION
$cart = new ShoppingCart();
// Ajout d'articles
$cart->addItem('Laptop', 899.99);
$cart->addItem('Souris', 29.99);
$cart->addItem('Clavier', 59.99);
$cart->displayItems();
echo "=== Test avec Carte Bancaire ===<br>";
$creditCard = new CreditCardPayment(
'4111111111111111',
'Alice Dupont',
'123'
);
$cart->setPaymentStrategy($creditCard);
$result1 = $cart->checkout();
echo "<br>=== Test avec PayPal ===<br>";
$paypal = new PayPalPayment(
'alice@example.com',
'secret123'
);
$cart->setPaymentStrategy($paypal);
$result2 = $cart->checkout();
echo "<br>=== Test avec Virement Bancaire ===<br>";
$bankTransfer = new BankTransferPayment(
'FR7630001007941234567890185',
'BNPAFRPP'
);
$cart->setPaymentStrategy($bankTransfer);
$result3 = $cart->checkout();
echo "<br>=== Résumé des transactions ===<br>";
echo "1. Carte: " . $result1['transaction_id'] . "<br>";
echo "2. PayPal: " . $result2['transaction_id'] . "<br>";
echo "3. Virement: " . $result3['transaction_id'] . "<br>";
?>
c. Command Pattern
Problème résolu : Encapsuler des requêtes
Transforme une requête en objet, permettant de paramétrer, mettre en file d'attente, journaliser ou annuler des opérations.
<?php
// Interface Command
interface Command {
public function execute();
public function undo();
public function getDescription();
}
// Receiver : Éditeur de texte
class TextEditor {
private $content = '';
private $cursorPosition = 0;
public function getContent() {
return $this->content;
}
public function insertText($text, $position = null) {
if ($position === null) {
$position = $this->cursorPosition;
}
$before = substr($this->content, 0, $position);
$after = substr($this->content, $position);
$this->content = $before . $text . $after;
$this->cursorPosition = $position + strlen($text);
return $position;
}
public function deleteText($position, $length) {
if ($position < 0 || $position >= strlen($this->content)) {
return null;
}
$deletedText = substr($this->content, $position, $length);
$before = substr($this->content, 0, $position);
$after = substr($this->content, $position + $length);
$this->content = $before . $after;
$this->cursorPosition = $position;
return $deletedText;
}
public function setCursorPosition($position) {
if ($position >= 0 && $position <= strlen($this->content)) {
$this->cursorPosition = $position;
}
}
public function getCursorPosition() {
return $this->cursorPosition;
}
public function display() {
echo "Contenu: \"" . $this->content . "\"<br>";
echo "Curseur: position $this->cursorPosition<br>";
}
}
// Commandes concrètes
class InsertTextCommand implements Command {
private $editor;
private $text;
private $position;
private $executedPosition;
public function __construct(TextEditor $editor, $text, $position = null) {
$this->editor = $editor;
$this->text = $text;
$this->position = $position;
}
public function execute() {
$this->executedPosition = $this->editor->insertText($this->text, $this->position);
}
public function undo() {
$this->editor->deleteText($this->executedPosition, strlen($this->text));
}
public function getDescription() {
return "Insérer '" . substr($this->text, 0, 20) . "...' à la position $this->position";
}
}
class DeleteTextCommand implements Command {
private $editor;
private $position;
private $length;
private $deletedText;
public function __construct(TextEditor $editor, $position, $length) {
$this->editor = $editor;
$this->position = $position;
$this->length = $length;
}
public function execute() {
$this->deletedText = $this->editor->deleteText($this->position, $this->length);
return $this->deletedText !== null;
}
public function undo() {
if ($this->deletedText !== null) {
$this->editor->insertText($this->deletedText, $this->position);
}
}
public function getDescription() {
return "Supprimer $this->length caractère(s) à la position $this->position";
}
}
class MoveCursorCommand implements Command {
private $editor;
private $newPosition;
private $oldPosition;
public function __construct(TextEditor $editor, $newPosition) {
$this->editor = $editor;
$this->newPosition = $newPosition;
}
public function execute() {
$this->oldPosition = $this->editor->getCursorPosition();
$this->editor->setCursorPosition($this->newPosition);
}
public function undo() {
$this->editor->setCursorPosition($this->oldPosition);
}
public function getDescription() {
return "Déplacer le curseur à la position $this->newPosition";
}
}
// Invoker : Gestionnaire de commandes (avec undo/redo)
class CommandManager {
private $undoStack = [];
private $redoStack = [];
private $history = [];
public function executeCommand(Command $command) {
try {
$command->execute();
$this->undoStack[] = $command;
$this->redoStack = []; // Vider redo quand on fait une nouvelle action
$this->history[] = [
'time' => date('H:i:s'),
'command' => $command->getDescription()
];
echo "✓ Command exécutée: " . $command->getDescription() . "<br>";
return true;
} catch (Exception $e) {
echo "✗ Erreur: " . $e->getMessage() . "<br>";
return false;
}
}
public function undo() {
if (empty($this->undoStack)) {
echo "Rien à annuler<br>";
return false;
}
$command = array_pop($this->undoStack);
$command->undo();
$this->redoStack[] = $command;
echo "↶ Annulé: " . $command->getDescription() . "<br>";
return true;
}
public function redo() {
if (empty($this->redoStack)) {
echo "Rien à refaire<br>";
return false;
}
$command = array_pop($this->redoStack);
$command->execute();
$this->undoStack[] = $command;
echo "↷ Refait: " . $command->getDescription() . "<br>";
return true;
}
public function showHistory() {
echo "=== HISTORIQUE ===<br>";
foreach ($this->history as $index => $entry) {
echo "$index+1. [$entry[time]] $entry[command]<br>";
}
}
public function showStatus() {
echo "=== ÉTAT ===<br>";
echo "Undo stack: " . count($this->undoStack) . " commandes<br>";
echo "Redo stack: " . count($this->redoStack) . " commandes<br>";
}
}
// UTILISATION
$editor = new TextEditor();
$commandManager = new CommandManager();
echo "=== ÉDITEUR DE TEXTE AVEC UNDO/REDO ===<br><br>";
// État initial
$editor->display();
echo "<br>";
// Exécution de commandes
$commandManager->executeCommand(new InsertTextCommand($editor, "Bonjour le monde!"));
$editor->display();
echo "<br>";
$commandManager->executeCommand(new MoveCursorCommand($editor, 8));
$editor->display();
echo "<br>";
$commandManager->executeCommand(new InsertTextCommand($editor, "tout "));
$editor->display();
echo "<br>";
$commandManager->executeCommand(new DeleteTextCommand($editor, 0, 7));
$editor->display();
echo "<br>";
// Annulation
echo "=== ANNULATION ===<br>";
$commandManager->undo();
$editor->display();
echo "<br>";
$commandManager->undo();
$editor->display();
echo "<br>";
// Refaire
echo "=== REFAIRE ===<br>";
$commandManager->redo();
$editor->display();
echo "<br>";
$commandManager->redo();
$editor->display();
echo "<br>";
// Historique et état
$commandManager->showHistory();
echo "<br>";
$commandManager->showStatus();
?>
5️⃣ Patterns Spécifiques PHP/Laravel
a. Repository Pattern
Problème résolu : Abstraction de l'accès aux données
Cache les détails d'accès aux données derrière une interface, permettant de changer facilement de source de données.
Exemple : UserRepository, ProductRepository
- Abstraction de l'accès à la base de données
- Facilite les tests unitaires (mocking)
- Séparation des responsabilités
- Réutilisation de requêtes complexes
b. Service Container
Problème résolu : Injection de dépendances
Gère automatiquement l'instanciation et l'injection des dépendances entre classes.
Exemple : Implémentation simple en PHP pur
- Résolution automatique des dépendances
- Gestion du cycle de vie des objets
- Facilite les tests et la maintenance
- Base du framework Laravel
c. Middleware Pattern
Problème résolu : Traitement de requêtes en chaîne
Permet de traiter les requêtes HTTP à travers une série de middlewares avant d'atteindre le contrôleur.
Exemple : Authentification, Logging, CORS
- Pipeline de traitement des requêtes
- Réutilisabilité des composants
- Flexibilité dans l'ordre d'exécution
- Utilisé dans Laravel, Symfony, Express.js
6️⃣ Projet Pratique : Système E-commerce
Contexte : Application de vente en ligne
Création d'un mini-système e-commerce utilisant plusieurs design patterns vus précédemment.
Architecture et Patterns utilisés :
- Factory Pattern : Création de produits (physiques, numériques, abonnements)
- Observer Pattern : Notifications (email, SMS, push) pour commandes et stocks
- Strategy Pattern : Différents moyens de paiement (carte, PayPal, virement)
- Repository Pattern : Accès aux données produits, commandes, utilisateurs
- Decorator Pattern : Ajout de taxes, réductions, frais de port
- Singleton Pattern : Connexion à la base de données
Structure du projet :
ecommerce-project/
src/
Factory/ # ProductFactory, UserFactory
Observer/ # EmailNotifier, StockObserver
Strategy/ # PaymentStrategy implementations
Repository/ # ProductRepository, OrderRepository
Decorator/ # PriceDecorator, TaxDecorator
Singleton/ # DatabaseConnection
Models/ # Product, Order, User, Cart
Services/ # OrderService, PaymentService
tests/ # Tests unitaires et d'intégration
public/ # Point d'entrée de l'application
config/ # Configuration (DB, paiement, etc.)
Scénario d'utilisation :
- Un utilisateur parcourt les produits (Repository Pattern)
- Il ajoute des produits au panier (Factory Pattern pour les produits)
- Il procède au checkout (Strategy Pattern pour le paiement)
- La commande est créée (Observer Pattern pour les notifications)
- Le prix final est calculé (Decorator Pattern pour taxes et réductions)
- Tout est persisté en base (Singleton Pattern pour la connexion DB)
💡 Avantages de cette architecture :
- Maintenable : Chaque pattern a une responsabilité unique
- Testable : Facile à tester unitairement
- Évolutif : Ajout facile de nouvelles fonctionnalités
- Flexible : Changement facile de composants
- Professionnel : Architecture similaire aux projets réels
Note : Pour des raisons de longueur, le code complet n'est pas inclus ici, mais chaque composant serait similaire aux exemples précédents, adapté au contexte e-commerce.
7️⃣ Tableau Comparatif des Design Patterns
| Pattern | Catégorie | Problème résolu | Quand l'utiliser | Complexité |
|---|---|---|---|---|
| Singleton | Création | Instance unique d'une classe | Connexions DB, Logger, Configuration | ★☆☆ |
| Factory | Création | Création flexible d'objets | Systèmes extensibles, multiples types d'objets | ★★☆ |
| Builder | Création | Construction d'objets complexes | Objets avec nombreux paramètres, requêtes SQL | ★★☆ |
| Adapter | Structurel | Interfaces incompatibles | Intégration d'APIs externes, legacy code | ★☆☆ |
| Decorator | Structurel | Ajout de fonctionnalités dynamiques | Systèmes de cache, logging, validation | ★★☆ |
| Facade | Structurel | Interface simplifiée à un système complexe | APIs complexes, sous-systèmes nombreux | ★☆☆ |
| Observer | Comportemental | Notifications entre objets | Événements, newsletters, notifications | ★★☆ |
| Strategy | Comportemental | Algorithmes interchangeables | Paiements, tris, algorithmes multiples | ★★☆ |
| Command | Comportemental | Encapsulation de requêtes | Undo/redo, files d'attente, transactions | ★★★ |
| Repository | PHP/Laravel | Abstraction accès données | Applications complexes, tests unitaires | ★★★ |
📊 Légende complexité :
- ★☆☆ Faible : Simple à comprendre et implémenter
- ★★☆ Moyenne : Quelques concepts à maîtriser
- ★★★ Élevée : Plusieurs concepts, implémentation complexe
8️⃣ Bonnes Pratiques et Pièges à Éviter
Quand utiliser (et ne pas utiliser) les patterns
✅ À FAIRE :
- Utiliser les patterns pour résoudre des problèmes réels, pas pour faire "joli"
- Commencer simple et refactoriser vers les patterns quand nécessaire
- Documenter pourquoi un pattern est utilisé
- Rester cohérent dans toute l'application
- Privilégier la lisibilité sur le pattern parfait
❌ À ÉVITER :
- Patternitis : Utiliser des patterns partout, même quand c'est inutile
- Complexité prématurée : Commencer avec des patterns complexes
- Ignorer le contexte : Utiliser un pattern parce que "c'est à la mode"
- Sacrifier la performance pour des patterns inutiles
- Copier-coller sans comprendre le problème résolu
Performance vs Maintenabilité
Les design patterns ajoutent une couche d'abstraction qui peut impacter légèrement les performances. Cependant, cet impact est généralement négligeable face aux avantages en maintenabilité.
📈 Règle générale :
Optimisez pour la maintenabilité d'abord, la performance ensuite. La plupart des applications n'ont pas besoin d'optimisations micro. Si vous avez des problèmes de performance, profilez d'abord pour identifier les vrais goulots d'étranglement.
Testing des patterns
- Singletons : Difficiles à tester, préférer l'injection de dépendances
- Factories : Faciles à tester avec des mocks
- Observers : Tester les notifications et les réactions
- Strategies : Tester chaque stratégie indépendamment
- Repositories : Utiliser des bases de données en mémoire pour les tests
Évolution des patterns dans PHP 8+
Nouvelles fonctionnalités PHP 8.x :
- Attributes : Métadonnées pour les patterns (routing, validation)
- Union Types : Meilleure typage pour les factories et stratégies
- Match Expression : Alternative élégante aux switch dans les factories
- Constructor Property Promotion : Réduction du boilerplate dans les DTOs
- Enums : Parfaits pour les états dans State Pattern
Patterns et frameworks modernes
- Laravel : Service Container (DI), Eloquent (Active Record + Repository)
- Symfony : Dependency Injection, EventDispatcher (Observer)
- ReactPHP : Event Loop (Reactor Pattern)
- Swoole : Coroutines pour des patterns asynchrones
9️⃣ Conclusion
Récapitulatif des patterns essentiels
Les design patterns ne sont pas des solutions magiques, mais des outils dans votre boîte à outils. Les plus importants à maîtriser :
- Créationnels : Factory (flexibilité), Singleton (avec parcimonie)
- Structurels : Adapter (intégration), Decorator (extensibilité)
- Comportementaux : Observer (événements), Strategy (algorithmes)
- PHP/Laravel : Repository (data access), Service Container (DI)
Prochaines étapes : Design Patterns avancés
- Patterns de conception d'entreprise : DDD, CQRS, Event Sourcing
- Patterns d'architecture : MVC, MVVM, Hexagonal, Clean Architecture
- Patterns spécifiques : Pub/Sub, Circuit Breaker, Retry Pattern
- Patterns asynchrones : Reactor, Proactor, Async/Await patterns
Ressources pour aller plus loin
📚 Livres recommandés :
- "Design Patterns: Elements of Reusable Object-Oriented Software" (GoF)
- "Head First Design Patterns" (O'Reilly)
- "Patterns of Enterprise Application Architecture" (Martin Fowler)
- "Clean Architecture" (Robert C. Martin)
🌐 Ressources en ligne :
- Refactoring.Guru - Explications illustrées des patterns
- Laravel Documentation - Patterns implémentés dans le framework
- PHP FIG Standards - Standards pour l'écosystème PHP
- GitHub - Projets open source utilisant divers patterns
📚 Vous maîtrisez PHP ? Passez à la vitesse supérieure !
La programmation orientée objet (POO) est LA compétence qui sépare les développeurs débutants des professionnels.
👉 Consultez notre guide complet : "Introduction à la programmation orientée objet (POO) en PHP"
Au programme : classes, objets, héritage, encapsulation, polymorphisme et des exemples pratiques pour structurer votre code comme un pro.
💭 Dernières pensées
Les design patterns sont comme un langage commun entre développeurs. Ils permettent de communiquer des solutions complexes avec un simple nom. Cependant, souvenez-vous :
"Les patterns sont des guides, pas des lois." Adaptez-les à vos besoins, comprenez les problèmes qu'ils résolvent, et n'hésitez pas à les combiner ou les modifier pour votre contexte spécifique.
Le vrai pouvoir des design patterns ne réside pas dans leur application mécanique, mais dans la façon dont ils changent votre façon de penser la conception logicielle.