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/Module/User/Create/Service/UserCreator.php
namespace App\Module\User\Create\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,
) {}
/**
* Simplified version of a create user service function
*/
public function createUser(array $userValues): bool|int
{
// The methods of the injected classes can be called directly on the class instance
$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)
{}
// ...
}