Skip to content
intermediate Phase 27 · DI Fundamentals

Constructor Injection in Detail

Deep dive into Magento 2 constructor injection: PHP 8 constructor promotion, required vs optional parameters, DI resolution, and best practices

45m
0 problems
Topic Progress 0%

How ObjectManager Resolves Dependencies

Resolution Process

When ObjectManager creates an object, it:

  1. Reads the class constructor parameters
  2. Checks di.xml for type configurations
  3. Resolves each parameter type:
    • Interface → check for preference
    • Concrete class → create instance
    • Scalar type → check di.xml arguments
    • Null/optional → skip if not configured
  4. 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?

Question 1 options

2. How do you provide a scalar value (string, int) via DI?

Question 2 options

3. What happens if a required constructor parameter cannot be resolved?

Question 3 options

Flashcards

Question

How does ObjectManager resolve an interface?

Answer

Checks di.xml for a preference that maps to a concrete class

Question

Can scalar types be auto-wired?

Answer

No, they must be provided via di.xml arguments

Question

What is PHP 8 constructor promotion?

Answer

Declaring properties directly in constructor parameters

Question

What is the difference between required and optional parameters?

Answer

Required has no default value; optional has a default value

Question

How do you avoid circular dependencies?

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