DI Decision Framework
When to Use What
Magento DI provides several patterns. Use this decision tree:
Do you need a dependency in your class?
├── YES → Is it a single instance for the request?
│ ├── YES → Constructor injection (shared object)
│ │ └── Example: Repository, Logger, Config
│ └── NO → Do you need to create multiple instances?
│ ├── YES → Factory injection
│ │ └── Example: ProductFactory, OrderFactory
│ └── NO → Is the dependency expensive to create?
│ ├── YES → Proxy injection
│ │ └── Example: API client, file processor
│ └── NO → Constructor injection with shared=false
│ └── Example: Data object, form
└── NO → Do you need different configs of same class?
└── YES → Virtual type
└── Example: Different loggers for different services
Pattern Selection Guide
Pattern │ Use Case │ Example
─────────────────────│───────────────────────────────────│──────────────────────
Constructor Injection│ Dependencies needed immediately │ Repository, Logger
Factory │ Creating new model instances │ ProductFactory
Proxy │ Expensive dependency, may not use │ API client, Report
Virtual Type │ Different config of same class │ Different loggers
Shared=false │ New instance each time │ Data objects, Forms
Preference │ Replace interface implementation │ Custom repository
Real Example: Choosing the Right Pattern
// Scenario: Order processing service
// Needs: OrderRepository (shared), InvoiceFactory (new each time),
// API client (expensive, may not use), Logger (shared)
class OrderProcessor
{
public function __construct(
// Constructor injection: shared, needed immediately
private OrderRepositoryInterface $orderRepository,
private LoggerInterface $logger,
// Factory: create new Invoice instances
private InvoiceFactory $invoiceFactory,
// Proxy: expensive API client, lazy-loaded
private ApiClient\Proxy $apiClient
) {}
public function process(int $orderId): void
{
$order = $this->orderRepository->get($orderId);
// Create new invoice (factory pattern)
$invoice = $this->invoiceFactory->create();
$invoice->setOrder($order);
// API client only instantiated when first used (proxy)
$this->apiClient->sendNotification($order);
$this->logger->info('Order processed', ['id' => $orderId]);
}
}
Why Not Just Use Constructor Injection?
Problem: Too many constructor dependencies (7+)
├── Symptom: Constructor has 7+ parameters
├── Impact: Hard to test, hard to understand
└── Fix: Extract to smaller services
Problem: Creating new instances in methods
├── Symptom: new Product() inside service methods
├── Impact: Tight coupling, hard to test
└── Fix: Use Factory injection
Problem: Expensive dependency not always used
├── Symptom: API client created even if method not called
├── Impact: Slow instantiation, wasted resources
└── Fix: Use Proxy for lazy loading
Problem: Same class, different configs
├── Symptom: Different services need same class with different settings
├── Impact: Code duplication or complex conditionals
└── Fix: Use Virtual Types
Common DI Anti-Patterns
Anti-Pattern 1: ObjectManager Direct Use
// BAD: Hides dependencies, impossible to test
use Magento\Framework\ObjectManager\ObjectManager;
class BadService
{
public function doSomething()
{
$repo = ObjectManager::getInstance()->get(SomeRepo::class);
// ... no way to mock in tests
}
}
// GOOD: Constructor injection
class GoodService
{
public function __construct(
private SomeRepoInterface $repo
) {}
public function doSomething()
{
// $repo is mockable in tests
}
}
Anti-Pattern 2: Circular Dependencies
// BAD: Circular dependency
class ServiceA
{
public function __construct(private ServiceB $b) {}
}
class ServiceB
{
public function __construct(private ServiceA $a) {}
}
// ObjectManager throws: Circular dependency detected
// GOOD: Extract shared logic
class SharedLogic
{
public function __construct(
private ServiceA $a,
private ServiceB $b
) {}
}
// OR: Use event/observer to break cycle
class ServiceA
{
public function __construct(private EventManager $eventManager) {}
public function doSomething()
{
$this->eventManager->dispatch('service_a_action', [...]);
}
}
Anti-Pattern 3: God Class (Too Many Dependencies)
// BAD: 10+ constructor parameters
class GodService
{
public function __construct(
private Repo1 $r1, private Repo2 $r2, private Repo3 $r3,
private Repo4 $r4, private Repo5 $r5, private Repo6 $r6,
private Repo7 $r7, private Repo8 $r8, private Repo9 $r9,
private Repo10 $r10
) {}
}
// GOOD: Extract to focused services
class OrderService
{
public function __construct(
private OrderRepositoryInterface $orderRepo,
private PaymentService $paymentService
) {}
}
class PaymentService
{
public function __construct(
private PaymentGatewayInterface $gateway,
private FraudChecker $fraudChecker
) {}
}
Anti-Pattern 4: Factory Inside Constructor
// BAD: Using factory as if it's a dependency
// (factory should be injected, not created)
class BadService
{
private $factory;
public function __construct()
{
// This won't work with DI
$this->factory = ObjectManager::getInstance()
->create(SomeFactory::class);
}
}
// GOOD: Inject the factory
class GoodService
{
public function __construct(
private SomeFactory $factory
) {}
}
Anti-Pattern 5: Static Method Calls
// BAD: Static methods bypass DI
class BadService
{
public function doSomething()
{
$result = SomeHelper::staticMethod();
// Not testable, not mockable
}
}
// GOOD: Inject helper as dependency
class GoodService
{
public function __construct(
private SomeHelper $helper
) {}
public function doSomething()
{
$result = $this->helper->method();
}
}
DI Troubleshooting
Common Error Messages
Error: Cannot create an instance of X
├── Cause: Missing preference for interface
├── Fix: Add <preference> in di.xml
└── Check: Is the interface mapped to implementation?
Error: Circular dependency detected
├── Cause: Class A depends on B, B depends on A
├── Fix: Extract shared logic to third class
└── Check: Review dependency graph
Error: Argument X must be of type Y
├── Cause: Wrong type in di.xml argument
├── Fix: Correct xsi:type in di.xml
└── Check: Is the argument type matching constructor?
Error: ServiceNotFoundException
├── Cause: Interface not registered in di.xml
├── Fix: Add preference or type configuration
└── Check: Is the module with preference enabled?
Debugging DI Resolution
// Method 1: Check what ObjectManager resolves
$objectManager = \Magento\Framework\ObjectManager\ObjectManager::getInstance();
// Check if a class can be created
try {
$instance = $objectManager->get(SomeClass::class);
echo 'Resolved: ' . get_class($instance);
} catch (\Exception $e) {
echo 'Failed: ' . $e->getMessage();
}
// Method 2: Check di.xml compilation
// Run: bin/magento setup:di:compile
// Errors will show which classes fail to compile
// Method 3: Check generated code
// Look in var/di/ for compiled dependency information
ls var/di/ | grep -i error
Performance Impact of DI
DI Compilation:
├── First request: Compiles DI configuration
├── Subsequent requests: Uses compiled config
├── Compilation time: 5-30 seconds
└── Stored in: var/di/ and generated/code/
Memory Impact:
├── Shared objects: Stay in memory for request
├── Non-shared: Created and garbage collected
├── Proxies: Minimal (deferred instantiation)
└── Factories: Minimal (create on demand)
Optimization Tips:
├── Use Proxies for expensive dependencies
├── Keep shared objects stateless
├── Minimize constructor parameters
├── Run setup:di:compile after di.xml changes
└── Monitor memory with debug toolbar
Testing with DI
Unit Testing Constructor Injection
namespace Vendor\Module\Test\Unit\Service;
use PHPUnit\Framework\TestCase;
use Vendor\Module\Service\OrderProcessor;
class OrderProcessorTest extends TestCase
{
private $processor;
private $orderRepoMock;
private $loggerMock;
protected function setUp(): void
{
// Mock all constructor dependencies
$this->orderRepoMock = $this->createMock(
\Magento\Sales\Api\OrderRepositoryInterface::class
);
$this->loggerMock = $this->createMock(
\Psr\Log\LoggerInterface::class
);
// Create instance with mocked dependencies
$this->processor = new OrderProcessor(
$this->orderRepoMock,
$this->loggerMock
);
}
public function testProcessOrder(): void
{
$orderMock = $this->createMock(
\Magento\Sales\Api\OrderInterface::class
);
$this->orderRepoMock->expects($this->once())
->method('get')
->with(1)
->willReturn($orderMock);
$this->processor->process(1);
// Assert behavior, not implementation
}
}
Testing with Factories
// Class under test
class ProductCreator
{
public function __construct(
private ProductFactory $productFactory
) {}
public function create(array $data): Product
{
$product = $this->productFactory->create();
$product->setData($data);
return $product;
}
}
// Test
class ProductCreatorTest extends TestCase
{
public function testCreate(): void
{
$factoryMock = $this->createMock(ProductFactory::class);
$productMock = $this->createMock(Product::class);
$factoryMock->expects($this->once())
->method('create')
->willReturn($productMock);
$productMock->expects($this->once())
->method('setData')
->with(['name' => 'Test']);
$creator = new ProductCreator($factoryMock);
$result = $creator->create(['name' => 'Test']);
$this->assertSame($productMock, $result);
}
}
Integration Testing with Real DI
namespace Vendor\Module\Test\Integration\Service;
use PHPUnit\Framework\TestCase;
use Magento\TestFramework\Helper\Bootstrap;
class OrderProcessorTest extends TestCase
{
public function testProcessOrderIntegration(): void
{
// Get real ObjectManager
$objectManager = Bootstrap::getObjectManager();
// Get real service with real dependencies
$processor = $objectManager->create(
\Vendor\Module\Service\OrderProcessor::class
);
// Test with real database, real services
$processor->process(1);
// Assert against database
$order = $objectManager->create(
\Magento\Sales\Api\OrderRepositoryInterface::class
)->get(1);
$this->assertEquals('processing', $order->getState());
}
}
Quiz
1. When should you use a Factory instead of Constructor Injection?
2. What is the symptom of a circular dependency?
3. Why use a Proxy instead of direct constructor injection for an API client?
4. What is the first thing to check when you see 'Cannot create an instance of X'?
Flashcards
Question
Constructor injection use case?
Click to reveal answer
Answer
Dependencies needed immediately and shared per request (repos, loggers)
Question
Factory use case?
Click to reveal answer
Answer
Creating new instances (models, data objects)
Question
Proxy use case?
Click to reveal answer
Answer
Expensive dependencies that may not be used (API clients, reports)
Question
Virtual type use case?
Click to reveal answer
Answer
Different configurations of the same class for different services
Question
Circular dependency fix?
Click to reveal answer
Answer
Extract shared logic to a third class
Question
First check for 'Cannot create instance' error?
Click to reveal answer
Answer
Verify preference exists in di.xml for the interface
Revision Notes
Key Takeaways
- 1. Constructor injection for shared dependencies, Factory for new instances, Proxy for lazy loading
- 2. Circular dependencies: Extract shared logic to break the cycle
- 3. God class (7+ deps): Extract to smaller, focused services
- 4. ObjectManager direct use: Always wrong in application code
- 5. Debugging: Check di.xml preferences, run setup:di:compile, check generated code
Interview Tips
- • Explain when to use Factory vs Constructor Injection vs Proxy
- • Describe how to detect and fix circular dependencies
- • Walk through debugging a 'Cannot create instance' error
- • Discuss testing patterns with mocked DI dependencies
- • Analyze performance impact of DI choices
Cheat Sheet
DI Decision Framework
- Shared dependency → Constructor Injection
- New instances → Factory Injection
- Expensive, may not use → Proxy Injection
- Same class, different config → Virtual Type
- Circular dependency → Extract shared logic
- 7+ constructor params → Extract to smaller services
Debug: Check preferences, run setup:di:compile, check var/di/