slimphp / Slim

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
http://slimframework.com
MIT License
11.94k stars 1.95k forks source link

RequestResponseNamedArgs and Typed Parameters #3198

Closed BusterNeece closed 1 month ago

BusterNeece commented 2 years ago

I was in the process of migrating my web application to use the Slim RequestResponseNamedArgs controller invoker in favor of the previous PHP-DI/Invoker-based one I was using, and I had expected that the transition would be a fairly straightforward one, but I'm running into one big issue: if you type a parameter in a controller action, for example as an int, it can never coerce into that value:

<?
$app->get('/test', function($request, $response, int $id) {
    // This will fail with RequestResponseNamedArgs
});

The reason this fails is because, at least per my own research, there's no way to specify that a route parameter should be cast as an integer, even if it only accepts numeric values per its FastRoute config. Because declare(strict_types=1) is set on the RequestResponseNamedArgs class, it won't coerce the always-string values for parameters to fit the typed argument in the action.

I'm not sure what the elegant solution to this is. You could remove the strict typing from that single class, since any type strictness originates from the calling file and this would allow type coercion...or somehow let FastRoute know that it should be parsing some parameters as integers.

I'm not even sure that it's something you'd want to fix on the framework side at all, instead choosing to say that every passed parameter will be a string because, well, URLs are strings. That's certainly what my immediate resolution will be, just to re-type all of those action arguments as strings and parse them down the line.

odan commented 2 years ago

~I think that would go beyond the scope of Slim.~

By default, Slim controllers have a strict signature: $request, $response, array $args. The PHP-DI bridge offers an alternative, for strictly typed parameters such as the route placeholders.

But generally, this is also quite easy to write it like this (without RequestResponseNamedArgs):

$app->get('/test/{id}', function($request, $response, array $args) {
    $id = (int)$args['id'];
    // ...
});
BusterNeece commented 2 years ago

@odan The RequestResponseNamedArgs controller invocation strategy is provided by, and part of, the Slim repo. While it isn't the default, it's certainly within the scope of Slim.

odan commented 2 years ago

That's right, obviously this feature was added in version 4.1. https://github.com/slimphp/Slim/pull/3132 I was not aware of that, and the reference is not yet included in the documentation.

I guess this specific feature (typed parameters) is not supported at the moment, because the ... operator in combination with declare(strict_types=1); requires the correct type. https://3v4l.org/SFgmi

~I guess the easiest solution would be to remove declare(strict_types=1); in your controller files and let PHP do the type casting.~ The more complex solution would be to add some kind of reflection based type resolver and caster to the RequestResponseNamedArgs class.

BusterNeece commented 2 years ago

@odan Like I mentioned in the original issue, the type strictness of a call is determined by the calling file, not the called file, so removing strictness on my controller classes wouldn't affect anything.

odan commented 1 month ago

This will be fixed in Slim 5. In Slim 4 you try to add the following strategy:

Installation:

composer require php-di/invoker

Add this custom strategy class:

File: src/Strategies/RequestResponseTypedArgs.php

<?php

declare(strict_types=1);

namespace App\Strategies;

use Invoker\InvokerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\InvocationStrategyInterface;

final class RequestResponseTypedArgs implements InvocationStrategyInterface
{
    private InvokerInterface $invoker;

    public function __construct(InvokerInterface $invoker)
    {
        $this->invoker = $invoker;
    }

    public function __invoke(
        callable $callable,
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $routeArguments
    ): ResponseInterface {
        $routeArguments['request'] ??= $request;
        $routeArguments['response'] ??= $response;

        return $this->invoker->call($callable, $routeArguments);
    }
}

Set as default invocation strategy:

use App\Strategies\RequestResponseTypedArgs;
use Invoker\Invoker;
// ...

$container = $app->getContainer();
$strategy = new RequestResponseTypedArgs(new Invoker(null, $container));

$routeCollector = $app->getRouteCollector();
$routeCollector->setDefaultInvocationStrategy($strategy);

// ...