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
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
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.
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);
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);
}
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());