Slim: Route Arguments Unavailable In Middleware

Created on 8 Sep 2019  路  13Comments  路  Source: slimphp/Slim

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.

The Problem

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

My Research

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 the route and routingResults attributes from $request you 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 the determineRouteBeforeAppMiddleware setting.

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.

Most helpful comment

We tried so hard and got so far. In the end we could solve this with the following solution:

  • creating the request with ServerRequestFactory
  • Mocking Route, RouteParser and RoutingResults like this:
$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;
            }
        };

All 13 comments

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:

  • creating the request with ServerRequestFactory
  • Mocking Route, RouteParser and RoutingResults like this:
$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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lwiwala picture lwiwala  路  5Comments

enygma picture enygma  路  3Comments

codeguy picture codeguy  路  3Comments

aranel616 picture aranel616  路  3Comments

odahcam picture odahcam  路  3Comments