explain implementation of ad-hoc depencency container
This commit is contained in:
parent
988a532b78
commit
2c3901e9f9
3 changed files with 197 additions and 4 deletions
|
@ -10,12 +10,201 @@ to explain the problem and work on a solution.
|
||||||
#### Adding a clock service
|
#### Adding a clock service
|
||||||
|
|
||||||
Lets assume that we want to show the current time in our Hello action. We could easily just call use one of the many
|
Lets assume that we want to show the current time in our Hello action. We could easily just call use one of the many
|
||||||
ways to get the current time directly in the handle-method, but maybe we want to make that configurable and
|
ways to get the current time directly in the handle-method, but lets create a separate class and interface for that so
|
||||||
|
we can later configure and switch our implementation.
|
||||||
|
|
||||||
|
We need a new 'Service\Time' namespace, so lets first create the folder in our 'src' directory 'src/Service/Time'.
|
||||||
|
There we place a Clock.php interface and a SystemClock.php implementation:
|
||||||
|
|
||||||
|
|
||||||
|
The Clock.php interface:
|
||||||
|
```php
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Service\Time;
|
||||||
|
|
||||||
|
interface Clock
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The SystemClock.php implementation:
|
||||||
|
```php
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Service\Time;
|
||||||
|
|
||||||
|
final class SystemClock implements Clock
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can require the Clockinterface as a depencency in our controller and use it to display the current time.
|
||||||
|
```php
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Action;
|
||||||
|
|
||||||
|
|
||||||
|
use Lubian\NoFramework\Service\Time\Clock;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
final class Hello implements RequestHandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ResponseInterface $response,
|
||||||
|
private readonly Clock $clock
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$name = $request->getAttribute('name', 'Stranger');
|
||||||
|
$body = $this->response->getBody();
|
||||||
|
|
||||||
|
$time = $this->clock->now()->format('H:i:s');
|
||||||
|
|
||||||
|
$body->write('Hello ' . $name . '!<br />');
|
||||||
|
$body->write('The Time is: ' . $time);
|
||||||
|
|
||||||
|
return $this->response->withBody($body)
|
||||||
|
->withStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
But if we try to access the corresponding route in the webbrowser we get an error:
|
||||||
|
> Too few arguments to function Lubian\NoFramework\Action\Hello::__construct(), 1 passed in /home/lubiana/PhpstormProjects/no-framework/app/src/Bootstrap.php on line 62 and exactly 2 expected
|
||||||
|
|
||||||
|
Our current problem is, that we have two Actions defined, which both have different constructor requirements. That means,
|
||||||
|
that we need to have some code in our Application, that creates our Action Objects and takes care of injection all the
|
||||||
|
needed dependencies.
|
||||||
|
|
||||||
|
This code is called a Dependency Injector. If you want you can read [this](https://afilina.com/learn/design/dependency-injection)
|
||||||
|
great blogpost about that topic, which I highly recommend.
|
||||||
|
|
||||||
|
Lets build our own Dependency Injector to make our application work again.
|
||||||
|
|
||||||
|
As a starting point we are going to take a look at the [Container Interface])(https://www.php-fig.org/psr/psr-11/) that
|
||||||
|
is widely adopted in the PHP-World.
|
||||||
|
|
||||||
|
#### Building a dependency container
|
||||||
|
|
||||||
|
**Short Disclaimer:** *Although it would be fun to write our own great implementation of this interface with everything that
|
||||||
|
is needed for modern php development I will take a shortcut here and implement very reduced version to show you the
|
||||||
|
basic concept.*
|
||||||
|
|
||||||
|
The `Pst\Container\ContainerIterface` defines two methods:
|
||||||
|
|
||||||
|
* has($id): bool
|
||||||
|
returns true if the container can provide a value for a given ID
|
||||||
|
* get($id): mixed
|
||||||
|
returns some kind of value that is registered in the container for the given ID
|
||||||
|
|
||||||
|
I mostly define an Interface or a fully qualified classname as an ID. That way I can query the container for
|
||||||
|
the Clock interface or an Action class and get an object of that class or an object implementing the given Interface.
|
||||||
|
|
||||||
|
For the sake of this tutorial we will put a new file in our config folder that returns an anonymous class implementing
|
||||||
|
the containerinterface.
|
||||||
|
|
||||||
|
In this class we will configure all services required for our application and make them accessible via the get($id)
|
||||||
|
method.
|
||||||
|
|
||||||
|
`config/container.php`:
|
||||||
|
```php
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
return new class () implements \Psr\Container\ContainerInterface {
|
||||||
|
|
||||||
|
private readonly array $services;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->services = [
|
||||||
|
\Psr\Http\Message\ServerRequestInterface::class => fn () => \Laminas\Diactoros\ServerRequestFactory::fromGlobals(),
|
||||||
|
\Psr\Http\Message\ResponseInterface::class => fn () => new \Laminas\Diactoros\Response(),
|
||||||
|
\FastRoute\Dispatcher::class => fn () => \FastRoute\simpleDispatcher(require __DIR__ . '/routes.php'),
|
||||||
|
\Lubian\NoFramework\Service\Time\Clock::class => fn () => new \Lubian\NoFramework\Service\Time\SystemClock(),
|
||||||
|
\Lubian\NoFramework\Action\Hello::class => fn () => new \Lubian\NoFramework\Action\Hello(
|
||||||
|
$this->get(\Psr\Http\Message\ResponseInterface::class),
|
||||||
|
$this->get(\Lubian\NoFramework\Service\Time\Clock::class)
|
||||||
|
),
|
||||||
|
\Lubian\NoFramework\Action\Other::class => fn () => new \Lubian\NoFramework\Action\Other(
|
||||||
|
$this->get(\Psr\Http\Message\ResponseInterface::class)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $id)
|
||||||
|
{
|
||||||
|
if (! $this->has($id)) {
|
||||||
|
throw new class () extends \Exception implements \Psr\Container\NotFoundExceptionInterface {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return $this->services[$id]();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $id): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($id, $this->services);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Here I have declared a services array, that has a class- or interfacename as the keys, and the values are short
|
||||||
|
closures that return an Object of the defined class or interface. The `has` method simply checks if the given id is
|
||||||
|
defined in our services array, and the `get` method calls the closure defined in the array for the given id key and then
|
||||||
|
returns the result of that closure.
|
||||||
|
|
||||||
|
To use the container we need to update our Bootstrap.php. Firstly we need to get an instance of our container, and then
|
||||||
|
use that to create our Request-Object as well as the Dispatcher. So remove the manual instantion of those objects and
|
||||||
|
replace that with the following code:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$container = require __DIR__ . '/../config/container.php';
|
||||||
|
assert($container instanceof \Psr\Container\ContainerInterface);
|
||||||
|
|
||||||
|
$request = $container->get(\Psr\Http\Message\ServerRequestInterface::class);
|
||||||
|
assert($request instanceof \Psr\Http\Message\ServerRequestInterface);
|
||||||
|
|
||||||
|
$dispatcher = $container->get(FastRoute\Dispatcher::class);
|
||||||
|
assert($dispatcher instanceof \FastRoute\Dispatcher);
|
||||||
|
```
|
||||||
|
|
||||||
|
In the Dispatcher switch block we manually build our handler object with this two lines:
|
||||||
|
|
||||||
|
|
||||||
|
```php
|
||||||
|
$handler = new $className($response);
|
||||||
|
assert($handler instanceof RequestHandlerInterface);
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead of manually creating the Handler-Instance we are going to kindly ask the Container to build it for us:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$handler = $container->get($className);
|
||||||
|
assert($handler instanceof RequestHandlerInterface);
|
||||||
|
```
|
||||||
|
|
||||||
|
If you now open the `/hello` route in your browser everything should work again!
|
||||||
|
|
||||||
|
#### Using Autowiring
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A dependency injector resolves the dependencies of your class and makes sure that the correct objects are injected when
|
A dependency injector resolves the dependencies of your class and makes sure that the correct objects are injected when
|
||||||
the class is instantiated.
|
the class is instantiated.
|
||||||
|
|
||||||
Again the psr has defined an [interface](https://www.php-fig.org/psr/psr-11/) for dependency injection that we can work
|
Again the FIG has defined an [interface](https://www.php-fig.org/psr/psr-11/) for dependency injection that we can work
|
||||||
with. Almost all common dependency injection containers implement this interface, so it is a good starting point to look
|
with. Almost all common dependency injection containers implement this interface, so it is a good starting point to look
|
||||||
for a [suitable solution on packagist](https://packagist.org/providers/psr/container-implementation).
|
for a [suitable solution on packagist](https://packagist.org/providers/psr/container-implementation).
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Lubian\NoFramework\Action;
|
namespace Lubian\NoFramework\Action;
|
||||||
|
|
||||||
|
|
||||||
use Lubian\NoFramework\Service\Time\Clock;
|
use Lubian\NoFramework\Service\Time\Clock;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
@ -9,7 +10,10 @@ use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
final class Hello implements RequestHandlerInterface
|
final class Hello implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ResponseInterface $response, private readonly Clock $clock)
|
public function __construct(
|
||||||
|
private readonly ResponseInterface $response,
|
||||||
|
private readonly Clock $clock
|
||||||
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ try {
|
||||||
switch ($routeInfo[0]) {
|
switch ($routeInfo[0]) {
|
||||||
case Dispatcher::FOUND:
|
case Dispatcher::FOUND:
|
||||||
$className = $routeInfo[1];
|
$className = $routeInfo[1];
|
||||||
$handler = $container->get($className);
|
$handler = new $className($response);
|
||||||
assert($handler instanceof RequestHandlerInterface);
|
assert($handler instanceof RequestHandlerInterface);
|
||||||
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
|
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
|
||||||
$request = $request->withAttribute($attributeName, $attributeValue);
|
$request = $request->withAttribute($attributeName, $attributeValue);
|
||||||
|
|
Loading…
Reference in a new issue