Best practices for Dependency Injection

Edit on GitHub

This document describes best practices and recommendations when working with Symfony’s Dependency Injection component in Spryker.

When to use DI vs Factory pattern

Use Dependency Injection when

  • Building new features - Start with DI for all new code
  • Services are reused - Multiple consumers need the same service instance
  • Dependencies are stable - Interfaces and contracts are well-defined
  • Testing is priority - You want easy mocking and testability
  • Cross-module dependencies - Services need to be shared across modules

Use Factory pattern when

  • Legacy code migration - Existing code not yet migrated to DI
  • Complex creation logic - Object instantiation requires conditional logic
  • Module-specific instances - Need to create new instances per call
  • Gradual migration - Transitioning from factory to DI incrementally

For facades and larger modules, use a hybrid approach during migration:

<?php

namespace Pyz\Zed\Customer\Business;

use Spryker\Zed\Kernel\Business\AbstractFacade;

class CustomerFacade extends AbstractFacade implements CustomerFacadeInterface
{
    // New DI pattern for new features
    public function validateCustomerEmail(string $email): bool
    {
        return $this->getService(EmailValidatorInterface::class)
            ->validate($email);
    }

    // Factory pattern for legacy code
    public function processLegacyCustomer(CustomerTransfer $customer): void
    {
        $this->getFactory()
            ->createLegacyProcessor()
            ->process($customer);
    }
}

Interface-based design

Always program against interfaces, not concrete implementations.

✅ Good: Interface-based

<?php

namespace Pyz\Zed\Customer\Business;

// Define interface
interface CustomerProcessorInterface
{
    public function process(CustomerTransfer $customer): void;
}

// Implementation
class CustomerProcessor implements CustomerProcessorInterface
{
    public function process(CustomerTransfer $customer): void
    {
        // Implementation
    }
}

// Consumer depends on interface
class CustomerService
{
    public function __construct(
        private CustomerProcessorInterface $processor
    ) {
    }
}

❌ Bad: Concrete class dependency

<?php

namespace Pyz\Zed\Customer\Business\Customer;

// Consumer depends on concrete class
class Customer
{
    public function __construct(
        private CustomerProcessor $processor // Tightly coupled!
    ) {
    }
}

Benefits of interfaces

  1. Flexibility - Easy to swap implementations
  2. Testability - Simple to create mocks
  3. Decoupling - Reduces dependencies between modules
  4. Contract clarity - Clear expectations for implementations

Avoiding circular dependencies

Circular dependencies occur when Service A depends on Service B, which depends on Service A.

❌ Bad: Circular dependency

<?php

namespace Pyz\Zed\Customer\Business\Customer;

class Customer implements CustomerInterface
{
    public function __construct(
        private OrderInterface $order
    ) {
    }
}

class Order implements OrderInterface
{
    public function __construct(
        private CustomerInterface $customer // Circular!
    ) {
    }
}

✅ Good: Extract shared dependency

<?php

namespace Pyz\Zed\Customer\Business\Customer;

// Create a shared service that both can depend on
class CustomerDataProvider implements CustomerDataProviderInterface
{
    public function getCustomerData(int $customerId): CustomerTransfer
    {
        // Implementation
    }
}

class Customer
{
    public function __construct(
        private CustomerDataProviderInterface $customerDataProvider
    ) {
    }
}

class Order
{
    public function __construct(
        private CustomerDataProviderInterface $customerDataProvider
    ) {
    }
}

Avoiding scalar types in constructors

Scalar constructor arguments (int, string, bool, float) require manual configuration and reduce autowiring benefits.

❌ Bad: Scalars in constructor

<?php

namespace Pyz\Zed\Payment\Business\Payment;

class Payment implements PaymentInterface
{
    public function __construct(
        private int $timeout,
        private bool $sandboxMode
    ) {
    }
}

This requires manual configuration:

$services->set(Payment::class)
    ->arg('$timeout', 30)
    ->arg('$sandboxMode', true);

✅ Good: Configuration object

<?php

namespace Pyz\Zed\Payment\Business;

// Value object for configuration
class PaymentConfig
{   
    public function getTimeout(): int {
        return 123;
    }
    
    public function getSandboxMode(): bool {
        return true;
    }
}

class Payment implements PaymentInterface
{
    public function __construct(
        private PaymentConfig $config
    ) {
    }
}

Benefits

  1. Single configuration point - Configure once, use everywhere
  2. Type safety - Strong typing for all configuration
  3. Easier testing - Create mock configs easily
  4. Better organization - Clear separation of concerns

Keep services focused (Single Responsibility)

Each service should have one clear purpose:

<?php

// ✅ Good: Focused services
class CustomerEmailValidator implements EmailValidatorInterface
{
    public function validate(string $email): bool
    {
        // Only validates emails
    }
}

class CustomerAddressValidator
{
    public function validate(AddressTransfer $addressTransfer): bool
    {
        // Only validates addresses
    }
}

// ❌ Bad: Too many responsibilities
class CustomerValidator
{
    public function validateEmail(string $email): bool { }
    public function validateAddress(AddressTransfer $address): bool { }
    public function validatePhone(string $phone): bool { }
    public function validateCreditCard(string $card): bool { }
    // Too many unrelated validations!
}

Performance considerations

Use service proxies for expensive services

For services that are expensive to instantiate but not always needed:

<?php

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $configurator): void {
    $services = $configurator->services()
        ->defaults()
        ->autowire()
        ->public()
        ->autoconfigure();

    // Mark service as lazy to create proxy
    $services->set(ExpensiveService::class)
        ->lazy();
};

Avoid service explosion

Don’t create a service for every single class. Use factories for lightweight objects:

<?php

// ✅ Good: Use factory for simple value objects
class CustomerFactory
{
    public function createCustomerData(array $data): CustomerData
    {
        return new CustomerData(
            $data['name'],
            $data['email']
        );
    }
}

// ❌ Bad: Making every value object a service
$services->set(CustomerData::class); // Unnecessary!

Consider container compilation time

Each service registered increases compilation time. For development:

  • Exclude heavy modules (DataImport, Storage, Search)
  • Use container cache
  • Compile container in deployment pipeline
<?php

$excludedModuleConfiguration = [
    'DataImport' => true,
    'ProductPageSearch' => true,
    'ProductStorage' => true,
    'PriceProductStorage' => true,
];

Testing best practices

Use constructor injection for better testability

<?php

namespace Pyz\Zed\Customer\Business\Customer;

class Customer implements CustomerInterface
{
    public function __construct(
        private CustomerRepositoryInterface $repository,
        private EmailValidatorInterface $validator
    ) {
    }

    public function registerCustomer(CustomerTransfer $customerTransfer): bool
    {
        if (!$this->validator->validate($customerTransfer->getEmail())) {
            return false;
        }

        $this->repository->save($customerTransfer);

        return true;
    }
}

Common anti-patterns to avoid

❌ Service locator pattern

<?php

// Don't inject the entire container
class Customer implements CustomerInterface
{
    public function __construct(
        private ContainerInterface $container // Anti-pattern!
    ) {
    }

    public function process(): void
    {
        $validator = $this->container->get(ValidatorInterface::class);
        // This hides dependencies
    }
}

❌ Static dependencies

<?php

// Don't use static dependencies
class Customer implements CustomerInterface
{
    public function process(): void
    {
        // Anti-pattern! Not testable
        $result = SomeStaticHelper::doSomething();
    }
}

❌ New keyword in services

<?php

// Don't instantiate dependencies directly
class Customer implements CustomerInterface
{
    public function process(): void
    {
        // Anti-pattern! Should be injected
        $validator = new EmailValidator();
    }
}

Next steps