Skip to content
intermediate Phase 18 · Events, Plugins & DI

Magento 2 Dependency Injection

DI decision framework: when to use constructor injection, factories, proxies, and virtual types. Includes common patterns, anti-patterns, and troubleshooting.

1h
0 problems
Topic Progress 0%

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?

Question 1 options

2. What is the symptom of a circular dependency?

Question 2 options

3. Why use a Proxy instead of direct constructor injection for an API client?

Question 3 options

4. What is the first thing to check when you see 'Cannot create an instance of X'?

Question 4 options

Flashcards

Question

Constructor injection use case?

Answer

Dependencies needed immediately and shared per request (repos, loggers)

Question

Factory use case?

Answer

Creating new instances (models, data objects)

Question

Proxy use case?

Answer

Expensive dependencies that may not be used (API clients, reports)

Question

Virtual type use case?

Answer

Different configurations of the same class for different services

Question

Circular dependency fix?

Answer

Extract shared logic to a third class

Question

First check for 'Cannot create instance' error?

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/