From 5dc8ad38dd79bef795d2ad06fc6d3a55307095e1 Mon Sep 17 00:00:00 2001 From: lubiana Date: Sat, 21 May 2022 00:53:33 +0200 Subject: [PATCH] explain implementation of ad-hoc depencency container --- 09-dependency-injector.md | 217 +++-------------------- app/composer.json | 3 +- app/composer.lock | 255 +++++++++++++++++++++++++-- app/config/container.php | 48 ++--- app/src/Action/Hello.php | 7 +- app/src/Bootstrap.php | 2 +- app/src/Service/Time/Clock.php | 6 +- app/src/Service/Time/SystemClock.php | 9 +- 8 files changed, 297 insertions(+), 250 deletions(-) diff --git a/09-dependency-injector.md b/09-dependency-injector.md index 8b0b5eb..060a09f 100644 --- a/09-dependency-injector.md +++ b/09-dependency-injector.md @@ -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) 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 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\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(); ``` -In this file we create a containerbuilder, add some definitions to it and return the container. -As the container supports autowiring we only need to define services where we want to use a specific implementation of -an interface. +As the PHP-DI container that is return by the `$builder->build()` method implements the same container interface as our +previously used ad-hoc container we won't need to update the our Bootstrap file and everything still works. -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 -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) \ No newline at end of file diff --git a/app/composer.json b/app/composer.json index 980b03b..335b122 100644 --- a/app/composer.json +++ b/app/composer.json @@ -17,7 +17,8 @@ "laminas/laminas-diactoros": "^2.11", "nikic/fast-route": "^1.3", "psr/http-server-handler": "^1.0", - "psr/container": "^2.0" + "psr/container": "^1.0", + "php-di/php-di": "^6.4" }, "require-dev": { "phpstan/phpstan": "^1.6", diff --git a/app/composer.lock b/app/composer.lock index 18e9e6b..386eb40 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1ccaabdd7944ba2f12098b7b2f1c91c2", + "content-hash": "0b6833b8fa6869bd212824769648e667", "packages": [ { "name": "filp/whoops", @@ -176,6 +176,65 @@ ], "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", "version": "v1.3.0", @@ -227,28 +286,196 @@ "time": "2018-02-13T20:26:39+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "php-di/invoker", + "version": "2.3.3", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/cd6d9f267d1a3474bdddf1be1da079f01b942786", + "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": "" }, "require": { "php": ">=7.4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -275,9 +502,9 @@ ], "support": { "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", diff --git a/app/config/container.php b/app/config/container.php index 55766cb..ceb20dc 100644 --- a/app/config/container.php +++ b/app/config/container.php @@ -1,37 +1,23 @@ 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) - ), - ]; - } +$builder = new ContainerBuilder; - public function get(string $id) - { - if (! $this->has($id)) { - throw new class () extends \Exception implements \Psr\Container\NotFoundExceptionInterface { - }; - } - return $this->services[$id](); - } +$builder->addDefinitions([ + ServerRequestInterface::class => fn () => ServerRequestFactory::fromGlobals(), + ResponseInterface::class => fn () => new Response, + Dispatcher::class => fn () => simpleDispatcher(require __DIR__ . '/routes.php'), + Clock::class => fn () => new SystemClock, +]); - public function has(string $id): bool - { - return array_key_exists($id, $this->services); - } -}; +return $builder->build(); diff --git a/app/src/Action/Hello.php b/app/src/Action/Hello.php index d107411..ec2e00c 100644 --- a/app/src/Action/Hello.php +++ b/app/src/Action/Hello.php @@ -2,7 +2,6 @@ namespace Lubian\NoFramework\Action; - use Lubian\NoFramework\Service\Time\Clock; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -13,8 +12,7 @@ final class Hello implements RequestHandlerInterface public function __construct( private readonly ResponseInterface $response, private readonly Clock $clock - ) - { + ) { } public function handle(ServerRequestInterface $request): ResponseInterface @@ -22,7 +20,8 @@ final class Hello implements RequestHandlerInterface $name = $request->getAttribute('name', 'Stranger'); $body = $this->response->getBody(); - $time = $this->clock->now()->format('H:i:s'); + $time = $this->clock->now() + ->format('H:i:s'); $body->write('Hello ' . $name . '!
'); $body->write('The Time is: ' . $time); diff --git a/app/src/Bootstrap.php b/app/src/Bootstrap.php index 4fe4a67..023c8c0 100644 --- a/app/src/Bootstrap.php +++ b/app/src/Bootstrap.php @@ -59,7 +59,7 @@ try { switch ($routeInfo[0]) { case Dispatcher::FOUND: $className = $routeInfo[1]; - $handler = new $className($response); + $handler = $container->get($className); assert($handler instanceof RequestHandlerInterface); foreach ($routeInfo[2] as $attributeName => $attributeValue) { $request = $request->withAttribute($attributeName, $attributeValue); diff --git a/app/src/Service/Time/Clock.php b/app/src/Service/Time/Clock.php index 50897dc..204ccb4 100644 --- a/app/src/Service/Time/Clock.php +++ b/app/src/Service/Time/Clock.php @@ -2,7 +2,9 @@ namespace Lubian\NoFramework\Service\Time; +use DateTimeImmutable; + interface Clock { - public function now(): \DateTimeImmutable; -} \ No newline at end of file + public function now(): DateTimeImmutable; +} diff --git a/app/src/Service/Time/SystemClock.php b/app/src/Service/Time/SystemClock.php index f0d0d0a..705d81f 100644 --- a/app/src/Service/Time/SystemClock.php +++ b/app/src/Service/Time/SystemClock.php @@ -2,11 +2,12 @@ namespace Lubian\NoFramework\Service\Time; +use DateTimeImmutable; + final class SystemClock implements Clock { - public function now(): \DateTimeImmutable + public function now(): DateTimeImmutable { - return new \DateTimeImmutable(); + return new DateTimeImmutable; } - -} \ No newline at end of file +}