Marketplace

symfony:interfaces-and-autowiring

Master Symfony's Dependency Injection with autowiring, interface binding, service decoration, and tagged services for flexible architecture

$ インストール

git clone https://github.com/MakFly/superpowers-symfony /tmp/superpowers-symfony && cp -r /tmp/superpowers-symfony/skills/interfaces-and-autowiring ~/.claude/skills/superpowers-symfony

// tip: Run this command in your terminal to install the skill


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

  1. Program to interfaces: Depend on interfaces, not implementations
  2. Constructor injection: Always use constructor injection
  3. Final classes: Make services final by default
  4. Readonly properties: Use private readonly for dependencies
  5. Minimal interfaces: Keep interfaces focused (ISP)
  6. Decorate, don't modify: Use decoration for cross-cutting concerns