Laravel Intermédiaire

Architecture Scalable avec Laravel : Patterns et Bonnes Pratiques Professionnelles

Maîtrisez les patterns architecturaux avancés de Laravel pour construire des applications robustes et maintenables en production. Découvrez comment structurer votre code comme les grands projets d'entreprise.

Preparetoi.academy 45 min

1. Les Principes SOLID appliqués à Laravel

Définition : Les principes SOLID sont cinq règles de conception fondamentales qui rendent le code plus flexible, maintenable et testable. En Laravel, ils s'appliquent particulièrement bien grâce à l'architecture modulaire du framework et à son conteneur de dépendances puissant.

Analogie : Si votre application Laravel était un immeuble, les principes SOLID seraient les règles de construction qui garantissent que chaque étage peut être rénové sans effondrer l'ensemble de la structure. Chaque pièce (classe) a une responsabilité unique, les murs sont indépendants, et les modifications ne cascadent pas partout.

Principe Application Laravel Bénéfice
Single Responsibility Une classe = une seule raison de changer (Ex: UserRepository pour les données) Maintenabilité accrue
Open/Closed Ouvert à l'extension, fermé à la modification via interfaces et contrats Extensibilité sans risque
Liskov Substitution Les implémentations interchangeables respectent le contrat Polymorphisme fiable
Interface Segregation Interfaces spécifiques plutôt que génériques Dépendances minimalistes
Dependency Inversion Dépendre d'abstractions, pas de concrétions (injection Laravel) Couplage faible

Astuce : Utilisez les contrats (interfaces) de Laravel systématiquement. Créez une interface PaymentGatewayInterface plutôt que de dépendre directement de StripePayment. Cela vous permet de swapper Stripe pour PayPal sans toucher votre contrôleur.

Attention : Ne tombez pas dans le piège de l'over-engineering. Un petit script n'a pas besoin de 5 couches d'abstraction. Les SOLID deviennent cruciaux avec les équipes et les projets évolutifs. Évaluez le coût-bénéfice pour chaque classe.

En pratique, voici comment implémenter Dependency Inversion dans un contrôleur Laravel :

// ❌ Mauvais - couplé à Stripe
class OrderController {
    public function pay(Order $order) {
        $stripe = new StripePayment();
        $stripe->charge($order->total);
    }
}

// ✅ Bon - dépend d'une abstraction
interface PaymentProcessor {
    public function process(float $amount): bool;
}

class OrderController {
    public function __construct(private PaymentProcessor $processor) {}
    
    public function pay(Order $order) {
        $this->processor->process($order->total);
    }
}

Cette approche rend vos tests unitaires possibles et votre code testable sans API réelle.


2. Le Pattern Repository et les Contrats de Données

Définition : Le pattern Repository encapsule la logique d'accès aux données en créant une couche intermédiaire entre votre domaine métier et votre source de données (base de données, API, cache). C'est l'abstraction de votre persistance.

Analogie : Un Repository est comme une bibliothèque. Vous ne cherchez pas vous-même les livres en rayonnage (requête SQL directe dans le contrôleur). Vous demandez au bibliothécaire (Repository) qui connaît l'organisation et vous apporte exactement ce dont vous avez besoin.

Aspect Sans Repository Avec Repository
Localisation logique requêtes Dispersée (contrôleurs, modèles) Centralisée et réutilisable
Testabilité Difficile (dépend BD réelle) Facile (mock du Repository)
Changement BD Impacte tout le code Changement isolé au Repository
Réutilisabilité requêtes Duplication Code partagé
Complexité Croît rapidement Reste organisée

Astuce : Créez un Repository abstract de base pour hériter des méthodes CRUD communes :

abstract class BaseRepository {
    public function __construct(protected Model $model) {}
    
    public function find($id) { return $this->model->find($id); }
    public function all() { return $this->model->all(); }
    public function create(array $data) { return $this->model->create($data); }
    public function update($id, array $data) { return $this->model->find($id)->update($data); }
}

class UserRepository extends BaseRepository {
    public function findActiveByEmail(string $email) {
        return $this->model->where('email', $email)->where('active', true)->first();
    }
}

Attention : Le Repository ne doit pas contenir de logique métier complexe, seulement l'accès aux données. Si vous avez des règles de calcul, mettez-les dans une classe de Service ou un Value Object.

Les Repositories brillent particulièrement avec les interfaces :

interface UserRepositoryInterface {
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function save(User $user): void;
}

class EloquentUserRepository implements UserRepositoryInterface {
    // Implémentation avec Eloquent
}

// En cas de migration future (MongoDB par exemple)
class MongoUserRepository implements UserRepositoryInterface {
    // Implémentation avec MongoDB
}

Cette abstraction fait que vos contrôleurs ignorent totalement la source de données réelle.


3. Les Services et la Logique Métier Complexe

Définition : Une classe Service encapsule la logique métier de haut niveau, orchestrant les repositories, les modèles et les dépendances externes. Elle est la couche qui dit "comment réaliser un processus complet", contrairement au Repository qui dit "comment récupérer des données".

Analogie : Si le Repository est le bibliothécaire qui apporte les livres, le Service est le professeur qui les utilise pour enseigner une leçon complète. Le professeur coordonne plusieurs ressources pour atteindre un objectif pédagogique.

Responsabilité Lieu Exemple
Récupérer des données Repository $user = $userRepo->find(1)
Persister des données Repository $userRepo->save($user)
Orchestrer un processus métier Service Créer un utilisateur + l'inscrire à une newsletter + envoyer email
Validation métier Service Vérifier les règles complexes
Appels externes Service API, notifications, paiements

Astuce : Nommez clairement vos Services avec des verbes d'action : CreateOrderService, ProcessRefundService, GenerateInvoiceService. Cela rend leur responsabilité évidente.

class CreateOrderService {
    public function __construct(
        private OrderRepository $orders,
        private InventoryService $inventory,
        private PaymentProcessor $payment,
        private NotificationService $notifications
    ) {}
    
    public function execute(CreateOrderRequest $request): Order {
        // 1. Vérifier la disponibilité du stock
        $this->inventory->reserve($request->items);
        
        // 2. Créer la commande
        $order = $this->orders->create($request->toArray());
        
        // 3. Traiter le paiement
        try {
            $this->payment->process($order);
        } catch (PaymentException $e) {
            $this->inventory->unreserve($request->items);
            throw $e;
        }
        
        // 4. Notifier le client
        $this->notifications->sendConfirmation($order);
        
        return $order;
    }
}

// Utilisation dans le contrôleur
class OrderController {
    public function store(CreateOrderRequest $request, CreateOrderService $service) {
        $order = $service->execute($request);
        return response()->json($order);
    }
}

Attention : Les Services peuvent devenir des "God Objects" gigantesques. Si un Service fait plus de 300 lignes, découpez-le en plusieurs Services plus petits avec responsabilités claires. Chaque Service devrait avoir une raison unique de changer.


4. Actions et Value Objects pour la Clarté du Code

Définition : Une Action est une classe qui représente une tâche atomique unique, complètement indépendante et réutilisable. Un Value Object est un objet immuable qui représente une valeur (comme Money, Email, PhoneNumber) plutôt qu'une entité identifiée.

Analogie : Les Actions sont des LEGO : chaque bloc fait une seule chose parfaitement. Les Value Objects sont les propriétés du LEGO (couleur, taille) : elles définissent quoi l'objet est, pas juste quelle données il contient.

Concept Quand l'utiliser Exemple
Action Une tâche discrète, atomique, réutilisable SendWelcomeEmailAction, CalculateShippingAction
Service Orchestration de plusieurs étapes CreateOrderService (coordonne plusieurs actions)
Value Object Représenter une valeur métier avec logique Money, Email, PhoneNumber
Entity Identité persistante avec état User, Order, Product

Astuce : Créez une classe Action abstraite pour la cohérence :

abstract class Action {
    abstract public function handle(...$arguments);
    
    public function __invoke(...$arguments) {
        return $this->handle(...$arguments);
    }
}

class SendWelcomeEmailAction extends Action {
    public function handle(User $user): void {
        Mail::to($user->email)->send(new WelcomeMail($user));
    }
}

// Utilisation flexible
$action = app(SendWelcomeEmailAction::class);
$action($user); // Via __invoke
// ou
$action->handle($user); // Direct

Les Value Objects apportent la sécurité des types et la logique métier aux propriétés :

class Money {
    private int $amount; // En centimes
    private string $currency;
    
    public function __construct(int $amount, string $currency = 'EUR') {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
        $this->amount = $amount;
        $this->currency = $currency;
    }
    
    public function add(Money $other): Money {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Different currencies');
        }
        return new Money($this->amount + $other->amount, $this->currency);
    }
    
    public function format(): string {
        return number_format($this->amount / 100, 2) . ' ' . $this->currency;
    }
}

// Utilisation type-safe
$order->price = new Money(9999, 'EUR'); // 99.99 EUR
$shipping = new Money(500, 'EUR');
$total = $order->price->add($shipping);

Attention : N'abusez pas des Value Objects. Un simple int pour une quantité suffit. Utilisez les Value Objects quand la logique métier justifie la complexité.


5. Événements, Listeners et Architecture Événementielle

Définition : L'architecture événementielle en Laravel découple les composants en utilisant des événements. Quand quelque chose d'important se produit dans votre domaine (UserCreated, OrderPlaced), vous déclenchez un événement que d'autres composants peuvent écouter sans couplage direct.

Analogie : Imaginez un système de notifications de ville. Quand un événement (concert, marché) se produit, la mairie émet un annonce. Chaque citoyen peut s'inscrire aux annonces qui l'intéressent, sans que la mairie connaisse chaque citoyen individuellement. L'ajout d'un nouveau type de citoyen intéressé ne change pas le système d'annonce.

Élément Rôle Exemple
Event Ce qui s'est passé UserRegistered
Dispatcher Qui déclenche l'événement Votre code métier
Listener Qui écoute SendWelcomeEmailListener
Handler Quoi faire en réaction Envoyer l'email

Astuce : Utilisez les événements de domaine pour découpler la logique asynchrone :

// 1. Définir l'événement
class UserRegistered {
    public function __construct(public User $user) {}
}

// 2. Déclencher l'événement dans le service
class RegisterUserService {
    public function execute(RegisterRequest $request): User {
        $user = User::create($request->validated());
        
        event(new UserRegistered($user)); // Fire and forget
        
        return $user;
    }
}

// 3. Définir les listeners (peuvent être multiples)
class SendWelcomeEmailListener {
    public function handle(UserRegistered $event): void {
        Mail::to($event->user->email)->send(new WelcomeMail($event->user));
    }
}

class AddUserToNewsletterListener {
    public function handle(UserRegistered $event): void {
        Newsletter::subscribe($event->user->email);
    }
}

class LogUserRegistrationListener {
    public function handle(UserRegistered $event): void {
        Log::info('User registered', ['user_id' => $event->user->id]);
    }
}

// 4. Enregistrer dans EventServiceProvider
protected $listen = [
    UserRegistered::class => [
        SendWelcomeEmailListener::class,
        AddUserToNewsletterListener::class,
        LogUserRegistrationListener::class,
    ],
];

Attention : Les événements synchrones peuvent ralentir votre réponse si vos listeners sont lents (appels API, gros calculs). Utilisez ShouldQueue pour faire tourner les listeners en background :

class SendWelcomeEmailListener implements ShouldQueue {
    public function handle(UserRegistered $event): void {
        // S'exécutera en queue, pas dans la requête
    }
}

L'architecture événementielle brille particulièrement pour l'audit, les notifications et les intégrations tierces :

  • Audit : Enregistrer chaque action importante
  • Notifications : Email, SMS, push sans polluer le code métier
  • Intégrations : Envoyer à Stripe, Segment, Firebase en parallèle
  • Cache invalidation : Invalider le cache quand les données changent

Cette approche rend votre code testable (mocker les événements) et votre architecture vraiment découplée, clé de la scalabilité professionnelle.

Accédez à des centaines d'examens QCM — Découvrir les offres Premium