Custom request objects in Slim v4

I’m wondering if it’s possible to invoke a route handler with a custom request object rather than ServerRequestInterface. For example, say I had something like this:

interface RequestObjectInterface {
    public static function fromRequest(RequestInterface $request);
}

class LoginRequest implements RequestObjectInterface {
    private $username;

    private $password;

    public function getUsername(): string {
        return $this->username;
    }

    public function getPassword(): string {
        return $this->password;
    }

    public static function fromRequest(ServerRequestInterface $request): self {
        // mapping + validation to new instance
    }
}

class LoginHandler {
    private $authenticationService;

    public function __construct(AuthenticationServiceInterface $authenticationService) {
        $this->authenticationService = $authenticationService;
    }

    public function login(LoginRequest $request, ResponseInterface $response): ResponseInterface {
        $token = $this->authenticationService->login(
            $request->getUsername(),
            $request->getPassword()
        );

        return $response->withHeader(..., $token);
    }
}

Ideally the route resolution strategy would look at the handler function, see that LoginRequest implements RequestObjectInterface and pass an instance of that to the handler function.

If I wanted to implement something like this where/how would I do it?

I had been looking at an implementation based on this integration, but wondering if there’s a better approach: https://github.com/PHP-DI/Slim-Bridge

have a look on Invocation Strategies http://www.slimframework.com/docs/v4/objects/routing.html#route-strategies

basically the default implementation from Slim is: “pass Request and Response into handler”
and you can define your own invocation strategy which might have some nice logic for passing your own parameters into methods like custom request or other custom objects based on method type etc.

I do have implementation of something similar what you are asking for but for Slim v3 and I will upgrade later this year :confused:

You can.

As jDolba said you can try to make your custom invocation strategy, which can also give you the opportunity to add or change the way you call your handler, with more or less function parameters.

But you can also approach that differently:

  1. Use a decorator pattern around the PSR-7 ServerRequest like Slim does with the Http Decorators: https://github.com/slimphp/Slim-Http/blob/master/src/ServerRequest.php.
    And then pass it on with $app->run($yourDecoratedServerRequest);

  2. Same as above, but instead of a decorator you could extend the PSR-7 Request Class and add only your specific code. And then pass it on with $app->run($yourExtendedServerRequest);

  3. Or, if you just want to access login attributes from Request, create a Middleware to store them via Request Attributes, see here: http://www.slimframework.com/docs/v4/objects/request.html#attributes.

For the use case you showed here, I would go for the middleware with request attributes. You don’t need an object for that.

:slight_smile:

Just a note: I think the term Request is a little bit confusing here, because the class LoginRequest is more some kind of a parameter object or DTO for a service. It looks like a domain specific object and not like a HTTP specific object. The domain should not use / require any HTTP request/response objects. Instead of passing the $request, you could pass the data as array from the request object. Then your DTO mapping method would be free from any infrastructure object.

I think it’s more like a contract class around the parameters the endpoint is required to receive. You’re right though about this mostly being just sugar - ultimately this just standardizes the mapping so you don’t have to do something like:

public function login(ServerRequestInterface $request, ResponseInterface $response) {
    $loginDto = LoginDto::createFromRequest($request);
}

Thanks @jDolba. Any chance you can make this a composer library?

I will try but as I said, I will have some time for this after chrismas-isg

…also note that there is quite a lot of strictness around that approach

  • basically it force you to map request values to object properties (it is done automatically)
  • it enables very strict validation and basically expected request body (post values) are defined as object properties and types and names has to match

for example, really simplified example, POST request with var “a”
has to be defined as SomeClass {public $a;}

  • there are also type checks etc

what I like about this approach it gives you really nice testing options and nice support for in IDEs

I think there zend-hydrator can convert an array to DTO / Structs with public properties.

Example:

final class LoginData
{
    public $username;
    public $password;
}

$hydrator = new \Zend\Hydrator\ObjectPropertyHydrator();
$hydrator->setNamingStrategy(new \Zend\Hydrator\NamingStrategy\UnderscoreNamingStrategy());

$requestData = (array)$request->getParsedBody();

$logindata = new LoginData();
$hydrator->hydrate($requestData, $loginData);

print_r($foo); // LoginData( [userame] => 'root', [password] => 'secret')

The problem is that this auto-mapping works only for very simple (flat) structures and not for complex (nested) data structures. This kind of mapping also doesn’t ensure the correct data type.
Implementing a custom mapping + validation would be more robust and flexible. This type of mapper provides the best performance but also required more time to implement. Example:

Passing the data to the constructor and build it there, or…

$loginData = new LoginData((array)$request->getParsedBody());

… or better using a factory / builder method:

final class LoginDataFactory
{
    public static function fromArray(array $data): LoginData
    {
        // mapping + validation

        $loginData = new LoginData();

        $loginData->username = (string)$data['username'];
        $loginData->password = (string)$data['password'];

        return $loginData
    }
}

// Usage in your action method

$loginData = LoginDataFactory::fromArray((array)$request->getParsedBody());
1 Like