Route specific configuration in Slim
Note: This article was updated on 14 October 2019 to cover Slim 4 in additional to Slim 3.
A friend emailed me recently asking about route specific configuration in Slim. He wants to be able to set properties when creating the route that he can pick up when the route is matched. The way to do this is using route arguments. I’ve written about route arguments before in the context of setting default values for a route parameter, but you can also use them to set new data for use later.
In this post, I’m going to look at how to use them for configuration data, such as setting required ACL permissions for routes.
Setting per-route data
To recap, we set an argument on a route using setArgument() and like this:
$app->get('/widgets/{id}', HelloAction::class)
->setArgument('permission', 'canReadWidgets');
We have added a new argument, permission, to the route’s arguments and given it a value of canReadWidgets. As the name “permission” is not in the path arguments, it cannot be overridden.
Access the per-route configuration data
We want to access the value of the permission in middleware, so let’s use group middleware:
$app->group('', function ($app) {
$app->get('/widgets/{id}', HelloAction::class)
->setArgument('permission', 'canReadWidgets');
// other routes that need an ACL check here
})->setMiddleware(AclCheckMiddleware::class);
We can now use getArgument() to retrieve the permission and see if the user is allowed to continue.
$route = $request->getAttribute('route');
$permission = $route->getArgument('permission', 'canDoNothing');
That is, we retrieve the Route object from the Request and then grab the argument. The second parameter to getArgument() is the default value to return if there is no argument set on the route.
To see it in context, the bare-bones AclCheckMiddleware code might look something like this:
class AclCheckMiddleware
{
public function __invoke($request, $response, $next)
{
$user = $request->getAttribute('user'); // set in an earlier middleware
// retrieve the configured permission value
$route = $request->getAttribute('route');
$permission = $route->getArgument('permission', 'canDoNothing');
if ($user->isAllowed($permission) === false) {
return new Response(StatusCode::HTTP_FORBIDDEN);
}
return $next($request, $response);
}
}
Note that for group or per-route middleware this works out-of-the-box. If the middleware is added using $app->add(), then you’ll need to ensure that routing happens before the middleware is called so that the route attribute exists when the middleware is run. In Slim 3 set the configuration setting determineRouteBeforeAppMiddleware to true. In Slim 4, add $app->addRoutingMiddleware(); to your middleware stack before your middleware.
That’s it
There you have it. The same mechanism used for setting a default route argument can also be used for per-route settings. This is incredibility useful for situations like ACL permissions checks and I’m confident that there are many more equally useful things that can be done.
Not quite related to this article, but I think there is a mistake in the slim 3 documentation.
It says like this
"`
$app = new \Slim\App($c);
$app->get('/hello/{name}', function ($request, $response, $name) {
return $response->write($name);
});
"`
But from what I see, $name is actually an array with key "name" and the value whatever was passed from request.
Should that be changed to $args as in other examples?
https://www.slimframework.com/docs/v3/objects/router.html#route-strategies
Catalin, Yes. We'd love a PR to the Slim-Website repo :)
Hi Rob,
in last code example it should be like below with creating class property user:
$this->user = $request->getAttribute('user'); // set in an earlier middleware
Or not to use class property but local variable here:
if ($user->isAllowed($permission) === false) {
Or maybe I totally miss some automatic DI of the user object to AclCheckMiddleware.
BTW this good article solved my task to make a simple login / registration form and 2 authenticated routes. Using some libraries fro that will be overkill.
Best, Marek
Marek,
Typo. $this->user should have been $user. I've updated.
Rob.
Hi Rob.
Thank you for your articles :)
I'm strugling with authorization middleware in Slim4. Here's my code:
$app = AppFactory::create();
$app->add(new Authentication());
$app->group('/providers', function(RouteCollectorProxy $group){
$group->get('/', 'Project\Controller\ProviderController:get');
})->add(new SuperuserAuthorization());
SuperuserAuthorization looks like this:
class SuperuserAuthorization
{
public function __invoke(Request $request, RequestHandler $handler): Response
{
$response = $handler->handle($request);
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
return $response->withStatus(403);//Forbidden
}
return $response;
}
}
The thing is that even though the user is not a superuser, the application continues executing and fetches all the providers, returning them with status 403. Shouldn't middleware stop the request from getting into the app and just return 403 right away?
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware, am I right or did I just misunderstand something here…