Hello,
I wish to use a route argument in my middleware, and the middleware must run before the route itself as it is authentication middleware.
Middleware:
class api_v1 {
public static function user_auth_middleware($request, $handler) {
$response = $handler->handle($request);
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$usersid = (int) $route->getArgument('usersid');
$result = api_v1::check_token($request, $usersid, $response);
if ($result['error'] == true) {
return $response->withStatus(401);
}
return $response;
}
}
Route:
$slim->group('/api', function ($slim) {
$slim->group('/v1', function ($slim) {
$slim->group('/users', function ($slim) {
$slim->group('/{usersid}', function ($slim) {
$slim->get('', function ($request, $response, $args) {
// Some Code
return $response;
});
})->add(api_v1::class . ':user_auth_middleware');
});
});
});
Attempting to access this route now throws the following error:
Fatal error: Uncaught RuntimeException: Cannot create RouteContext before routing has been completed in C:\xampp\htdocs\composer\vendor\slim\slim\Slim\Routing\RouteContext.php:30
Stack trace:
#0 C:\xampp\htdocs\api.php(174): Slim\Routing\RouteContext::fromRequest(Object(Slim\Psr7\Request))
#1 C:\xampp\htdocs\composer\vendor\slim\slim\Slim\MiddlewareDispatcher.php(180): api_v1::user_auth_middleware(Object(Slim\Psr7\Request), Object(Slim\MiddlewareDispatcher))
#2 C:\xampp\htdocs\composer\vendor\slim\slim\Slim\MiddlewareDispatcher.php(73): class@anonymous->handle(Object(Slim\Psr7\Request))
#3 C:\xampp\htdocs\composer\vendor\slim\slim\Slim\Routing\Route.php(333): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request))
#4 C:\xampp\htdocs\composer\vendor\slim\slim\Slim\Routing\RouteRunner.php(65): Slim\Routing\Route->run(Object(Slim\Psr7\Request))
#5 C:\xampp\htdocs\composer\vendor\slim\slim\Slim\MiddlewareDispatcher.php(73): Slim\Routing\RouteRunner->handle(Object(Slim\Psr7\Request))
#6 C:\xampp\htdocs\composer\vendor\sli in C:\xampp\htdocs\composer\vendor\slim\slim\Slim\Routing\RouteContext.php on line 30
To my understanding, this is caused because the route has not yet been initialized/parsed/etc.
I have seen other issues such as #1281, #1505 and #1973, but it seems that the setting suggested has been removed in Slim 4 (http://www.slimframework.com/2019/08/01/slim-4.0.0-release.html):
Routing is now done via the
Slim\Middleware\RoutingMiddleware.By default routing will be performed last in the middleware queue. If you want to access therouteandroutingResultsattributes from$requestyou will need to add this middleware last as it will get executed first within the queue when running the app. The middleware queue is still being executed in Last In First Out (LIFO) order. This replaces thedetermineRouteBeforeAppMiddlewaresetting.
I've been searching for a while and still don't understand how to add my middleware in such a way that it can gain access to route arguments.
Any help would be very appreciated.
Where do you add $app->addRoutingMiddleware(); ?
I don't know what that is, and I can't see it in the documentation. @odan
The RoutingMiddleware is essential. You can find it in the documentation on the first page:
$app = AppFactory::create();
// Add Routing Middleware
$app->addRoutingMiddleware();
// ...
$app->run();
Huh, okay.
I would have anticipated it to have been included within the Middleware documentation sections as it is that specific.
Thankyou
Hello, I have the same issue as above.
And as suggested above i added the addRoutingMiddleware middleware but sadly i still get the same error message.
I find the use of a static method RouteContext::fromRequest goes against modern design principles. I currently have a PSR-15 middleware I'm trying to test though a test unit but of course this static RouteContext::fromRequest is difficult to mock as RouteContext (or Route) is not even an injected object.
@willy0275 have you found a solution for unittesting PSR-15 Middlewares?
@willy0275 have you found a solution for unittesting PSR-15 Middlewares?
Actually I did. With Mockery. But wow, I mean, come on, it's 2021, it's a crime to short circuit a design with such a static dependency in the middle of nowhere.
// mock Slim\Routing\RouteContext
$route = new class() {
public function getRoute()
{
return null;
}
};
$mock = \Mockery::mock('overload:Slim\Routing\RouteContext');
$mock->allows(['fromRequest' => $route]);
@willy0275 why not mock the ServerRequestInterface instead since that's where the route context is sourced from. This is not a design flaw with Slim but because of the PSR-7 architecture. We have to pass in the routing results via $request->withAttribute() which ends up being untyped. RouteContext gives you a typed interface instead of doing `$request->getAttribute('__routing_results__'); which would net you in the same spot anyway.
```php
declare(strict_types=1);
namespace MyApp\Tests;
use PHPUnit\Framework\TestCase as PHPUnit_TestCase;
use Psr\Http\MessageServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\RoutingRouteContext;
class MiddlewareTestCase extends PHPUnit_TestCase
{
public function testProcess(): void
{
$myRoute = new class() {};
$requestProphecy = $this->prophesize(ServerRequestInterface::class);
$requestProphecy
->getAttribute(RouteContext::ROUTE)
->willReturn($myRoute)
->shouldBeCalledOnce();
$handlerProphecy = $this->prophesize(RequestHandlerInterface::class);
$middleware = new MyMiddleware();
$middleware->process($requestProphecy->reveal(), $handlerProphecy->reveal());
}
}
@l0gicgate In my middleware I've got some code that needs to get the route from the RouteContext::fromRequest static method.
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$argument = $route->getArgument('id');
This is based on Slim's documentation. I don't think your solution would work with this?
We tried so hard and got so far. In the end we could solve this with the following solution:
$route = $this->createMock(RouteInterface::class);
$route->method('getArgument')->with('id')->willReturn('1');
$request = (new ServerRequestFactory())->createServerRequest('PUT', '/test');
$request = $request->withAttribute(RouteContext::ROUTE, $route);
$request = $request->withAttribute(RouteContext::ROUTE_PARSER, $this->createMock(RouteParserInterface::class));
$request = $request->withAttribute(RouteContext::ROUTING_RESULTS, $this->createMock(RoutingResults::class));
$response = $myMiddleware->process($request, $this->requestHandlerMock);
The handler is a anonymous class like this:
$this->requestHandlerMock = new class implements RequestHandlerInterface {
public function handle(ServerRequestInterface $request): ResponseInterface
{
$response = (new ResponseFactory())->createResponse(200);
$response->getBody()->write(json_encode($request->getParsedBody(), JSON_THROW_ON_ERROR));
return $response;
}
};
@willy0275 the RouteContext object sources everything via the request's getAttribute() method so mocking the request works.
RouteContext::fromRequest() has to call getAttribute() so you can pass in whatever you want to it when you're writing your tests. I don't know how else to explain this.
@l0gicgate Thanks, that's good to know, I didn't get far enough in the code I guess. You guys have much more elegant solutions than mine, I'll look into refactoring my test(s) based on what you suggest.
Most helpful comment
We tried so hard and got so far. In the end we could solve this with the following solution:
The handler is a anonymous class like this: