Let’s talk about Dependency Injection!
SOLID principles
As you know SOLID is an acronym for the five object-oriented design principles. In this topic, we will focus on Interface segregation principle and Dependency inversion principle.
Interface segregation principle states that a client must not be forced to implement an interface that they do not use, or clients shouldn’t be forced to depend on methods they do not use. In other words, having many client-specific interfaces is better than one general-purpose interface.
From the other side, Dependency inversion principle states that objects must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
To follow Dependency inversion principle, we need to construct low-level modules and pass them to constructors, and that might create a lot of manual work for developers. The dependency injection container is created specifically for solving the problem with manual construction of an object, before creating a specific object.
If we follow interface segregation principle when developing application modules, it would be easy to configure a container and switch module dependency. This is where the interface shows its incredible power.
Few words about CakePHP Events System
CakePHP Events System was created to allow injecting some logic using listeners. However, in some cases, it is used to get results from code that will be created by the module user. When an event is dispatched by the listener, it can return the result.
Callback injection through the event system has some drawbacks. First of all, parameters passed to the event need to pass as a hash array. So unfortunately, there is no way to check that all params are really passed or to be sure that all passed params have correct types.
Is there a way to solve this problem? Yes, and containers could help with that. Instead of passing events, we can get the required object from the container and call it method. But you could say: wait, we don't know what object could be used in client code within the developed plugin. That's fine, and this is where interface segregation principle can help.
In our plugin, we define an interface for each such case, and instead of dispatching an event, we can easily get an object from the container by interface.
$updater = $container->get(AfterLoginInterface::class);
if ($updater !== null) {
$user = $updater->afterLogin($user);
}
In the Application::services method, users link the interface with the specific class.
public function services(ContainerInterface $container): void
{
$container->add(AfterLoginInterface::class, MyAfterLogin::class);
}
In some of default behavior needed we can map service class for container to default implementation using Plugin::services method.
public function services(ContainerInterface $container): void
{
if (!$container->has(AfterLoginInterface::class)) {
$container->add(AfterLoginInterface::class, NullAfterLogin::class);
}
}
Container propagation
Dependency injection is an experimental feature.
Initial implementation limited by Controllers constructors and methods, and Commands constructors.
If we want to access the container in other parts of the application, we may want to propagate it from app level. The most logical way would be to implement middleware and store the container inside the request attribute.
<?php
declare(strict_types=1);
namespace App\Middleware;
use Cake\Core\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
/**
* Container Injector Middleware
*/
class ContainerInjectorMiddleware implements MiddlewareInterface
{
/**
* @var \Cake\Core\ContainerInterface
*/
protected $container;
/**
* Constructor
*
* @param \Cake\Core\ContainerInterface $container The container to build controllers with.
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Serve assets if the path matches one.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
* @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
* @return \Psr\Http\Message\ResponseInterface A response.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $handler->handle($request->withAttribute('container', $this->container));
}
}
That’s it! I hope that this will help you when you are baking with dependency injections. If you run into any problems, there are many support channels that allow the CakePHP community to help You can check them out under the community tab at CakePHP.org.