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)
{}
// ...
}