ahmed-bhs/hexagonal-maker-bundle
layout: default
En architecture hexagonale, l'Injection de Dépendances (DI) est critique pour lier ports (interfaces) à adaptateurs (implémentations). Le conteneur DI de Symfony gère ce câblage automatiquement avec configuration appropriée.
// ❌ Sans DI : Dépendance codée en dur
class RegisterUserHandler
{
public function __invoke(RegisterUserCommand $command): void
{
$repository = new DoctrineUserRepository(); // ❌ Couplage fort!
// ...
}
}
// ✅ Avec DI : Dépendance injectée
class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $users // ✅ Interface, pas implémentation
) {}
public function __invoke(RegisterUserCommand $command): void
{
$this->users->save(...); // Utilise implémentation configurée
}
}
**Configuration Conteneur DI :** Dit à Symfony "quand quelqu'un a besoin de `UserRepositoryInterface`, donne-lui `DoctrineUserRepository`".
---
## Bases Autowiring Symfony
### Qu'est-ce que l'Autowiring ?
```php
// Handler avec dépendances type-hintées
class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $users, // Autowired
private EmailSenderInterface $emailSender, // Autowired
private EventDispatcherInterface $events, // Autowired
) {}
}
Symfony voit ces type hints et fournit automatiquement services corrects.
---
### Activer Autowiring
```yaml
services:
_defaults:
autowire: true # Activer autowiring
autoconfigure: true # Configurer automatiquement services (tags, etc.)
public: false # Services privés par défaut
# Auto-enregistrer toutes classes dans src/ comme services
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/*/Domain/Model/' # Exclure entités
- '../src/*/Domain/ValueObject/' # Exclure value objects
- '../src/Kernel.php'
**Comment ça marche :**
1. Symfony scanne répertoire `src/`
3. Autowire dépendances constructeur
---
## Liaison Ports vers Adaptateurs
### Configuration de Base
```php
// Handler nécessite UserRepositoryInterface
class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $users // ❌ Interface, ne peut être instanciée!
) {}
}
**Solution :** Lier interface à implémentation dans `services.yaml`.
---
```yaml
services:
# Lier interface → implémentation
App\User\Domain\Port\UserRepositoryInterface:
class: App\User\Infrastructure\Persistence\DoctrineUserRepository
**Explication :**
- Symfony fournit `DoctrineUserRepository`
---
```yaml
services:
# Implémentation
App\User\Infrastructure\Persistence\DoctrineUserRepository:
# Autowired par défaut
# Alias : interface → implémentation
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\DoctrineUserRepository
**Bénéfices :**
- Plus facile changer implémentations
---
```yaml
services:
_defaults:
bind:
# Lier automatiquement cette interface à cette implémentation
# pour TOUS les services
App\User\Domain\Port\UserRepositoryInterface: '[@App](https://github.com/App)\User\Infrastructure\Persistence\DoctrineUserRepository'
**Quand utiliser :** Interface utilisée dans beaucoup d'endroits, éviter répéter configuration.
---
## Liaisons Spécifiques Environnement
### Le Problème : Implémentations Différentes pour Environnements Différents
- **Développement :** Utiliser repository en mémoire (tests rapides)
- **Production :** Utiliser repository Doctrine (vraie base données)
---
### Solution : Configuration Spécifique Environnement
```yaml
services:
_defaults:
autowire: true
autoconfigure: true
# Enregistrer toutes implémentations
App\User\Infrastructure\Persistence\DoctrineUserRepository:
App\User\Infrastructure\Persistence\InMemoryUserRepository:
# Liaison par défaut (production)
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\DoctrineUserRepository
---
```yaml
services:
# Surcharger liaison pour environnement test
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\InMemoryUserRepository
**Résultat :**
- Production → utilise `DoctrineUserRepository`
---
```yaml
# Optionnel : utiliser en mémoire pour feedback dev rapide
services:
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\InMemoryUserRepository
# Ou activer logging debug
App\User\Infrastructure\Persistence\DoctrineUserRepository:
decorates: App\User\Infrastructure\Persistence\DoctrineUserRepository
arguments:
$decorated: '@.inner'
$logger: '[@logger](https://github.com/logger)'
---
```yaml
# config/services.yaml (défaut : production)
services:
# Production : vrai SMTP
App\Notification\Domain\Port\EmailSenderInterface:
alias: App\Notification\Infrastructure\Email\SymfonyEmailSender
# config/services_test.yaml
services:
# Test : fake en mémoire
App\Notification\Domain\Port\EmailSenderInterface:
alias: App\Notification\Infrastructure\Email\InMemoryEmailSender
# config/services_dev.yaml
services:
# Dev : logger emails au lieu d'envoyer
App\Notification\Domain\Port\EmailSenderInterface:
alias: App\Notification\Infrastructure\Email\LoggingEmailSender
---
## Services Taggués
### Le Problème : Multiples Implémentations Même Interface
```php
interface EventSubscriberInterface
{
public function handle(DomainEvent $event): void;
}
class SendEmailSubscriber implements EventSubscriberInterface { /* ... */ }
class LogEventSubscriber implements EventSubscriberInterface { /* ... */ }
class UpdateCacheSubscriber implements EventSubscriberInterface { /* ... */ }
**Besoin :** Injecter toutes implémentations, pas juste une.
---
### Solution : Services Taggués
```yaml
services:
# Taguer chaque implémentation
App\Notification\Infrastructure\Event\SendEmailSubscriber:
tags: ['app.event_subscriber']
App\Notification\Infrastructure\Event\LogEventSubscriber:
tags: ['app.event_subscriber']
App\Notification\Infrastructure\Event\UpdateCacheSubscriber:
tags: ['app.event_subscriber']
---
```yaml
services:
# Event dispatcher reçoit tous subscribers
App\Shared\Infrastructure\Event\EventDispatcher:
arguments:
$subscribers: !tagged_iterator app.event_subscriber
---
```php
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class EventDispatcher implements EventDispatcherInterface
{
public function __construct(
#[TaggedIterator('app.event_subscriber')]
private iterable $subscribers // Tous services taggués injectés ici
) {}
public function dispatch(DomainEvent $event): void
{
foreach ($this->subscribers as $subscriber) {
$subscriber->handle($event);
}
}
}
---
### Auto-Tagging avec Interfaces
```yaml
services:
_instanceof:
# Taguer automatiquement toutes classes implémentant EventSubscriberInterface
App\Shared\Domain\Event\EventSubscriberInterface:
tags: ['app.event_subscriber']
**Maintenant vous n'avez pas besoin de taguer manuellement chaque implémentation !**
---
```yaml
services:
_instanceof:
# Auto-taguer tous query handlers
App\Shared\Application\Query\QueryHandlerInterface:
tags: ['app.query_handler']
# Query bus reçoit tous handlers
App\Shared\Infrastructure\Query\QueryBus:
arguments:
$handlers: !tagged_iterator app.query_handler
class QueryBus
{
public function __construct(
#[TaggedIterator('app.query_handler')]
private iterable $handlers
) {}
public function dispatch(Query $query): mixed
{
foreach ($this->handlers as $handler) {
if ($handler->supports($query)) {
return $handler->handle($query);
}
}
throw new NoHandlerFoundException();
}
}
---
## Décoration de Services
### Le Problème : Ajouter Fonctionnalité Sans Modifier Code
**Exemple :** Ajouter cache au repository sans changer code repository.
---
### Solution : Pattern Decorator avec DI
```php
namespace App\User\Infrastructure\Persistence;
final readonly class CachedUserRepository implements UserRepositoryInterface
{
public function __construct(
private UserRepositoryInterface $decorated, // Repository original
private CacheInterface $cache,
) {}
public function findById(UserId $id): ?User
{
return $this->cache->get(
"user:{$id}",
fn() => $this->decorated->findById($id) // Déléguer à l'original
);
}
public function save(User $user): void
{
$this->decorated->save($user);
$this->cache->delete("user:{$user->getId()}"); // Invalider cache
}
}
---
```yaml
services:
# Repository original
App\User\Infrastructure\Persistence\DoctrineUserRepository:
# Décorateur enveloppe l'original
App\User\Infrastructure\Persistence\CachedUserRepository:
decorates: App\User\Infrastructure\Persistence\DoctrineUserRepository
arguments:
$decorated: '@.inner' # @.inner = le service décoré
**Résultat :**
- Quiconque demande `DoctrineUserRepository` obtient `CachedUserRepository`
- Cache ajouté de façon transparente
---
### Priorité Décoration
```yaml
services:
App\User\Infrastructure\Persistence\DoctrineUserRepository:
# Premier décorateur : cache
App\User\Infrastructure\Persistence\CachedUserRepository:
decorates: App\User\Infrastructure\Persistence\DoctrineUserRepository
decoration_priority: 10 # Priorité plus élevée = couche externe
arguments:
$decorated: '@.inner'
# Second décorateur : logging
App\User\Infrastructure\Persistence\LoggingUserRepository:
decorates: App\User\Infrastructure\Persistence\DoctrineUserRepository
decoration_priority: 5 # Priorité plus basse = couche interne
arguments:
$decorated: '@.inner'
Handler → LoggingUserRepository → CachedUserRepository → DoctrineUserRepository → Base de Données
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
# Auto-enregistrer tous services
App\:
resource: '../src/'
exclude:
- '../src/*/Domain/Model/'
- '../src/*/Domain/ValueObject/'
- '../src/Kernel.php'
# Lier ports à adaptateurs
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\DoctrineUserRepository
App\User\Domain\Port\PasswordHasherInterface:
alias: App\User\Infrastructure\Security\SymfonyPasswordHasher
App\Shared\Domain\Port\EmailSenderInterface:
alias: App\Shared\Infrastructure\Email\SymfonyEmailSender
App\Shared\Domain\Port\EventDispatcherInterface:
alias: App\Shared\Infrastructure\Event\SymfonyEventDispatcher
---
```yaml
services:
# Côté écriture
App\Order\Domain\Port\OrderRepositoryInterface:
alias: App\Order\Infrastructure\Persistence\DoctrineOrderRepository
# Côté lecture (CQRS)
App\Order\Application\Query\OrderQueryInterface:
alias: App\Order\Infrastructure\Query\SqlOrderQuery
# Services externes
App\Order\Domain\Port\PaymentProcessorInterface:
alias: App\Order\Infrastructure\Payment\StripePaymentProcessor
App\Order\Domain\Port\InventoryServiceInterface:
alias: App\Order\Infrastructure\Inventory\HttpInventoryService
---
```yaml
services:
# Résolveur tenant
App\Shared\Infrastructure\Tenancy\TenantResolver:
# Repository tenant-aware
App\User\Infrastructure\Persistence\TenantAwareUserRepository:
arguments:
$tenantResolver: '[@App](https://github.com/App)\Shared\Infrastructure\Tenancy\TenantResolver'
# Lier interface à implémentation tenant-aware
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\TenantAwareUserRepository
---
```yaml
services:
# Repository de base
App\Product\Infrastructure\Persistence\DoctrineProductRepository:
# Décorateur : cache
App\Product\Infrastructure\Persistence\CachedProductRepository:
decorates: App\Product\Infrastructure\Persistence\DoctrineProductRepository
decoration_priority: 10
arguments:
$decorated: '@.inner'
$cache: '[@cache](https://github.com/cache).app'
# Décorateur : logging
App\Product\Infrastructure\Persistence\LoggingProductRepository:
decorates: App\Product\Infrastructure\Persistence\DoctrineProductRepository
decoration_priority: 5
arguments:
$decorated: '@.inner'
$logger: '[@logger](https://github.com/logger)'
# Lier interface à la base (décorateurs l'enveloppent automatiquement)
App\Product\Domain\Port\ProductRepositoryInterface:
alias: App\Product\Infrastructure\Persistence\DoctrineProductRepository
**Chaîne d'appel :** `Handler → Logging → Caching → Doctrine → BD`
---
## Dépannage
### Problème 1 : "Cannot autowire service: argument is type-hinted with interface"
Cannot autowire service "App\User\Application\Handler\RegisterUserHandler": argument "$users" of method "__construct()" is type-hinted with the interface "App\User\Domain\Port\UserRepositoryInterface" but no implementation is registered.
services:
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\DoctrineUserRepository
---
### Problème 2 : "Service not found"
Service "App\User\Infrastructure\Persistence\DoctrineUserRepository" not found.
services:
App\:
resource: '../src/'
exclude:
- '../src/*/Domain/Model/' # ✅ Exclure entités
# ❌ Ne pas exclure Infrastructure!
# ❌ Ne pas exclure Infrastructure!
Circular reference detected for service "App\User\Infrastructure\Persistence\DoctrineUserRepository".
Circular reference detected for service "App\User\Infrastructure\Persistence\DoctrineUserRepository".
services:
App\User\Infrastructure\Persistence\DoctrineUserRepository:
calls:
- setLogger: ['[@logger](https://github.com/logger)'] # Injection setter au lieu constructeur
- setLogger: ['[@logger](https://github.com/logger)'] # Injection setter au lieu constructeur
# config/services_test.yaml
services:
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\InMemoryUserRepository
alias: App\User\Infrastructure\Persistence\InMemoryUserRepository
services:
# Définition tag
_instanceof:
App\Shared\Domain\Event\EventSubscriberInterface:
tags: ['app.event_subscriber'] # Nom tag
# Injection (doit correspondre!)
App\Shared\Infrastructure\Event\EventDispatcher:
arguments:
$subscribers: !tagged_iterator app.event_subscriber # Même nom tag
$subscribers: !tagged_iterator app.event_subscriber # Même nom tag
App\User\Domain\Port\UserRepositoryInterface:
alias: App\User\Infrastructure\Persistence\DoctrineUserRepository
App\User\Domain\Port\UserRepositoryInterface:
App\User\Domain\Port\UserRepositoryInterface:
class: App\User\Infrastructure\Persistence\DoctrineUserRepository
App\User\Domain\Port\UserRepositoryInterface:
**Raison :** Alias plus clairs et plus faciles à surcharger.
config/ ├── services.yaml # Défaut (production) ├── services_dev.yaml # Surcharges développement ├── services_test.yaml # Surcharges test └── services_prod.yaml # Spécifique production (optionnel)
├── services_test.yaml # Surcharges test
_instanceof:
App\Shared\Application\Query\QueryHandlerInterface:
tags: ['app.query_handler']
_instanceof:
App\User\Application\Query\FindUserQueryHandler:
tags: ['app.query_handler']
App\Order\Application\Query\FindOrderQueryHandler:
tags: ['app.query_handler']
# ... taguer manuellement chacun
tags: ['app.query_handler']
App\:
resource: '../src/'
exclude:
- '../src/*/Domain/Model/' # Entités
- '../src/*/Domain/ValueObject/' # Value objects
- '../src/*/Application/Command/' # DTOs
- '../src/*/Application/Query/' # DTOs
- '../src/Kernel.php'
- '../src/*/Application/Command/' # DTOs
- '../src/Kernel.php'
| Tâche | Configuration |
|---|---|
| Lier port à adaptateur | alias: App\...\Implementation |
| Auto-taguer par interface | _instanceof: { Interface: tags: [...] } |
| Décorer service | decorates: OriginalService + $decorated: '@.inner' |
How can I help you explore Laravel packages today?