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
|
||||
|
||||
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
|
||||
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
|
||||
for a [suitable solution on packagist](https://packagist.org/providers/psr/container-implementation).
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Lubian\NoFramework\Action;
|
||||
|
||||
|
||||
use Lubian\NoFramework\Service\Time\Clock;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
@ -9,7 +10,10 @@ use Psr\Http\Server\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]) {
|
||||
case Dispatcher::FOUND:
|
||||
$className = $routeInfo[1];
|
||||
$handler = $container->get($className);
|
||||
$handler = new $className($response);
|
||||
assert($handler instanceof RequestHandlerInterface);
|
||||
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
|
||||
$request = $request->withAttribute($attributeName, $attributeValue);
|
||||
|
|
Loading…
Reference in a new issue