Constructor Injection

Type-hint an interface in the constructor and Magento passes an instance; promoted properties mean the same as assigning them.

Syntaxpublic function __construct(private readonly ScopeConfigInterface $scopeConfig) { }

Constructor injection is how a class receives everything it needs. You declare parameters, type them, and Magento supplies them. There is no setter injection, no service locator and no container variable to reach for.

What Magento passes

For a concrete class, Magento builds that class. For an interface, it builds whichever implementation is bound to it. Objects are shared by default: within one request, two classes asking for Magento\Framework\App\Config\ScopeConfigInterface receive the same instance. A name ending in Factory or \Proxy needs no file on disk, because Magento generates those classes.

Shorthand and longhand

Constructor property promotion — private readonly ScopeConfigInterface $scopeConfig in the parameter list — declares the property and assigns it in one step. The longhand declares the property above the constructor and assigns it in the body. They compile to the same class. Core modules use the longhand, so you will read both.

Parameter names and order are an API

Configuration binds arguments by parameter name, and subclasses call parent::__construct() positionally. Never rename, reorder or remove a parameter in released code. Add new dependencies last, with a default.

Making it take effect

In developer mode the constructor is read on each request. In production mode the signature comes from the compiled map in generated/metadata/global.php, so a changed constructor needs bin/magento setup:di:compile.

Example

Example · php
<?php
declare(strict_types=1);
// app/code/Vendor/Storefront/Service/StoreGreeting.php

namespace Vendor\Storefront\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class StoreGreeting
{
    private const PATH_STORE_NAME = 'general/store_information/name';

    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig
    ) {
    }

    public function forStore(?int $storeId = null): string
    {
        $name = (string) $this->scopeConfig->getValue(
            self::PATH_STORE_NAME,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );

        if ($name === '') {
            return 'Welcome';
        }

        return sprintf('Welcome to %s', $name);
    }
}

// Output when general/store_information/name holds "Acme Tools":
// Welcome to Acme Tools

When to use it

  • Pulling a store configuration value into a service without touching global state.
  • Reading a core class and working out what it will be handed at runtime.
  • Adding a logger to a class that other modules already extend, without breaking them.

More examples

Longhand: the form core modules use

Identical to the promoted version in the primary example: same property, same visibility, same assignment. Magento 2.4.9 core is written this way, with the docblock carrying the type, so you need to recognise it. New module code can use either, as long as one file does not mix the two.

Example · php
<?php
declare(strict_types=1);
// app/code/Vendor/Storefront/Service/StoreGreeting.php

namespace Vendor\Storefront\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;

class StoreGreeting
{
    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    public function __construct(ScopeConfigInterface $scopeConfig)
    {
        $this->scopeConfig = $scopeConfig;
    }
}

Adding a dependency without breaking subclasses

The pattern core uses when a released class gains a dependency. The object manager call is allowed here precisely because the alternative is a signature change that breaks other people's code. Run bin/magento setup:di:compile after it, so the compiled map records the new argument.

Example · php
<?php
declare(strict_types=1);
// app/code/Vendor/Storefront/Service/StoreGreeting.php

namespace Vendor\Storefront\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Psr\Log\LoggerInterface;

class StoreGreeting
{
    private LoggerInterface $logger;

    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig,
        ?LoggerInterface $logger = null
    ) {
        // New parameter last, nullable, with a fallback. Existing
        // subclasses calling parent::__construct($scopeConfig) keep
        // working. This is the backward-compatibility exception.
        $this->logger = $logger ?? ObjectManager::getInstance()
            ->get(LoggerInterface::class);
    }
}

Interface versus concrete type hint

Both hints are legal, and the difference matters when you want to replace something later. A preference can re-bind either one site-wide, because the for attribute accepts a class name as well as an interface name; the difference is that a replacement for a concrete class must extend it to satisfy the type hint, while an interface lets you supply a completely unrelated implementation. Type-hint the interface whenever one exists, which in Magento is almost always.

Example · php
<?php
declare(strict_types=1);
// app/code/Vendor/Storefront/Service/LabelBuilder.php

namespace Vendor\Storefront\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Escaper;

class LabelBuilder
{
    public function __construct(
        // Interface: configuration decides which class arrives.
        private readonly ScopeConfigInterface $scopeConfig,
        // Concrete class: this exact class is built.
        private readonly Escaper $escaper
    ) {
    }

    public function label(string $raw): string
    {
        return $this->escaper->escapeHtml($raw);
    }
}

Discussion

  • Be the first to comment on this lesson.