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?
2. What is the difference between shared and non-shared objects?
3. How do you make a class non-shared?
Flashcards
Question
What does constructor injection do?
Click to reveal answer
Answer
ObjectManager automatically resolves and injects dependencies based on constructor type hints
Question
What is a di.xml preference?
Click to reveal answer
Answer
Binds an interface to a concrete implementation for DI resolution
Question
When should you use non-shared objects?
Click to reveal answer
Answer
For models, data objects, and collections that should be fresh instances
Question
What causes circular dependency errors?
Click to reveal answer
Answer
When Class A depends on Class B which depends on Class A
Question
Where do global di.xml configurations go?
Click to reveal answer
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