Dependency Injection

Table of contents

Loading...

What is Dependency Injection

When coding, we need a way to access code from other files. For instance, when we want to create a new user, it makes sense to split the code in different files that are in different layers of the application. Receiving the user request (Action), validating the data (Service) and creating the user in the database (Repository) are totally different things and should be handled by different classes.

In native PHP this can be done with the require or include statements. But it quickly becomes a mess and leads to problems because everything required is basically like one huge file with all the associated drawbacks. It's complex, not modular, almost impossible to test, and tough to maintain.

This is where dependency injection and the autoloader come in handy.

The autoloader allows us to refer to classes via a PSR-4 alias (such as App for src/) and the declared namespace of the class. App/Namespace/Classname for example.
Instead of using the fully qualified name in the code (which includes the namespace and the class name), the class can be referred to, just by its classname, by declaring it at the top of the file with the use statement.

With dependency injection, the class instances can be loaded by "injecting" them directly as arguments into the constructor of the classes that need them.

If we take the user use-case as example, we can inject the UserValidator and the UserCreatorRepository into the service UserCreator.

The reason we can't just create a new instance of the repository like new UserCreatorRepository() inside UserCreator, is that the repository needs the database connection with the secret database credentials from the configuration file which isn't practical to access in the UserCreator class.

In the following example, the UserValidator and UserCreatorRepository instances are injected into the UserCreator constructor which are then available in the entire UserCreator class.

File: src/Domain/User/Service/UserCreator.php

namespace App\Domain\User\Service;

use App\Domain\User\Repository\UserCreatorRepository;
use App\Domain\User\Service\UserValidator;

final readonly class UserCreator
{
    public function __construct(
        private UserValidator $userValidator,
        private UserCreatorRepository $userCreatorRepository,
    ) {}
}

The methods of the injected classes can be accessed in the class with $this->userValidator->validationMethod() and $this->userCreatorRepository->repositoryMethod().

/**
* Simplified version of a create user service function
*/
public function createUser(array $userValues): bool|int
{
    $this->userValidator->validateUser($userValues);
    $userId = $this->userCreatorRepository->insertUser($userValues);
    return $userId;
}

This magic is made possible by the DI container library php-di/php-di autowiring feature.

Autowiring

Autowiring means that the library automatically creates instances without them having to be instantiated manually.
It checks what instances are needed in the constructor of the class and finds the corresponding classes in the container (either because they are manually configured or also autowired), and uses them to create an instance.

In the example above, the UserCreator class constructor requires UserValidator and UserCreatorRepository objects to be instantiated.

The UserCreator instance can be injected into another class's constructor, and the autowiring feature will take care of injecting UserValidator and UserCreatorRepository instances into this UserCreator instance.

Manual configuration

The class QueryFactory that is used by the slim-example-project and slim-starter to create SQL queries uses the cakephp/databse Connection class for the connection to the database:

use Cake\Database\Connection;

final readonly class QueryFactory
{
    public function __construct(public Connection $connection)
    {}
    // ...
}

If we take a look at the Connection class's constructor, we can see that it requires an array $config.

class Connection implements ConnectionInterface
{
    public function __construct(array $config)
    {
        // ...
    }
}

Now were is this $config array supposed to come from and where is the Connection object instantiated?
This can't automatically be handled by the autowiring features because it needs the database credentials.

The Connection instance must be created via the DI container.

Dependency Injection Container

The container.php file is a key-value store that holds all the classes that must be instantiated manually.
Either because they require a specific configuration or because more logic is associated with their instantiation like in the case of the App class that starts the framework.

In the following example, the Connection class is defined in the container and the instance created with an array of the database configuration values as argument for the constructor.
By using the array key Connection::class, the instance can be injected with the Connection classname into other classes like in the QueryFactory.

The container can also be used to define other values than class instances.

Definitions can be retrieved in the container file itself with $container->get('key') which enables access to the configuration values.

File: config/container.php

use Cake\Database\Connection;
use Psr\Container\ContainerInterface;

return [
    'settings' => function () {
        return require __DIR__ . '/settings.php';
    },
    // Database connection
    Connection::class => function (ContainerInterface $container) {
        // Get the container key 'settings' and retrieve the 'db' array
        $dbSettings = $container->get('settings')['db'];
        // Create the Connection instance with the database configuration as argument
        return new Connection($dbSettings);
    },

    // ...
];

The Connection class holding the right database credentials can now be injected into the QueryFactory.

use Cake\Database\Connection;

final readonly class QueryFactory
{
    public function __construct(public Connection $connection)
    {}
    // ...
}
^