How ObjectManager Resolves Dependencies
Resolution Process
When ObjectManager creates an object, it:
- Reads the class constructor parameters
- Checks di.xml for type configurations
- Resolves each parameter type:
- Interface → check for preference
- Concrete class → create instance
- Scalar type → check di.xml arguments
- Null/optional → skip if not configured
- Creates the object with resolved dependencies
namespace Amazon\Prep\Service;
class WarrantyService
{
public function __construct(
private WarrantyFactory $warrantyFactory, // Resolved via DI
private WarrantyResource $warrantyResource, // Resolved via DI
private LoggerInterface $logger, // Resolved via preference
private ?string $apiKey = null // Optional, from di.xml
) {}
}
Resolution Priority
1. di.xml type arguments
2. di.xml preferences (for interfaces)
3. Auto-wiring (concrete classes)
4. Default values (optional parameters)
Scalar Type Resolution
Scalar types (string, int, float, bool) cannot be auto-wired. They must come from di.xml:
<type name="Amazon\Prep\Service\WarrantyService">
<arguments>
<argument name="apiKey" xsi:type="string">sk_live_abc123</argument>
<argument name="timeout" xsi:type="number">30</argument>
<argument name="debug" xsi:type="boolean">false</argument>
</arguments>
</type>
Null Safety
// Nullable type - can be null
public function __construct(
private ?LoggerInterface $logger = null
) {}
// Required type - cannot be null
public function __construct(
private WarrantyFactory $factory // Must be provided
) {}
PHP 8 Constructor Promotion
Traditional vs PHP 8+
// PHP 7 style
class WarrantyService
{
private $factory;
private $resource;
private $logger;
public function __construct(
WarrantyFactory $factory,
WarrantyResource $resource,
LoggerInterface $logger
) {
$this->factory = $factory;
$this->resource = $resource;
$this->logger = $logger;
}
}
// PHP 8+ constructor promotion
class WarrantyService
{
public function __construct(
private WarrantyFactory $factory,
private WarrantyResource $resource,
private LoggerInterface $logger
) {}
}
Mixed Visibility
public function __construct(
private readonly WarrantyFactory $factory, // Read-only property
protected WarrantyResource $resource, // Protected property
public LoggerInterface $logger // Public property
) {}
Readonly Properties (PHP 8.1+)
public function __construct(
private readonly WarrantyFactory $factory,
private readonly WarrantyResource $resource
) {}
// Can only be set in constructor
// $this->factory = $newFactory; // Error!
Typed Properties
public function __construct(
private int $limit = 10,
private string $prefix = 'warranty_',
private bool $debug = false,
private float $taxRate = 0.08
) {}
Union Types (PHP 8.0+)
public function __construct(
private Warranty|false $warranty // Can be Warranty or false
) {}
Named Arguments (PHP 8.0+)
// Create with named arguments
$service = new WarrantyService(
logger: $logger,
factory: $factory,
resource: $resource
);
// In DI, arguments are resolved by name from di.xml
Required vs Optional Parameters
Required Parameters
// These MUST be provided - no default value
public function __construct(
private WarrantyFactory $factory, // Required
private WarrantyResource $resource, // Required
private SearchCriteriaBuilder $searchBuilder // Required
) {}
If DI cannot resolve a required parameter, it throws:
Cannot create an instance of Amazon\Prep\Service\WarrantyService
Optional Parameters
// These have defaults - can be omitted
public function __construct(
private WarrantyFactory $factory, // Required
private WarrantyResource $resource, // Required
private ?string $apiKey = null, // Optional (null)
private int $pageSize = 20, // Optional (20)
private bool $enableCache = true // Optional (true)
) {}
Overriding Optional Parameters
<type name="Amazon\Prep\Service\WarrantyService">
<arguments>
<argument name="apiKey" xsi:type="string">sk_live_custom</argument>
<argument name="pageSize" xsi:type="number">50</argument>
<argument name="enableCache" xsi:type="boolean">false</argument>
</arguments>
</type>
Null vs Optional
// Nullable - can be explicitly null
public function __construct(
private ?LoggerInterface $logger = null
) {}
// Usage
$service = new WarrantyService(logger: null); // Explicitly null
$service = new WarrantyService(); // Default null
// Required nullable - must be provided, can be null
public function __construct(
private ?LoggerInterface $logger // Must provide, even if null
) {}
Common DI Scenarios
// Service with required dependencies
public function __construct(
private ProductRepositoryInterface $productRepo,
private CategoryRepositoryInterface $categoryRepo
) {}
// Service with configuration
public function __construct(
private ProductRepositoryInterface $productRepo,
private ScopeConfigInterface $config,
private int $cacheLifetime = 3600
) {}
// Service with optional dependencies
public function __construct(
private ProductRepositoryInterface $productRepo,
private ?LoggerInterface $logger = null,
private ?ProfilerInterface $profiler = null
) {}
DI Best Practices
Keep Constructors Clean
// Bad: too many dependencies (7+)
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 ProductService $productService,
private WarrantyService $warrantyService
) {}
}
Depend on Interfaces
// Good - depends on interface
public function __construct(
private WarrantyRepositoryInterface $repository
) {}
// Bad - depends on concrete class
public function __construct(
private WarrantyRepository $repository // Tightly coupled
) {}
Avoid Circular Dependencies
// BAD: Circular
class ServiceA {
public function __construct(private ServiceB $b) {}
}
class ServiceB {
public function __construct(private ServiceA $a) {}
}
// GOOD: Extract shared logic
class SharedService {
public function __construct(
private ServiceA $a,
private ServiceB $b
) {}
}
Use Plugins Instead of Preferences
<!-- Prefer: extending behavior -->
<type name="Magento\Catalog\Model\Product">
<plugin name="my_plugin" type="Amazon\Prep\Plugin\ProductPlugin"/>
</type>
<!-- Avoid: replacing entirely (unless necessary) -->
<preference for="ProductInterface" type="MyProduct"/>
Testability
// Easy to test - just mock constructor dependencies
class WarrantyServiceTest extends TestCase
{
public function testCreateWarranty(): void
{
$factory = $this->createMock(WarrantyFactory::class);
$resource = $this->createMock(WarrantyResource::class);
$logger = $this->createMock(LoggerInterface::class);
$service = new WarrantyService($factory, $resource, $logger);
$result = $service->createWarranty('Test', 12);
$this->assertNotNull($result);
}
}
Quiz
1. How does ObjectManager resolve an interface type-hint?
2. How do you provide a scalar value (string, int) via DI?
3. What happens if a required constructor parameter cannot be resolved?
Flashcards
Question
How does ObjectManager resolve an interface?
Click to reveal answer
Answer
Checks di.xml for a preference that maps to a concrete class
Question
Can scalar types be auto-wired?
Click to reveal answer
Answer
No, they must be provided via di.xml arguments
Question
What is PHP 8 constructor promotion?
Click to reveal answer
Answer
Declaring properties directly in constructor parameters
Question
What is the difference between required and optional parameters?
Click to reveal answer
Answer
Required has no default value; optional has a default value
Question
How do you avoid circular dependencies?
Click to reveal answer
Answer
Extract shared logic to a third service class
Revision Notes
Key Takeaways
- 1. ObjectManager resolves dependencies by reading constructor type hints
- 2. Interfaces require di.xml preferences; concrete classes are auto-wired
- 3. Scalar types must come from di.xml arguments
- 4. PHP 8 constructor promotion reduces boilerplate code
- 5. Keep constructors clean and depend on interfaces
Interview Tips
- • Explain how ObjectManager resolves constructor dependencies
- • Know the difference between required and optional parameters
- • Be ready to discuss circular dependency solutions
- • Understand when to use preferences vs plugins
Cheat Sheet
Constructor injection:
public function __construct(private Interface $dep) {}
Resolution:
1. di.xml arguments
2. di.xml preferences
3. Auto-wiring (concrete classes)
4. Default values (optional)
Scalar: must use di.xml arguments
Interface: must use di.xml preferences