explain implementation of ad-hoc depencency container

This commit is contained in:
lubiana 2022-05-21 00:53:33 +02:00 committed by Andre Lubian
parent 2c3901e9f9
commit 5dc8ad38dd
8 changed files with 297 additions and 250 deletions

View file

@ -119,6 +119,10 @@ the containerinterface.
In this class we will configure all services required for our application and make them accessible via the get($id) In this class we will configure all services required for our application and make them accessible via the get($id)
method. 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`: `config/container.php`:
```php ```php
<?php declare(strict_types=1); <?php declare(strict_types=1);
@ -199,214 +203,41 @@ If you now open the `/hello` route in your browser everything should work again!
#### Using Autowiring #### Using Autowiring
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.
A dependency injector resolves the dependencies of your class and makes sure that the correct objects are injected when You can query the composer database to find all [libraries that implment the container interface](https://packagist.org/providers/psr/container-implementation).
the class is instantiated.
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).
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) 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)
out of the box. out of the box, and also solves the autowiring problem.
After installing the container through composer create a new file with the name `dependencies.php` in your config folder: Lets rewrite our `container.php` file to use the PHP-DI container and only define the Services the Container cannot
automatically build.
```php ```php
<?php declare(strict_types = 1); <?php declare(strict_types=1);
$builder = new \DI\ContainerBuilder;
$builder = new \DI\ContainerBuilder();
$builder->addDefinitions([ $builder->addDefinitions([
\Psr\Http\Message\ResponseInterface::class => \DI\create(\Laminas\Diactoros\Response::class),
\Psr\Http\Message\ServerRequestInterface::class => fn () => \Laminas\Diactoros\ServerRequestFactory::fromGlobals(), \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(),
]); ]);
return $builder->build(); return $builder->build();
``` ```
In this file we create a containerbuilder, add some definitions to it and return the container. As the PHP-DI container that is return by the `$builder->build()` method implements the same container interface as our
As the container supports autowiring we only need to define services where we want to use a specific implementation of previously used ad-hoc container we won't need to update the our Bootstrap file and everything still works.
an interface.
In the example i used two different ways of defining the service. The first is by using the 'create' method of PHP-DI to
tell the container that it should create a Diactoros\Response object when ever I query a ResponseInterface, in the second
exampler I choose to write a small factory closure that wraps the Laminas Requestfactory.
Make sure to read the documentation on definition types on the [PHP-DI website](https://php-di.org/doc/php-definitions.html#definition-types),
as we will use that extensively.
Of course your `Bootstrap.php` will also need to be changed. Before you were setting up `$request` and `$response` with `new` calls. Switch that to the dependency container. We do not need to get the response here, as the container will create and use it internally
to create our Handler-Object
```php
$container = require __DIR__ . '/../config/dependencies.php';
assert($container instanceof \Psr\Container\ContainerInterface);
$request = $container->get(\Psr\Http\Message\ServerRequestInterface::class);
assert($request instanceof \Psr\Http\Message\ServerRequestInterface);
```
The other part that has to be changed is the dispatching of the route. Before you had the following code:
```php
$className = $routeInfo[1];
$handler = new $className($response);
assert($handler instanceof \Psr\Http\Server\RequestHandlerInterface)
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
```
Change that to the following:
```php
/** @var RequestHandlerInterface $handler */
$className = $routeInfo[1];
$handler = $container->get($className);
assert($handler instanceof RequestHandlerInterface);
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
```
Make sure to use the container fetch the response object in the catch blocks as well:
```php
} catch (MethodNotAllowed) {
$response = $container->get(ResponseInterface::class);
assert($response instanceof ResponseInterface);
$response = $response->withStatus(405);
$response->getBody()->write('Not Allowed');
} catch (NotFound) {
$response = $container->get(ResponseInterface::class);
assert($response instanceof ResponseInterface);
$response = $response->withStatus(404);
$response->getBody()->write('Not Found');
}
```
Now all your controller constructor dependencies will be automatically resolved with PHP-DI.
We can now use that to inject all kinds of services. Often we need to work with the Current time to do some comparisons
in an application. Of course we are writing S.O.L.I.D. and testable code so that we would never be so crazy as to call
`$time = new \DateTimeImmutable();` in our Action directly, because then we would need to change the system time of we
want to work with a different date in a test.
Therefore we are creating a new Namespace called 'Service\Time' where we introduce a Now-Interface and an Implementation
that creates us a DateTimeImmutable object with the current date and time.
src/Service/Time/Now.php:
```php
namespace Lubian\NoFramework\Service\Time;
interface Now
{
public function __invoke(): \DateTimeImmutable;
}
```
src/Service/Time/SystemClockNow.php:
```php
namespace Lubian\NoFramework\Service\Time;
final class SystemClockNow implements Now
{
public function __invoke(): \DateTimeImmutable
{
return new \DateTimeImmutable;
}
}
```
If we want to use that Service in our HelloAction we just need to add it as another argument for the Constructor and
update the handle-method to use the new class property:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Service\Time\SystemClockNow;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Hello implements RequestHandlerInterface
{
public function __construct(
private ResponseInterface $response,
private SystemClockNow $now,
)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getAttribute('name', 'Stranger');
$nowAsString = ($this->now)()->format('H:i:s');
$body = $this->response->getBody();
$body->write('Hello ' . $name . '!');
$body->write(' The Time is ' . $nowAsString);
return $this->response
->withBody($body)
->withStatus(200);
}
}
```
If you open the route in your browser you should see that the current time gets displayed. This happens because PHP-DI
automatically figures out what classes are requested in the constructor and tries to create the objects needed.
But we do not want to depend on the SystemClockNow implementation in our class because that would violate our sacred
S.O.L.I.D. principles therefore we need to change the Typehint to the Now interface:
```php
public function __construct(
private ResponseInterface $response,
private Now $now,
)
```
When we are now accessing the Handler in the Browser we get an Error because we have not defined which implementation
should be use to satisfy dependencies on the Now interface. So lets add that definition to our dependencies file:
```php
\Lubian\NoFramework\Service\Time\Now::class => fn () => new \Lubian\NoFramework\Service\Time\SystemClockNow(),
```
we could also use the PHP-DI create method to delegate the object creation to the container implementation:
```php
\Lubian\NoFramework\Service\Time\Now::class => DI\create(\Lubian\NoFramework\Service\Time\SystemClockNow::class),
```
this way the container can try to resolve any dependencies that the class might have internally, but prefer the other
method because we are not depending on this specific dependency injection implementation.
Either way the container should now be able to correctly resolve the dependency on the Now interfacen when you are
requesting the Hello action.
If you run phpstan now, you will get some errors, because the get method on the ContainerInterface returns 'mixed'. As
we will adress these issues later, lets tell phpstan that we know about the issue and we can ignore it for now. This way
we wont get any warnings for this particular issue, but for any other issues we add to our code.
Update the phpstan.neon file to include a "baseline" file:
```
includes:
- phpstan-baseline.neon
parameters:
level: 9
paths:
- src
```
if we run phpstan with './vendor/bin/phpstan analyse --generate-baseline' it will add all current errors to that file and
ignore them in the future. You can also add that command to your composer.json for easier access. I have called it just
'baseline'
[<< previous](08-inversion-of-control.md) | [next >>](10-invoker.md) [<< previous](08-inversion-of-control.md) | [next >>](10-invoker.md)

View file

@ -17,7 +17,8 @@
"laminas/laminas-diactoros": "^2.11", "laminas/laminas-diactoros": "^2.11",
"nikic/fast-route": "^1.3", "nikic/fast-route": "^1.3",
"psr/http-server-handler": "^1.0", "psr/http-server-handler": "^1.0",
"psr/container": "^2.0" "psr/container": "^1.0",
"php-di/php-di": "^6.4"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.6", "phpstan/phpstan": "^1.6",

255
app/composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "1ccaabdd7944ba2f12098b7b2f1c91c2", "content-hash": "0b6833b8fa6869bd212824769648e667",
"packages": [ "packages": [
{ {
"name": "filp/whoops", "name": "filp/whoops",
@ -176,6 +176,65 @@
], ],
"time": "2022-05-17T10:57:52+00:00" "time": "2022-05-17T10:57:52+00:00"
}, },
{
"name": "laravel/serializable-closure",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "09f0e9fb61829f628205b7c94906c28740ff9540"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/09f0e9fb61829f628205b7c94906c28740ff9540",
"reference": "09f0e9fb61829f628205b7c94906c28740ff9540",
"shasum": ""
},
"require": {
"php": "^7.3|^8.0"
},
"require-dev": {
"pestphp/pest": "^1.18",
"phpstan/phpstan": "^0.12.98",
"symfony/var-dumper": "^5.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\SerializableClosure\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Nuno Maduro",
"email": "nuno@laravel.com"
}
],
"description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.",
"keywords": [
"closure",
"laravel",
"serializable"
],
"support": {
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2022-05-16T17:09:47+00:00"
},
{ {
"name": "nikic/fast-route", "name": "nikic/fast-route",
"version": "v1.3.0", "version": "v1.3.0",
@ -227,28 +286,196 @@
"time": "2018-02-13T20:26:39+00:00" "time": "2018-02-13T20:26:39+00:00"
}, },
{ {
"name": "psr/container", "name": "php-di/invoker",
"version": "2.0.2", "version": "2.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-fig/container.git", "url": "https://github.com/PHP-DI/Invoker.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/cd6d9f267d1a3474bdddf1be1da079f01b942786",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786",
"shasum": ""
},
"require": {
"php": ">=7.3",
"psr/container": "^1.0|^2.0"
},
"require-dev": {
"athletic/athletic": "~0.1.8",
"mnapoli/hard-mode": "~0.3.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Invoker\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Generic and extensible callable invoker",
"homepage": "https://github.com/PHP-DI/Invoker",
"keywords": [
"callable",
"dependency",
"dependency-injection",
"injection",
"invoke",
"invoker"
],
"support": {
"issues": "https://github.com/PHP-DI/Invoker/issues",
"source": "https://github.com/PHP-DI/Invoker/tree/2.3.3"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
}
],
"time": "2021-12-13T09:22:56+00:00"
},
{
"name": "php-di/php-di",
"version": "6.4.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-DI/PHP-DI.git",
"reference": "ae0f1b3b03d8b29dff81747063cbfd6276246cc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/ae0f1b3b03d8b29dff81747063cbfd6276246cc4",
"reference": "ae0f1b3b03d8b29dff81747063cbfd6276246cc4",
"shasum": ""
},
"require": {
"laravel/serializable-closure": "^1.0",
"php": ">=7.4.0",
"php-di/invoker": "^2.0",
"php-di/phpdoc-reader": "^2.0.1",
"psr/container": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"require-dev": {
"doctrine/annotations": "~1.10",
"friendsofphp/php-cs-fixer": "^2.4",
"mnapoli/phpunit-easymock": "^1.2",
"ocramius/proxy-manager": "^2.11.2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"doctrine/annotations": "Install it if you want to use annotations (version ~1.2)",
"ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"DI\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "The dependency injection container for humans",
"homepage": "https://php-di.org/",
"keywords": [
"PSR-11",
"container",
"container-interop",
"dependency injection",
"di",
"ioc",
"psr11"
],
"support": {
"issues": "https://github.com/PHP-DI/PHP-DI/issues",
"source": "https://github.com/PHP-DI/PHP-DI/tree/6.4.0"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/php-di/php-di",
"type": "tidelift"
}
],
"time": "2022-04-09T16:46:38+00:00"
},
{
"name": "php-di/phpdoc-reader",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/PHP-DI/PhpDocReader.git",
"reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/66daff34cbd2627740ffec9469ffbac9f8c8185c",
"reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"require-dev": {
"mnapoli/hard-mode": "~0.3.0",
"phpunit/phpunit": "^8.5|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpDocReader\\": "src/PhpDocReader"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)",
"keywords": [
"phpdoc",
"reflection"
],
"support": {
"issues": "https://github.com/PHP-DI/PhpDocReader/issues",
"source": "https://github.com/PHP-DI/PhpDocReader/tree/2.2.1"
},
"time": "2020-10-12T12:39:22+00:00"
},
{
"name": "psr/container",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.4.0" "php": ">=7.4.0"
}, },
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Psr\\Container\\": "src/" "Psr\\Container\\": "src/"
@ -275,9 +502,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/php-fig/container/issues", "issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2" "source": "https://github.com/php-fig/container/tree/1.1.2"
}, },
"time": "2021-11-05T16:47:00+00:00" "time": "2021-11-05T16:50:12+00:00"
}, },
{ {
"name": "psr/http-factory", "name": "psr/http-factory",

View file

@ -1,37 +1,23 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
return new class () implements \Psr\Container\ContainerInterface { use DI\ContainerBuilder;
use FastRoute\Dispatcher;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Lubian\NoFramework\Service\Time\Clock;
use Lubian\NoFramework\Service\Time\SystemClock;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
private readonly array $services; use function FastRoute\simpleDispatcher;
public function __construct() $builder = new ContainerBuilder;
{
$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) $builder->addDefinitions([
{ ServerRequestInterface::class => fn () => ServerRequestFactory::fromGlobals(),
if (! $this->has($id)) { ResponseInterface::class => fn () => new Response,
throw new class () extends \Exception implements \Psr\Container\NotFoundExceptionInterface { Dispatcher::class => fn () => simpleDispatcher(require __DIR__ . '/routes.php'),
}; Clock::class => fn () => new SystemClock,
} ]);
return $this->services[$id]();
}
public function has(string $id): bool return $builder->build();
{
return array_key_exists($id, $this->services);
}
};

View file

@ -2,7 +2,6 @@
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;
@ -13,8 +12,7 @@ final class Hello implements RequestHandlerInterface
public function __construct( public function __construct(
private readonly ResponseInterface $response, private readonly ResponseInterface $response,
private readonly Clock $clock private readonly Clock $clock
) ) {
{
} }
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
@ -22,7 +20,8 @@ final class Hello implements RequestHandlerInterface
$name = $request->getAttribute('name', 'Stranger'); $name = $request->getAttribute('name', 'Stranger');
$body = $this->response->getBody(); $body = $this->response->getBody();
$time = $this->clock->now()->format('H:i:s'); $time = $this->clock->now()
->format('H:i:s');
$body->write('Hello ' . $name . '!<br />'); $body->write('Hello ' . $name . '!<br />');
$body->write('The Time is: ' . $time); $body->write('The Time is: ' . $time);

View file

@ -59,7 +59,7 @@ try {
switch ($routeInfo[0]) { switch ($routeInfo[0]) {
case Dispatcher::FOUND: case Dispatcher::FOUND:
$className = $routeInfo[1]; $className = $routeInfo[1];
$handler = new $className($response); $handler = $container->get($className);
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);

View file

@ -2,7 +2,9 @@
namespace Lubian\NoFramework\Service\Time; namespace Lubian\NoFramework\Service\Time;
use DateTimeImmutable;
interface Clock interface Clock
{ {
public function now(): \DateTimeImmutable; public function now(): DateTimeImmutable;
} }

View file

@ -2,11 +2,12 @@
namespace Lubian\NoFramework\Service\Time; namespace Lubian\NoFramework\Service\Time;
use DateTimeImmutable;
final class SystemClock implements Clock final class SystemClock implements Clock
{ {
public function now(): \DateTimeImmutable public function now(): DateTimeImmutable
{ {
return new \DateTimeImmutable(); return new DateTimeImmutable;
} }
} }