Skip to content
intermediate Phase 25 · Module Backend

Setting Up Dependency Injection

Guide to Magento 2 Dependency Injection: constructor injection, di.xml configuration, interface bindings, and DI best practices

45m
0 problems
Topic Progress 0%

Constructor Injection Fundamentals

How Constructor Injection Works

Magento's ObjectManager reads the constructor parameters and automatically injects dependencies. No manual instantiation needed.

namespace Amazon\Prep\Service;

class WarrantyService
{
    public function __construct(
        private \Amazon\Prep\Model\WarrantyFactory $warrantyFactory,
        private \Amazon\Prep\Model\ResourceModel\Warranty $warrantyResource,
        private \Magento\Framework\Registry $registry,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function createWarranty(string $name, int $duration): \Amazon\Prep\Model\Warranty
    {
        $warranty = $this->warrantyFactory->create();
        $warranty->setName($name);
        $warranty->setDuration($duration);
        $this->warrantyResource->save($warranty);
        $this->logger->info('Warranty created: ' . $warranty->getId());
        return $warranty;
    }
}

PHP 8 Constructor Promotion

// Modern PHP 8+ syntax
class WarrantyService
{
    public function __construct(
        private WarrantyFactory $warrantyFactory,
        private WarrantyResource $warrantyResource,
        private LoggerInterface $logger
    ) {}
}

// Equivalent to older syntax:
class WarrantyService
{
    private $warrantyFactory;
    private $warrantyResource;
    private $logger;

    public function __construct(
        WarrantyFactory $warrantyFactory,
        WarrantyResource $warrantyResource,
        LoggerInterface $logger
    ) {
        $this->warrantyFactory = $warrantyFactory;
        $this->warrantyResource = $warrantyResource;
        $this->logger = $logger;
    }
}

Required vs Optional Dependencies

public function __construct(
    // Required - no default value
    private WarrantyFactory $warrantyFactory,
    private WarrantyResource $warrantyResource,

    // Optional - has default value
    private ?string $customParam = null,
    private int $limit = 10
) {}

Magento skips optional parameters if not configured in di.xml.

di.xml Configuration

File Locations

etc/di.xml                 (global - all areas)
etc/frontend/di.xml       (frontend only)
etc/adminhtml/di.xml      (admin only)
etc/crontab/di.xml        (cron only)
etc/webapi/rest/di.xml    (REST API only)
etc/webapi/soap/di.xml    (SOAP API only)

Interface Binding (Preference)

Bind an interface to its implementation:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Amazon\Prep\Api\WarrantyRepositoryInterface"
                type="Amazon\Prep\Model\WarrantyRepository"/>
</config>

Now anywhere WarrantyRepositoryInterface is type-hinted, DI injects WarrantyRepository.

Constructor Arguments

Override constructor parameters:

<type name="Amazon\Prep\Service\WarrantyService">
    <arguments>
        <argument name="limit" xsi:type="number">50</argument>
        <argument name="customParam" xsi:type="string">custom-value</argument>
    </arguments>
</type>

Type Configuration

<type name="Amazon\Prep\Model\Warranty">
    <arguments>
        <argument name="statusOptions" xsi:type="array">
            <item name="active" xsi:type="string">Active</item>
            <item name="inactive" xsi:type="string">Inactive</item>
        </argument>
    </arguments>
</type>

Plugin Configuration

<type name="Magento\Catalog\Model\Product">
    <plugin name="amazon_prep_product" type="Amazon\Prep\Plugin\ProductPlugin"/>
</type>

Virtual Types

Create anonymous class configurations:

<virtualType name="ActiveWarrantyCollection" type="Amazon\Prep\Model\ResourceModel\Warranty\Collection">
    <arguments>
        <argument name="mainTable" xsi:type="string">vendor_warranty</argument>
    </arguments>
</virtualType>

Use virtual types in other configurations:

<type name="Amazon\Prep\Service\WarrantyService">
    <arguments>
        <argument name="collection" xsi:type="object">ActiveWarrantyCollection</argument>
    </arguments>
</type>

Shared vs Non-Shared Instances

Shared Instances (Singletons)

By default, ObjectManager creates shared instances. The same object is returned every time:

$service1 = $objectManager->get(WarrantyService::class);
$service2 = $objectManager->get(WarrantyService::class);
// $service1 === $service2 (same instance)

Non-Shared Instances

Force new instances each time:

<type name="Amazon\Prep\Model\Warranty" shared="false"/>
$warranty1 = $objectManager->get(Warranty::class);
$warranty2 = $objectManager->get(Warranty::class);
// $warranty1 !== $warranty2 (different instances)

When to Use Each

Pattern Use Case
Shared (default) Services, factories, registries
Non-shared Models, data objects, collections

Non-Shared in di.xml

<type name="Amazon\Prep\Model\Warranty" shared="false"/>
<type name="Amazon\Prep\Model\ResourceModel\Warranty\Collection" shared="false"/>

Factory Pattern

Factories always create new instances:

// Factory always creates new model
$warranty1 = $this->warrantyFactory->create();
$warranty2 = $this->warrantyFactory->create();
// $warranty1 !== $warranty2 (new instances)

This is why factories are shared (the factory itself is a singleton) but the objects they create are new.

DI Best Practices and Troubleshooting

Best Practices

1. Depend on Interfaces

// Good - depends on interface
public function __construct(
    private WarrantyRepositoryInterface $repository
) {}

// Bad - depends on concrete class
public function __construct(
    private WarrantyRepository $repository
) {}

2. Avoid Circular Dependencies

// BAD: Circular dependency
class ServiceA {
    public function __construct(private ServiceB $b) {}
}
class ServiceB {
    public function __construct(private ServiceA $a) {}
}

// SOLUTION: Extract shared logic to ServiceC
class ServiceC {
    public function __construct(
        private ServiceA $a,
        private ServiceB $b
    ) {}
}

3. Use Plugins Instead of Preferences When Possible

<!-- Prefer plugins for extending behavior -->
<type name="Magento\Catalog\Model\Product">
    <plugin name="my_plugin" type="Amazon\Prep\Plugin\ProductPlugin"/>
</type>

<!-- Use preferences only when replacing entirely -->
<preference for="MyInterface" type="MyImplementation"/>

4. Keep Constructors Clean

// Bad - too many dependencies
class BadService {
    public function __construct(
        private Factory1 $f1,
        private Factory2 $f2,
        private Factory3 $f3,
        private Factory4 $f4,
        private Factory5 $f5,
        private Factory6 $f6,
        private Factory7 $f7
    ) {}
}

// Good - extract to smaller services
class GoodService {
    public function __construct(
        private SubServiceA $subA,
        private SubServiceB $subB
    ) {}
}

Common DI Issues

Circular Dependency Error

Circular dependency detected: A -> B -> A

Solution: Extract shared logic to a third class.

Unknown Class Error

Invalid type Amazon\Prep\Model\NonExistent

Solution: Check class name spelling and namespace.

Preference Conflict

Preference already defined for interface

Solution: Use plugins instead of conflicting preferences.

Debug DI

# Compile and check for errors
bin/magento setup:di:compile

# Generate dependency report
bin/magento dev:di:log

# Check generated classes
cat var/di/development.log

Quiz

1. What does di.xml <preference> do?

Question 1 options

2. What is the difference between shared and non-shared objects?

Question 2 options

3. How do you make a class non-shared?

Question 3 options

Flashcards

Question

What does constructor injection do?

Answer

ObjectManager automatically resolves and injects dependencies based on constructor type hints

Question

What is a di.xml preference?

Answer

Binds an interface to a concrete implementation for DI resolution

Question

When should you use non-shared objects?

Answer

For models, data objects, and collections that should be fresh instances

Question

What causes circular dependency errors?

Answer

When Class A depends on Class B which depends on Class A

Question

Where do global di.xml configurations go?

Answer

etc/di.xml

Revision Notes

Key Takeaways

  • 1. Constructor injection is Magento's primary DI mechanism
  • 2. di.xml preferences bind interfaces to implementations
  • 3. Shared objects are singletons; non-shared create new instances
  • 4. Depend on interfaces, not concrete classes
  • 5. Use plugins to extend behavior; preferences to replace entirely

Interview Tips

  • Explain how ObjectManager resolves constructor dependencies
  • Know when to use preferences vs plugins
  • Be ready to troubleshoot circular dependency issues
  • Discuss shared vs non-shared object trade-offs

Cheat Sheet

Constructor injection:
  public function __construct(private Interface $dep) {}

di.xml:
  <preference for="Interface" type="Implementation"/>
  <type name="Class"><arguments>...</arguments></type>
  <type name="Class" shared="false"/>

Debug: bin/magento setup:di:compile