no-framework-tutorial/09-dependency-injector.md

244 lines
10 KiB
Markdown
Raw Normal View History

2022-03-29 18:35:06 +00:00
[<< previous](08-inversion-of-control.md) | [next >>](10-invoker.md)
### Dependency Injector
2022-05-20 07:28:47 +00:00
In the last chapter we rewrote our Actions to require the response-objet as a constructor parameter, and provided it
in the dispatcher section of our `Bootstrap.php`. As we only have one dependency this works really fine, but if we have
2022-05-20 23:19:14 +00:00
different classes with different dependencies our bootstrap file gets complicated quite quickly. Let's look at an example
2022-05-20 07:28:47 +00:00
to explain the problem and work on a solution.
#### 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
2022-05-20 23:19:14 +00:00
ways to get the current time directly in the handle-method, but let's 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 Clock interface is modelled after the [proposed clock interface psr](https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md)
which may or may not one day be accepted as an official standard.
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();
}
}
```
2022-05-20 23:19:14 +00:00
Now we can require the Clock interface as a dependency 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);
}
}
```
2022-05-20 23:19:14 +00:00
But if we try to access the corresponding route in the browser 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 `Psr\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
2022-05-20 23:19:14 +00:00
the container-interface.
In this class we will configure all services required for our application and make them accessible via the get($id)
method.
p
Before we can implement the interface we need to install its definition with composer `composer require "psr/container:^1.0"`.
now we can create a file with a Class that implements that interface.
`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);
}
};
```
2022-05-20 23:19:14 +00:00
Here I have declared a services array, that has a class- or interface name 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
2022-05-20 23:19:14 +00:00
use that to create our Request-Object as well as the Dispatcher. So remove the manual instantiation 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!
2022-05-20 23:19:14 +00:00
#### Using Auto wiring
If you take a critical look at the services array you might see that we need to manually define how our Hello- and
Other-Action are getting constructed. This is quite repetitive, as we have already declared what objects to create
when asking for the ResponseInterface and the Clock-Interface. We would need to write way less code, if our Container
was smart enough to automatically figure our which services to Inject by looking at the constructor of a class.
PHP provides us with the great Reflection Api that is capable of showing us, [what arguments a constructor of any
given class requires](https://www.php.net/manual/de/reflectionclass.getconstructor.php). We could implement that
functionality ourselves, or just try to use a library that takes care of that for us.
2022-05-20 07:28:47 +00:00
2022-05-23 06:43:33 +00:00
You can query the composer database to find all [libraries that implement the container interface](https://packagist.org/providers/psr/container-implementation).
2022-03-29 18:35:06 +00:00
I choose the [PHP-DI](https://packagist.org/packages/php-di/php-di) container, as it is easy to configure and provides some very [powerfull features](https://php-di.org/#autowiring)
2022-05-20 23:19:14 +00:00
out of the box, and also solves the auto wiring problem.
2022-03-29 18:35:06 +00:00
2022-05-20 23:19:14 +00:00
Let's rewrite our `container.php` file to use the PHP-DI container and only define the Services the Container cannot
automatically build.
2022-03-29 18:35:06 +00:00
```php
<?php declare(strict_types=1);
$builder = new \DI\ContainerBuilder;
2022-03-29 18:35:06 +00:00
$builder->addDefinitions([
\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(),
2022-03-29 18:35:06 +00:00
]);
return $builder->build();
```
As the PHP-DI container that is return by the `$builder->build()` method implements the same container interface as our
2022-05-20 23:19:14 +00:00
previously used ad-hoc container we won't need to update the Bootstrap file and everything still works.
2022-03-29 18:35:06 +00:00
[<< previous](08-inversion-of-control.md) | [next >>](10-invoker.md)