Creating an application #3 - the container di

In our post today, we will talk about the container di, but first we must know some important terms, which will help us understand the functioning.

Dependency injection according to Wikipedia in software engineering, dependency injection is a design pattern in which an object or function receives other objects or functions that it depends on. A form of inversion of control, dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs.[1][2][3] The pattern ensures that an object or function which wants to use a given service should not have to know how to construct those services. Instead, the receiving ‘client’ (object or function) is provided with its dependencies by external code (an ‘injector’), which it is not aware of.[4] Dependency injection helps by making implicit dependencies explicit and helps solve the following problems:[5]

  • How can a class be independent from the creation of the objects it depends on?
  • How can an application, and the objects it uses support different configurations?
  • How can the behavior of a piece of code be changed without editing it directly?

In addition, dependency injection is used for keeping code in-line with the dependency inversion principle.[6][7]

Fundamentally, dependency injection consists of passing parameters to a method.[8]

Let’s look at the following example:

There are two ways of re-using things in OOP: inheritance and composition.

/**
 * Inheritance is simple.
 */
class Cache
{
    public function getCachedValue($key)
    {
        // ..
    }
}

class CachedWidget extends Cache
{
    public function render(): string
    {
        $output = $this->getCachedValue('cachedWidget');

        if ($output !== null) {
            return $output;
        }

        // ...        
    }
}

The issue here is that these two are becoming unnecessarily coupled or inter-dependent making them more fragile.

/**
 * Composition.
 */
interface CacheInterface
{
    public function getCachedValue($key);
}

final class Cache implements CacheInterface
{
    public function getCachedValue($key)
    {
        // ..
    }
}

final class CachedWidget
{
    private CacheInterface $cache;

    // Dependency injection.
    public function __construct(CacheInterface $cache)
    {
        $this->cache = $cache;
    }
    
    public function render(): string
    {
        $output = $this->cache->getCachedValue('cachedWidget');
        if ($output !== null) {
            return $output;
        }

        // ...        
    }
}

In the above we’ve avoided unnecessary inheritance and used interface to reduce coupling. You can replace cache implementation without changing so it is becoming more stableCachedWidget.

The here is a dependency: an object another object depends on. The process of putting an instance of dependency into an object () is called dependency injection. There are multiple ways to perform it:CacheInterface CachedWidget.

  1. Constructor injection. Best for mandatory dependencies.
  2. Method injection. Best for optional dependencies.
  3. Property injection. Better to be avoided in PHP except maybe data transfer objects.

Using the container di: Usage of the container di is fairly simple: You first initialize it with an array of definitions. The array keys are usually interface names. It will then use these definitions to create an object whenever that type is requested. This happens for example when fetching a type directly from the container somewhere in the application. But objects are also created implicitly if a definition has a dependency to another definition.

Usually a single container is used for the whole application. It is often configured either in the entry script such as or a configuration file:index.php

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()->withDefinitions($definitions);
$container = new Container($config);
/**
 * Definitions.
 * Very important all the values, parameters must go between square brackets.
 */
return [
    EngineInterface::class => EngineMarkOne::class,
    'full_definition' => [
        'class' => EngineMarkOne::class,
        '__construct()' => [42],
        '$propertyName' => 'value',
        'setX()' => [42],
    ],
    'closure' => fn (SomeFactory $factory) => $factory->create('args'),
    'static_call_preferred' => fn () => MyFactory::create('args'),
    'static_call_supported' => [MyFactory::class, 'create'],
    'object' => new MyClass(),
];

As seen above an object can be defined in several ways:

  • In the simple case an interface definition maps an id to a particular class.
  • A full definition describes how to instantiate a class in more detail:
    • class contains the name of the class to be instantiated.
    • __construct() holds an array of constructor arguments.
    • The rest of the config are property values (prefixed with ) and method calls, postfixed with . They are set/called in the order they appear in the array.$()
  • Closures are useful if instantiation is tricky and can better be described in code. When using these, arguments are auto-wired by type. could be used to get current container instance. ContainerInterface
  • If it is even more complicated, it is a good idea to move such code into a factory and reference it as a static call.
  • While it is usually not a good idea, you can also set an already instantiated object into the container.

Definitions is describing a way to create and configure a service, an object or return any other value. It must implement Yiisoft\Definitions\Contract\DefinitionInterface that has a single method resolve(ContainerInterface $container).

References are typically stored in the container or a factory and are resolved into object at the moment of obtaining a service instance or creating an object.

Array definition allows describing a service or an object declaratively:

use \Yiisoft\Definitions\ArrayDefinition;

$definition = ArrayDefinition::fromConfig(
    [
        'class' => MyServiceInterface::class,
        '__construct()' => [42], 
        '$propertyName' => 'value',
        'setName()' => ['Alex'],
    ],
);
$object = $definition->resolve($container);

class: contains the name of the class to be instantiated.

__construct(): holds an array of constructor arguments.

The rest of the config are property values (prefixed with $) and method calls, postfixed with (). They are set/called in the order they appear in the array.

Callable definition builds an object by executing a callable injecting dependencies based on types used in its signature:

use \Yiisoft\Definitions\CallableDefinition;

$definition = new CallableDefinition(
    fn (SomeFactory $factory) => $factory->create('args')
);
$object = $definition->resolve($container);

// or 

$definition = new CallableDefinition(
    fn () => MyFactory::create('args')
);
$object = $definition->resolve($container);

// or

$definition = new CallableDefinition(
    [MyFactory::class, 'create']
);
$object = $definition->resolve($container);

In the above we use a closure, a static call and a static method passed as array-callable. In each case we determine and pass dependencies based on the types of arguments in the callable signature.

Parameter definition resolves an object based on information from ReflectionParameter
instance:

use \Yiisoft\Definitions\ParameterDefinition;

$definition = new ParameterDefinition($reflectionParameter);
$object = $definition->resolve($container);

Value definition resolves value passed as is:

use \Yiisoft\Definitions\ValueDefinition;

$definition = new ValueDefinition(42, 'int');
$value = $definition->resolve($container); // 42

References point to other definitions so when defining a definition you can use other definitions as its dependencies:

[
    InterfaceA::class => ConcreteA::class,
    'alternativeForA' => ConcreteB::class,

    MyService::class => [
        '__construct()' => [
            Reference::to('alternativeForA'),
        ],
    ],
]

Optional reference returns null when there’s no corresponding definition in container:

[
    MyService::class => [
        '__construct()' => [
            // If container doesn't have definition for `EventDispatcherInterface`
            // reference returns `null` when resolving dependencies
            Reference::optional(EventDispatcherInterface::class), 
        ],
    ],
]

Dynamic reference defines a dependency to a service not defined in the container:

[
   MyService::class => [
       '__construct()' => [
           DynamicReference::to(
					     [
                  'class' => SomeClass::class,
                  '$someProp' => 15
               ],
           )
       ]
   ]
]

In order to pass an array of IDs as references to a property or an argument, Yiisoft\Definitions\ReferencesArray or Yiisoft\Definitions\DynamicReferencesArray
could be used:

File: params.php
return [
   'yiisoft/data-response' => [
       'contentFormatters' => [
           'text/html' => HtmlDataResponseFormatter::class,
           'application/xml' => XmlDataResponseFormatter::class,
           'application/json' => JsonDataResponseFormatter::class,
       ],
   ],
];
File: contentnegotiator.php

ContentNegotiator::class => [
    '__construct()' => [
        'contentFormatters' => ReferencesArray::from(
             $params['yiisoft/data-response']['contentFormatters']
        ),
    ],
],

After explaining the functioning, it seems that the configurations are complex, but they are not, the container di, does all the work for you, applying the best practices, you just have to learn the syntax of the container and the references, and everything will be simple, now let’s see the actual example in our app template.

File: config/common/logger.php

<?php

declare(strict_types=1);

use Psr\Log\LoggerInterface;
use Yiisoft\Definitions\ReferencesArray;
use Yiisoft\Log\Logger;
use Yiisoft\Log\Target\File\FileTarget;

/** @var array $params */

return [
    LoggerInterface::class => [
        'class' => Logger::class,
        '__construct()' => [
            'targets' => ReferencesArray::from(
						    [
                   FileTarget::class,
                ],
            ),
        ],
    ],
];
File: config/common/translator.php

<?php

declare(strict_types=1);

use Yiisoft\Aliases\Aliases;
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\IntlMessageFormatter;
use Yiisoft\Translator\Message\Php\MessageSource;

/** @var array $params */

return [
    // Configure application CategorySource
    'translation.app' => [
        'definition' => static function (Aliases $aliases) use ($params) {
            return new CategorySource(
                $params['yiisoft/translator']['defaultCategory'],
                new MessageSource($aliases->get('@messages')),
                new IntlMessageFormatter(),
            );
        },
        'tags' => ['translation.categorySource'],
    ],
];
file: config/web/application.php

<?php

declare(strict_types=1);

use App\Handler\NotFoundHandler;
use Yiisoft\Definitions\DynamicReference;
use Yiisoft\Definitions\Reference;
use Yiisoft\Injector\Injector;
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;

/** @var array $params */

return [
    \Yiisoft\Yii\Http\Application::class => [
        '__construct()' => [
            'dispatcher' => DynamicReference::to(static function (Injector $injector) use ($params) {
                return ($injector->make(MiddlewareDispatcher::class))->withMiddlewares($params['middlewares']);
            }),
            'fallbackHandler' => Reference::to(NotFoundHandler::class),
        ],
    ],
    \Yiisoft\Yii\Middleware\Locale::class => [
        '__construct()' => [
            'locales' => $params['locale']['locales'],
            'ignoredRequests' => $params['locale']['ignoredRequests'],
        ],
        'withEnableSaveLocale()' => [false],
    ],
];

Now we understand how to do any configuration of any YiiFramework package or external, it is not necessary to have a single long and complex configuration file, we can organize it according to the group of configurations and Yii config will do the work for you, as well as the container it applies the definitions for you, with the automatic wiring facility in controllers, which makes it easy to access any container dependency without the need to use static access to it, or depend on the container itself.

In our next post we will talk about the factory, and how to get all the power, flexibility and simplicity of YiiFramework.

Wilmer Arámbula.

Twitter follow

7 Likes