Skip to content
intermediate Phase 9 · DI & Patterns

Dependency Injection

What dependency injection is, constructor/setter/interface injection, benefits, and why Magento uses DI heavily

1h
0 problems
Topic Progress 0%

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?

Question 1 options

2. What does `shared="false"` in di.xml do?

Question 2 options

3. What is the Service Locator anti-pattern?

Question 3 options

Flashcards

Question

What is dependency injection?

Answer

Providing dependencies externally rather than creating them internally

Question

What are the three main injection types?

Answer

Constructor injection (preferred), setter injection (optional deps), interface injection (plugins)

Question

How does Magento configure DI?

Answer

Via di.xml files that are merged across modules

Question

What does shared="false" do?

Answer

Creates a new instance for each injection point instead of reusing one

Question

What is a circular dependency?

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