From 2c3901e9f9562d7803a7272a93bd088c510beb37 Mon Sep 17 00:00:00 2001 From: lubiana Date: Sat, 21 May 2022 00:21:15 +0200 Subject: [PATCH] explain implementation of ad-hoc depencency container --- 09-dependency-injector.md | 193 +++++++++++++++++++++++++++++++++++++- app/src/Action/Hello.php | 6 +- app/src/Bootstrap.php | 2 +- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/09-dependency-injector.md b/09-dependency-injector.md index 67d79e3..8b0b5eb 100644 --- a/09-dependency-injector.md +++ b/09-dependency-injector.md @@ -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 +getAttribute('name', 'Stranger'); + $body = $this->response->getBody(); + + $time = $this->clock->now()->format('H:i:s'); + + $body->write('Hello ' . $name . '!
'); + $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 +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). diff --git a/app/src/Action/Hello.php b/app/src/Action/Hello.php index 6b4d24a..d107411 100644 --- a/app/src/Action/Hello.php +++ b/app/src/Action/Hello.php @@ -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 + ) { } diff --git a/app/src/Bootstrap.php b/app/src/Bootstrap.php index 023c8c0..4fe4a67 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 = $container->get($className); + $handler = new $className($response); assert($handler instanceof RequestHandlerInterface); foreach ($routeInfo[2] as $attributeName => $attributeValue) { $request = $request->withAttribute($attributeName, $attributeValue);