What is Dependency Injection?
The Problem Without DI
Without DI, classes create their own dependencies, leading to tight coupling:
namespace Vendor\Catalog\Model;
class ProductExporter
{
private \Vendor\Catalog\Model\CsvFormatter $formatter;
private \Vendor\Catalog\Model\FileWriter $writer;
public function __construct()
{
// Hard-coded dependencies — cannot swap implementations
$this->formatter = new CsvFormatter();
$this->writer = new FileWriter();
}
public function export(array $products): void
{
$csv = $this->formatter->format($products);
$this->writer->write('products.csv', $csv);
}
}
Problems:
- Cannot test without real files
- Cannot swap CSV for JSON without modifying this class
- Cannot mock dependencies in unit tests
The Solution: Dependency Injection
namespace Vendor\Catalog\Model;
interface FormatterInterface
{
public function format(array $data): string;
}
interface WriterInterface
{
public function write(string $filename, string $content): void;
}
class ProductExporter
{
public function __construct(
private FormatterInterface $formatter,
private WriterInterface $writer
) {}
public function export(array $products): void
{
$csv = $this->formatter->format($products);
$this->writer->write('products.csv', $csv);
}
}
The class no longer decides which formatter or writer to use — that decision is made externally (by the DI container).
Types of Injection
1. Constructor Injection (Preferred)
Dependencies are provided when the object is created:
namespace Vendor\Catalog\Model;
class CategoryProcessor
{
public function __construct(
private \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepo,
private \Magento\Catalog\Api\ProductRepositoryInterface $productRepo,
private LoggerInterface $logger
) {}
public function process(int $categoryId): void
{
$category = $this->categoryRepo->get($categoryId);
$this->logger->info('Processing category: ' . $category->getName());
}
}
Advantages: Dependencies are explicit, object is always in valid state, immutable.
2. Setter Injection
Dependencies are provided after construction:
class ReportGenerator
{
private ?FormatterInterface $formatter = null;
// Required dependencies via constructor
public function __construct(
private DataCollector $collector
) {}
// Optional dependencies via setter
public function setFormatter(FormatterInterface $formatter): void
{
$this->formatter = $formatter;
}
public function generate(): string
{
$data = $this->collector->collect();
if ($this->formatter !== null) {
return $this->formatter->format($data);
}
return print_r($data, true);
}
}
Use when: Dependency is optional or configuration depends on runtime values.
3. Interface Injection (Magento Plugins)
Magento implements interface injection through its plugin system:
<!-- di.xml -->
<config>
<type name="Magento\Catalog\Model\Product">
<plugin name="vendor_product_plugin" type="Vendor\Catalog\Plugin\ProductPlugin"/>
</type>
</config>
namespace Vendor\Catalog\Plugin;
class ProductPlugin
{
public function beforeSave(\Magento\Catalog\Model\Product $subject): void
{
// Behavior injected via plugin interface
}
}
The plugin implements an implicit interface defined by the method name convention (before*, after*, around*).
Magento's DI Configuration
di.xml: The Heart of Magento DI
Magento uses di.xml files to configure dependencies. These are merged across modules.
Automatic Resolution
// If constructor type-hints an interface, Magento looks for
// a configured implementation or uses the concrete class
namespace Vendor\Import\Model;
class ProductImporter
{
public function __construct(
private \Magento\Catalog\Api\ProductRepositoryInterface $productRepo
) {}
}
// Magento automatically resolves ProductRepositoryInterface
// to Magento\Catalog\Model\ProductRepository
Explicit Configuration
<!-- app/code/Vendor/Module/etc/di.xml -->
<config>
<!-- Constructor argument override -->
<type name="Vendor\Import\Model\ProductImporter">
<arguments>
<argument name="batchSize" xsi:type="number">500</argument>
<argument name="formatter" xsi:type="object">Vendor\Import\Model\JsonFormatter</argument>
</arguments>
</type>
<!-- Virtual type (alias for implementation) -->
<virtualType name="JsonWriter" type="Vendor\Filesystem\Model\Writer">
<arguments>
<argument name="format" xsi:type="string">json</argument>
</arguments>
</virtualType>
<!-- Shared vs non-shared instances -->
<type name="Vendor\Logger\Model\FileLogger" shared="false"/>
</config>
Shared vs Non-Shared
// shared="true" (default): one instance per request
// shared="false": new instance every time it's injected
// Example: Logger should be shared (avoid multiple file handles)
// Example: Request-scoped objects should be non-shared
Benefits and Anti-Patterns
Benefits of DI
1. Testability
// Easy to test with mocked dependencies
public function testExport(): void
{
$formatter = $this->createMock(FormatterInterface::class);
$writer = $this->createMock(WriterInterface::class);
$exporter = new ProductExporter($formatter, $writer);
$exporter->export([/* test data */]);
$writer->expects($this->once())
->method('write')
->with('products.csv', $this->anything());
}
2. Flexibility
Swap implementations without changing consuming code.
3. Explicit Dependencies
Constructor signatures document exactly what a class needs.
Anti-Patterns to Avoid
// ANTI-PATTERN 1: Service Locator (hides dependencies)
class BadExample
{
public function doSomething()
{
$repo = ObjectManager::getInstance()->get(ProductRepository::class);
// Hidden dependency — not visible in constructor
}
}
// ANTI-PATTERN 2: God Object (too many dependencies)
class OrderProcessor
{
public function __construct(
private $a, private $b, private $c, private $d,
private $e, private $f, private $g, private $h
) {} // 8+ dependencies = likely SRP violation
}
// ANTI-PATTERN 3: Circular dependency
class A { public function __construct(private B $b) {} }
class B { public function __construct(private A $a) {} }
// Magento will throw an error during compilation
Rule of thumb: If a class has more than 5 constructor dependencies, it probably does too much.
Quiz
1. Which injection type is generally preferred in Magento?
2. What does `shared="false"` in di.xml do?
3. What is the Service Locator anti-pattern?
Flashcards
Question
What is dependency injection?
Click to reveal answer
Answer
Providing dependencies externally rather than creating them internally
Question
What are the three main injection types?
Click to reveal answer
Answer
Constructor injection (preferred), setter injection (optional deps), interface injection (plugins)
Question
How does Magento configure DI?
Click to reveal answer
Answer
Via di.xml files that are merged across modules
Question
What does shared="false" do?
Click to reveal answer
Answer
Creates a new instance for each injection point instead of reusing one
Question
What is a circular dependency?
Click to reveal answer
Answer
When class A depends on B and B depends on A — Magento detects and rejects this
Revision Notes
Key Takeaways
- 1. DI externalizes dependency creation, enabling testability and flexibility
- 2. Constructor injection is preferred — makes dependencies explicit
- 3. Magento uses di.xml to configure and override dependencies
- 4. Virtual types create aliases for different configurations
- 5. Shared instances = one per request; non-shared = new instance each time
- 6. Avoid service locator, god objects, and circular dependencies
Interview Tips
- • Explain why constructor injection beats setter injection (immutable, explicit)
- • Demonstrate knowledge of di.xml configuration syntax
- • Discuss how DI enables unit testing (mocking constructor args)
Cheat Sheet
DI Types:
Constructor → Required deps (preferred)
Setter → Optional deps
Interface → Plugins (Magento-specific)
di.xml:
<type name="Class">
<arguments>
<argument name="dep" xsi:type="object">Interface</argument>
<argument name="config" xsi:type="number">42</argument>
</arguments>
</type>
<virtualType name="Alias" type="Concrete\Class"/>
shared="false" → New instance per injection