Master Symfony's Dependency Injection with autowiring, interface binding, service decoration, and tagged services for flexible architecture
This skill inherits all available tools. When active, it can use any tool Claude has access to.
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.
<?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;
}
<?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
}
}
# 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)%'
class OrderService
{
public function __construct(
private PaymentGatewayInterface $paymentGateway, // Autowired!
) {}
}
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);
}
}
<?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;
}
<?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);
}
}
<?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;
}
}
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
) {}
}
Load service only when actually used:
use Symfony\Component\DependencyInjection\Attribute\Lazy;
#[Lazy]
class ExpensiveService
{
public function __construct()
{
// Heavy initialization
}
}
# 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
final by defaultprivate readonly for dependencies