| name | symfony:interfaces-and-autowiring |
| description | Master Symfony's Dependency Injection with autowiring, interface binding, service decoration, and tagged services for flexible architecture |
Interfaces and Autowiring in Symfony
Basic Autowiring
Symfony automatically injects dependencies based on type-hints:
<?php
// src/Service/OrderService.php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
class OrderService
{
public function __construct(
private EntityManagerInterface $em,
private LoggerInterface $logger,
) {}
public function createOrder(array $data): Order
{
$this->logger->info('Creating order', $data);
// ...
}
}
No configuration needed - Symfony wires it automatically.
Interface Binding
Define Interface
<?php
// src/Service/PaymentGatewayInterface.php
namespace App\Service;
interface PaymentGatewayInterface
{
public function charge(int $amount, string $currency): PaymentResult;
public function refund(string $transactionId, int $amount): RefundResult;
}
Implementation
<?php
// src/Service/StripePaymentGateway.php
namespace App\Service;
class StripePaymentGateway implements PaymentGatewayInterface
{
public function __construct(
private string $apiKey,
) {}
public function charge(int $amount, string $currency): PaymentResult
{
// Stripe implementation
}
public function refund(string $transactionId, int $amount): RefundResult
{
// Stripe implementation
}
}
Bind Interface to Implementation
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# Bind interface to implementation
App\Service\PaymentGatewayInterface: '@App\Service\StripePaymentGateway'
# Or with parameters
App\Service\StripePaymentGateway:
arguments:
$apiKey: '%env(STRIPE_API_KEY)%'
Use in Services
class OrderService
{
public function __construct(
private PaymentGatewayInterface $paymentGateway, // Autowired!
) {}
}
Service Decoration
Wrap a service to add behavior without modifying it:
<?php
// src/Service/LoggingPaymentGateway.php
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
#[AsDecorator(decorates: StripePaymentGateway::class)]
class LoggingPaymentGateway implements PaymentGatewayInterface
{
public function __construct(
#[AutowireDecorated]
private PaymentGatewayInterface $inner,
private LoggerInterface $logger,
) {}
public function charge(int $amount, string $currency): PaymentResult
{
$this->logger->info('Charging payment', [
'amount' => $amount,
'currency' => $currency,
]);
$result = $this->inner->charge($amount, $currency);
$this->logger->info('Payment result', [
'success' => $result->isSuccessful(),
'transactionId' => $result->getTransactionId(),
]);
return $result;
}
public function refund(string $transactionId, int $amount): RefundResult
{
$this->logger->info('Processing refund', [
'transactionId' => $transactionId,
'amount' => $amount,
]);
return $this->inner->refund($transactionId, $amount);
}
}
Tagged Services
Define Tag
<?php
// src/Export/ExporterInterface.php
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.exporter')]
interface ExporterInterface
{
public function supports(string $format): bool;
public function export(array $data): string;
}
Implementations
<?php
// src/Export/CsvExporter.php
namespace App\Export;
class CsvExporter implements ExporterInterface
{
public function supports(string $format): bool
{
return $format === 'csv';
}
public function export(array $data): string
{
// CSV export logic
}
}
// src/Export/JsonExporter.php
class JsonExporter implements ExporterInterface
{
public function supports(string $format): bool
{
return $format === 'json';
}
public function export(array $data): string
{
return json_encode($data, JSON_PRETTY_PRINT);
}
}
Inject All Tagged Services
<?php
// src/Service/ExportService.php
namespace App\Service;
use App\Export\ExporterInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
class ExportService
{
/**
* @param iterable<ExporterInterface> $exporters
*/
public function __construct(
#[AutowireIterator('app.exporter')]
private iterable $exporters,
) {}
public function export(array $data, string $format): string
{
foreach ($this->exporters as $exporter) {
if ($exporter->supports($format)) {
return $exporter->export($data);
}
}
throw new \InvalidArgumentException("Unsupported format: {$format}");
}
public function getSupportedFormats(): array
{
$formats = [];
foreach ($this->exporters as $exporter) {
// Each exporter reports what it supports
}
return $formats;
}
}
Named Autowiring
When you have multiple implementations:
# config/services.yaml
services:
App\Service\StripePaymentGateway:
arguments:
$apiKey: '%env(STRIPE_API_KEY)%'
App\Service\PaypalPaymentGateway:
arguments:
$clientId: '%env(PAYPAL_CLIENT_ID)%'
# Named bindings
App\Service\PaymentGatewayInterface $stripeGateway: '@App\Service\StripePaymentGateway'
App\Service\PaymentGatewayInterface $paypalGateway: '@App\Service\PaypalPaymentGateway'
class PaymentService
{
public function __construct(
private PaymentGatewayInterface $stripeGateway, // Stripe
private PaymentGatewayInterface $paypalGateway, // PayPal
) {}
}
Lazy Services
Load service only when actually used:
use Symfony\Component\DependencyInjection\Attribute\Lazy;
#[Lazy]
class ExpensiveService
{
public function __construct()
{
// Heavy initialization
}
}
Debug Commands
# List all services
bin/console debug:container
# Find specific service
bin/console debug:container OrderService
# Show autowiring candidates
bin/console debug:autowiring
# Show autowiring for specific type
bin/console debug:autowiring Payment
Best Practices
- Program to interfaces: Depend on interfaces, not implementations
- Constructor injection: Always use constructor injection
- Final classes: Make services
finalby default - Readonly properties: Use
private readonlyfor dependencies - Minimal interfaces: Keep interfaces focused (ISP)
- Decorate, don't modify: Use decoration for cross-cutting concerns