Best practices for Dependency Injection
Edit on GitHubThis 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
Hybrid approach (recommended)
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
- Flexibility - Easy to swap implementations
- Testability - Simple to create mocks
- Decoupling - Reduces dependencies between modules
- 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
- Single configuration point - Configure once, use everywhere
- Type safety - Strong typing for all configuration
- Easier testing - Create mock configs easily
- 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
Thank you!
For submitting the form