no-framework-tutorial/10-invoker.md

103 lines
4.9 KiB
Markdown
Raw Normal View History

2022-03-29 18:35:06 +00:00
[<< previous](09-dependency-injector.md) | [next >>](11-templating.md)
### Invoker
Currently all our Actions need to implement the RequestHandlerInterface, which forces us to accept the Request as the
one and only argument to our handle function, but most of the time we only need a few attributes in our Action a long
with some services and not the whole Requestobject with all its various properties.
If we take our Hello action for example we only need a response object, the time service and the 'name' information from
the request-uri. And as that class only provides one simple method we could easily make that invokable as we alreay named
the class hello and it would be redundant to also call the the method hello. So an updated version of that class could
look like this:
```php
final class Hello
{
public function __invoke(
ResponseInterface $response,
Now $now,
string $name = 'Stranger',
): ResponseInterface
{
$body = $this->response->getBody();
$nowString = $now->get()->format('H:i:s');
$body->write('Hello ' . $name . '!');
$body->write(' The Time is ' . $nowString);
return $response
->withBody($body)
->withStatus(200);
}
}
```
It would also be neat if we could define a classname plus a method as target handler in our routes, or even a short
closure function if we want to redirect all requests from '/' to '/hello' because we have not defined a handler for the
rootpath of our application yet.
```php
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/other-route', [Other::class, 'someFunctionName']);
$r->addRoute('GET', '/', fn (Response $r) => $r->withStatus(302)->withHeader('Location', '/hello'));
```
In order to support this crazy route definitions we would need to write a lot of for actually calling the result of the
route dispatcher. If the result is a name of an invokable class we would use the container to create an instance of that
class for us and then use the [reflection api](https://www.php.net/manual/en/book.reflection.php) to figure out what
arguments the __invoke function has, try to fetch all arguments from the container and then add some more from the router
if they are needed and available. The same if we have an array of a class name with a function to call, and for a simple
callable we would need to manually use reflection as well to resolve all the arguments.
But we are quite lucky as the PHP-DI container provides us with a [great 'call' method](https://php-di.org/doc/container.html#call)
which handles all of that for us.
After you added the described changes to your routes file you can modify the Dispatcher::FOUND case of you $routeInfo
switch section in the Bootstrap.php file to use the container->call() method:
```php
$handler = $routeInfo[1];
$args = $routeInfo[2];
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$args['request'] = $request;
$response = $container->call($handler, $args);
```
Try to open [localhost:1234/](http://localhost:1234/) in your browser and check if you are getting redirected to '/hello'.
But by now you should know that I do not like to depend on specific implementations and the call method is not defined in
the psr/container interface. Therefore we would not be able to use that if we are ever switching to the symfony container
or any other implementation.
Fortunately for us (or me) the PHP-CI container ships that function as its own class that is independent of the specific
container implementation so we could use it with any container that implements the ContainerInterface. And best of all
the class ships with its own [Interface](https://github.com/PHP-DI/Invoker/blob/master/src/InvokerInterface.php) that
we could implement if we ever want to write our own implementation or we could write an adapter that uses a different
class that solves the same problem.
But for now we are using the solution provided by PHP-DI.
So lets request a Service implementing the InvokerInterface from the container and use that inside of the switch-case block
```php
$handler = $routeInfo[1];
$args = $routeInfo[2] ?? [];
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$args['request'] = $request;
$invoker = $container->get(InvokerInterface::class);
assert($invoker instanceof InvokerInterface);
$response = $invoker->call($handler, $args);
assert($response instanceof ResponseInterface);
```
Now we are able to define absolutely everything in routes that is considered a [callable](https://www.php.net/manual/de/language.types.callable.php)
by php, and even some more.
But let us move on to something more fun and add some templating functionality to our application as we are trying to build
a website in the end.
[<< previous](09-dependency-injector.md) | [next >>](11-templating.md)