Framework Documentation - DI Package

Overview

The Joomla! Dependency Injection package provides a simple PSR-11 compatible Inversion of Control (IoC) Container for your application.

The Joomla! Dependency Injection Container supports

  • factories and instances (i.e., closures, callables, objects, arrays, and even scalars)
  • shared and protected modes
  • caching and cloning of resources according to the modes
  • enforcing recreation of resources
  • aliases
  • scopes using the decorator pattern for containers
  • delegate lookup
  • object creation on the fly to resolve dependencies
  • ability to extend resources
  • service providers
  • tagged services

In this document,

  • factory is a callable or closure, that takes the container as argument and returns the resource instance;
  • instance is the resource instance, i.e., the return value of a factory, or an explicitly defined value (may be a scalar as well);
  • resource is a key/value pair, with the key being the id, and the value being a factory or an instance.

Container Interoperability

The Joomla! Dependency Injection package implements the PSR-11 ContainerInterface for Dependency Injection Containers to achieve interoperability.

Creating a Container object

Creating a container usually happens very early in the application lifecycle. It can be created even before the application is instantiated, and provided to the application as an external dependency. This allows your application access to the DI Container, which you can then use within the application class to build your controllers and their dependencies.

<?php
$container = (new Joomla\DI\Container)
    ->registerServiceProvider(new MyApp\Service\CoolProvider)
    ->registerServiceProvider(new MyApp\Other\CoolStuffProvider)
    ->registerServiceProvider(new MyApp\ApplicationProvider);

/** @var \Joomla\Application\AbstractApplication $app */
$app = $container->get(MyApp\Application::class);
$app->execute();

Hierarchical Containers

Decorating other Containers

If you have any other container implementing the ContainerInterface, you can pass it to the constructor.

<?php
use \Joomla\DI\Container;

$container = new Container($arbitraryPsr11Container);

You'll then be able to access any resource from $arbitraryPsr11Container through $container, thus virtually adding the features (like aliasing) of the Joomla! DI Container to the other one.

Scopes

The decoration feature can be used to manage different resolution scopes.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->set('Some\Interface\I\NeedInterface', new My\App\InterfaceImplementation);

// Application executes... Come to a class that needs a different implementation.

$child = new Container($container);
$child->set('Some\Interface\I\NeedInterface', new My\Other\InterfaceImplementation);

This allows you to easily override an interface binding for a specific controller, without destroying the resolution scope for the rest of the classes using the container.

A child container will search recursively through its parent containers to resolve all the required dependencies. For this behaviour, a convenience method createChild is provided.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->set('Some\Interface\I\NeedInterface', new My\App\InterfaceImplementation);

// Application executes... Come to a class that needs a different implementation.

$child = $container
    ->createChild()
    ->set('Some\Interface\I\NeedInterface', new My\Other\InterfaceImplementation);

Setting an Item

Setting an item within the container is very straightforward.

You pass the set method a string $key and a $value, which can be pretty much anything. If the $value is an anonymous function or a Closure, or a callable value, that value will be set as the resolving callback for the $key. If it is anything else (an instantiated object, array, integer, serialized controller, etc) it will be wrapped in a closure and that closure will be set as the resolving callback for the $key.

If the $value you are setting is a closure or a callable, it will receive a single function argument, the calling container. This allows access to the container within your resolving callback.

<?php
use Joomla\DI\Container;

$container = new Container;

// Setting a scalar
$container->set('foo', 'bar');

// Setting an object
$container->set('something', new Something);

// Setting a callable
$container->set('callMe', [$this, 'callMe']);

// Setting a closure
$container->set(
    'baz',
    function (Container $c)
    {
        // some expensive $stuff;

        return $stuff;
    }
);

Resource Options

When setting items in the container, you can specify whether or not the item is supposed to be a shared or protected item.

<?php
use Joomla\DI\Container;

$container = new Container;

$shared = true;
$protected = true;

$container->set(
    'foo',
    function ()
    {
        // some expensive $stuff;

        return $stuff;
    },
    $shared,
    $protected
);

The default mode for a resource is 'not shared' and 'not protected'.

If a container was passed to the constructor, which is not a Joomla\DI\Container, the resources from that container are treated as 'shared' and 'protected'.

Container provides convenience methods for setting shared and protected resources.

Shared Resources

A shared item means that when you get an item from the container, the resolving callback will be fired once, and the value will be stored and used on every subsequent request for that item. You can set a shared resource using the share method.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->share(
    'foo',
    function ()
    {
        // some expensive $stuff;

        return $stuff;
    }
);

Resources set with share are not protected by default.

If you pass true as third argument, you can both share AND protect an item. A good use case for this would be a database connection that you only want one of, and you don't want it to be overwritten.

You can check whether a resource is shared with the isShared method.

<?php
var_dump($container->isShared('foo')); // prints bool(true) for the example above
Protected Resources

The other option, protected, is a special status that you can use to prevent others from overwriting the item down the line. A good example for this would be a global config that you don't want to be overwritten.

<?php
use Joomla\Database\DatabaseFactory;
use Joomla\DI\Container;

$container = new Container;

// Don't overwrite my db connection.
$container->protect(
    'bar',
    function (Container $c)
    {
        $config = $c->get('config');

        $databaseConfig = (array) $config->get('database');

        return (new DatabaseFactory)->getDriver($databaseConfig['driver'], $databaseConfig);
    }
);

Resources set with protect are not shared by default.

If you pass true as third argument, you can both share AND protect an item.

You can check whether a resource is protected with the isProtected method.

<?php
var_dump($container->isProtected('bar')); // prints bool(true) for the example above

Item Aliases

Any item set in the container can be aliased. This allows you to create an object that is a named dependency for object resolution, but also have a "shortcut" access to the item from the container.

You get the same resource with the alias as you would with the original key.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->set(
    'Really\Long\ConfigClassName',
    function ()
    {
        // ...snip
    }
);

$container->alias('config', 'Really\Long\ConfigClassName');

$container->get('config'); // Returns the value set on the aliased key.

Getting an Item

At its most basic level, the DI Container is a registry that holds keys and values. When you set an item on the container, you can retrieve it by passing the same $key to the get method that you did when you set the method in the container.

If you've aliased a set item, you can also retrieve it using the alias key.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->set('foo', 'bar');

$foo = $container->get('foo'); // $foo now contains 'bar'

Normally, the value you'll be passing will be a closure. When you fetch the item from the container, the closure is executed, and the result is returned.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->set(
    'github',
    function ()
    {
        // Create an instance of \Joomla\Github\Github;

        return $github;
    }
);

$github = $container->get('github');

var_dump($github instanceof \Joomla\Github\Github); // prints bool(true)

If you get the item again, the closure is executed again and the result is returned.

<?php
// Picking up from the previous code block

$github2 = $container->get('github');

var_dump($github2 === $github); // prints bool(false)

However, if you specify that the object as shared when setting it in the container, the closure will only be executed once (the first time it is requested). The value will be stored and then returned on every subsequent request.

<?php
use Joomla\DI\Container;

$container = new Container;

$container->share(
    'twitter',
    function ()
    {
        // Create an instance of \Joomla\Twitter\Twitter;

        return $twitter;
    }
);

$twitter  = $container->get('twitter');
$twitter2 = $container->get('twitter');

var_dump($twitter === $twitter2); // prints bool(true)

If you've specified an item as shared, but you really need a new instance of it for some reason, you can force the creation of a new instance by using the getNewInstance method.

When you force create a new instance on a shared object, that new instance replaces the instance that is stored in the container and will be used on subsequent requests.

<?php
// Picking up from the previous code block

$twitter3 = $container->getNewInstance('twitter');

var_dump($twitter === $twitter3); // prints bool(false)

$twitter4 = $container->get('twitter');
var_dump($twitter3 === $twitter4); // prints bool(true)

If you try to retrieve a resource for an undefined key, an InvalidArgumentException is thrown.

To avoid that, the existence of a resource can be checked with the has method. It returns true if the container knows the key, and false otherwise.

Of course, has also resolves an alias (if set).

Building Objects

The most powerful feature of setting an item in the container is the ability to bind an implementation to an interface. This is useful when using the container to build your app objects. You can typehint against an interface, and when the object gets built, the container will pass your implementation.

This gives you great flexibility to build your objects within the container. If your model class requires a user repository, you can typehint against a UserRepositoryInterface and then bind an implementation to that interface to be passed into the model when it is created.

<?php
use Joomla\DI\Container;

interface UserRepositoryInterface {}

class User implements UserRepositoryInterface {}

class UserProfile
{
    /**
     * The user (property should not be public in production code!)
     *
     * @var  UserRepositoryInterface
     */
    public $user;

    public function __construct(UserRepositoryInterface $user)
    {
        $this->user = $user;
    }
}

$container = new Container;

// Tell the container, that we want `User` to fulfill the `UserRepositoryInterface` dependency
$container->set(
    'UserRepositoryInterface',
    function ()
    {
        return new User;
    }
);

// Create the `UserProfile` object.
$userProfile = $container->buildObject('UserProfile');

var_dump($userProfile->user instanceof User); // prints bool(true)
var_dump($userProfile->user instanceof UserRepositoryInterface); // prints bool(true)

As the last example shows, the power lies in the container's ability to build complete objects, instantiating any needed dependency along the way.

How it works

To do that, the container looks at the constructor of the class being instantiated. It then tries to resolve the dependencies using the typehints.

If a typehint requires an interface, the dependency is resolved from the container's known resources.

Dependencies not known by the container are created, if possible, following the same rules as the current resource.

Scalar arguments are provided with their default value.

When it does not work

The container can only resolve dependencies that have been properly typehinted or given a default value. If the resource can not be built, a DependencyResolutionException is thrown.

When you try to build a non-class, buildObject and buildSharedObject both return false.

Sometimes circular dependencies are encountered.

<?php
use Joomla\DI\Container;

$container = new Container();

$fqcn = 'Extension\\vendor\\FooComponent\\FooComponent';
$data = array();

$container->set(
    $fqcn,
    function (Container $c) use ($fqcn, $data)
    {
        $instance = $c->buildObject($fqcn);
        $instance->setData($data);

        return $instance;
    }
);

$container->get($fqcn);

It is not possible for the container to resolve that, as it produces an endless loop. However, Container detects that and throws a DependencyResolutionException.

Shared and non-shared Objects

When you build an object, it is stored as a known resource in the container with the class name as the key. You can then get the item from the container by class name later on. Alias support applies here as well.

You can also specify to build a shared object by using the function buildSharedObject($key). This works exactly as you would expect. The instantiated object is cached and returned on subsequent requests.

Extending an Item

The Container also allows you to extend items. Extending an item can be thought of as a way to implement the decorator pattern, although it's not really in the strictest sense. When you extend an item, you must pass the key for the item you want to extend, and then a closure as the second argument.

The closure will receive 2 arguments. The first is result of the callable for the given key, and the second will be the container itself.

When extending an item, the new extended version overwrites the existing item in the container.

<?php
use Joomla\DI\Container;

$container = new Container();

$container->set('foo', 'bar');

var_dump($container->get('foo')); // prints string(3) "bar"

$container->extend(
    'foo',
    function ($originalResult, Container $c)
    {
        return $originalResult . 'baz';
    }
);

var_dump($container->get('foo')); // prints string(6) "barbaz"

If you try to extend an item that does not exist, an KeyNotFoundException will be thrown.

When extending an item, normal rules apply. A protected object cannot be overwritten, so you also can not extend them.

Service Providers

Another strong feature of the Container is the ability register a service provider to the container. Service providers are useful in that they are a simple way to encapsulate setup logic for your objects. In order to use create a service provider, you must implement the Joomla\DI\ServiceProviderInterface.

The ServiceProviderInterface tells the container that your object has a register method that takes the container as its only argument.

Registering service providers is typically done very early in the application lifecycle. Usually right after the container is created.

<?php
use Joomla\Database\Service\DatabaseProvider;
use Joomla\DI\Container;

$container = new Container;

// This example uses the service provider from the `joomla/database` package
$container->registerServiceProvider(new DatabaseProvider);

Here is an alternative using a callable.

<?php
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

class CallableServiceProvider implements ServiceProviderInterface
{
    public function getCallable(Container $c)
    {
        return 'something';
    }

    public function register(Container $c)
    {
        $c->set('callable', [$this, 'getCallable']);
    }
}

$container = new Container();

$container->registerServiceProvider(new CallableServiceProvider);

The advantage here is that it is easier to write unit tests for the callable method (closures can be awkward to isolate and test).

Tagged Services

Services within a container can be grouped by using tags. This is useful in applications which can have a dynamic set of arguments, such as event subscribers for the joomla/event package.

To tag a collection of services, the container's tag() method should be called with a tag name and a list of services to add the tag to. Note this method can be called multiple times and will ensure that only unique service keys are stored. Also note that this method will resolve aliases, so a service should be tagged after it is added to the container.

To fetch all services with a tag, the container's getTagged() method should be called with a tag name. This will execute a resource's callback if it has not yet already been called.

<?php
use Joomla\Application\Event\ApplicationErrorEvent;
use Joomla\Application\ApplicationEvents;
use Joomla\Database\DatabaseEvents;
use Joomla\Database\Event\ConnectionEvent;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\Dispatcher;
use Joomla\Event\SubscriberInterface;

class ApplicationEventSubscriber implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            ApplicationEvents::ERROR => ['onError']
        ];
    }

    public function onError(ApplicationErrorEvent $event)
    {
        // do something
    }
}

class DatabaseEventSubscriber implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            DatabaseEvents::POST_CONNECT => ['onPostConnect']
        ];
    }

    public function onPostConnect(ConnectionEvent $event)
    {
        // do something
    }
}

class EventServiceProvider implements ServiceProviderInterface
{
    public function getDispatcher(Container $container): Dispatcher
    {
        $dispatcher = new Dispatcher;

        foreach ($container->getTagged('event_subscriber') as $subscriber)
        {
            $dispatcher->addSubscriber($subscriber);
        }

        return $dispatcher;
    }

    public function getEventSubscriberApplication(Container $container): ApplicationEventSubscriber
    {
        return new ApplicationEventSubscriber;
    }

    public function getEventSubscriberDatabase(Container $container): DatabaseEventSubscriber
    {
        return new DatabaseEventSubscriber;
    }

    public function register(Container $container)
    {
        $container->set('dispatcher', [$this, 'getDispatcher']);
        $container->set('event_subscriber.application', [$this, 'getEventSubscriberApplication']);
        $container->set('event_subscriber.database', [$this, 'getEventSubscriberDatabase']);

        $container->tag('event_subscriber', ['event_subscriber.application', 'event_subscriber.database']);
    }
}

$container = new Container;
$container->registerServiceProvider(new EventServiceProvider);

Container Aware Objects

You are able to make objects ContainerAware by implementing the Joomla\DI\ContainerAwareInterface in your class. This can be useful when used within the construction level of your application. The construction level is considered to be anything that is responsible for the creation of other objects.

When using the MVC pattern as recommended by Joomla, this can be at the application or controller level. Controllers in Joomla applications are responsible for creating Models and Views, and linking them together. In this case, it would be reasonable for the controllers to have access to the container in order to build these objects.

NOTE: The business layer of your app (eg: Models) should never be container aware. Doing so will make your code harder to test, and is a far cry from best practices.

Container Aware Trait

To help with making objects container aware, the Joomla\DI\ContainerAwareTrait is available to fulfill the ContainerAwareInterface requirements.

If you try to retrieve a container that was not set before, a ContainerNotFoundException is thrown.

Usage:

<?php
use Joomla\Controller\AbstractController;
use Joomla\DI\ContainerAwareInterface;
use Joomla\DI\ContainerAwareTrait;

class MyController extends AbstractController implements ContainerAwareInterface
{
    use ContainerAwareTrait;

    public function execute()
    {
        $container = $this->getContainer();
    }
}

Internal Representation of Resources

@todo

Exceptions

@todo

#