It would be useful if we could extend the idea of route groups (which is a many-to-one relationship) to route tags. For example:
// Front page
$app->get('/', function (Request $request, Response $response, $args) {
...
})->tag('page');
// About page
$app->get('/about', function (Request $request, Response $response, $args) {
...
})->tag('page');
// Retrieve flash messages (JSON format)
$app->get('/flash-messages', function (Request $request, Response $response, $args) {
...
})->tag('api', 'messages');
// Log the user out
$app->get('/logout', function (Request $request, Response $response, $args) {
...
})->tag('api', 'auth');
These tags could then be used in middleware for more advanced filtering:
$route = $request->getAttribute('route');
if ($route->hasTag('auth'){
// Do something only for auth-related requests
}
I this is an interesting idea! I tinkered with this last night to prototype it, although I started to struggle to decide when and how I should use a tag feature. In your example, you were using the tag to decide when to apply middleware. I think it would be better to just configure your routes to use the middleware that you need, instead of having a stack that checked route tags. I'm just curious, do you have a real life example that you've encountered that this would help you solve?
How about setArgument and getArgument?
https://github.com/slimphp/Slim/blob/3.x/Slim/Route.php#L216
This would be useful for ACL, but you could solve this by using Route names. I done this by making my route names use a dot notation e.g users users.profile users.update
This way I can set ACL on users and it would get applied to all other instances using users at the beginning of the name. I will try and dig the code out to share so you can see a different approach of doing this.
I also would like to know your exact use case for this @alexweissman
So, my use case is that I have a Slim application that contains routes for both fully-rendered HTML pages, and routes for a JSON API. I would like to apply a certain piece of environment-checking middleware to the fully-rendered pages, but not the JSON API. In each of these types of routes, I also have some routes that need to be access-controlled, and some that should be "public". Again, I'd like to use middleware for this.
As you can see, I'd have to either use route-level middleware, or I'd have to break it up combinatorially into groups (page-auth, page-public, api-auth, api-auth). I guess route-level middleware is the best solution at the moment, but I can imagine situations where I want to convey additional information to the middleware about a certain route. Conversely, this could also be useful in something like CSRF-guard, to whitelist specific routes when global middleware has been applied.
Now that I think about it, we could make this feature even more generic. Instead of route "tags", we could simply have route properties. This would be an associative array of metadata to be associated with a route. This would be distinct from the contents of the request itself.
@JoeBengalen I wouldn't really consider these to be part of the request, or even a modification of the request, so I don't think setArgument and getArgument would be right.
I think this sort of scheme would be useful for adding middleware to groups of routes which you don't wish to actually group using the $app->group('/segment', ... notation. Oftentimes, I don't want to actually group some routes under the same URL segment, but I do for the purposes of applying middleware.
For instance, consider a Slim project which has some routes which produce a page and some which are API endpoints. It would be nice to be able to do something like $app->getRoutesWithTag('endpoint')->add( $isXhrCheck ); or $app->getRoutesWithTag('page')->add( $addHeaderAndFooter );.
The above situation is something I run in to often. I usually group the endpoints under /api or something similar using route groups, but I don't want to do the same thing with pages and produce a useless /page URL segment. I think this is the sort of thing Alex is getting at.
Just some thoughts from the outside! Thanks for all your hard work.
@RyanGittins example makes sense. This is behavior that you can already accomplish, but not easily, and using tags might just be convenient to clean up some code bases and organize it, slightly differently than groups.
@alexweissman brought up the idea that maybe a key-value store would be better than a list of tags. What does everyone think about that?
I whipped this up if you guys want to pull down the branch from my remote and play with it:
https://github.com/slimphp/Slim/pull/1894
I changed the tag method to addTag to be more consistent with the argument API.
That looks pretty good, @danielgsims!
Would it be possible to implement something like I described above, like $app->getRoutesWithTag('endpoint')->add( $isXhrCheck );? I think the most value of tags would be adding middleware to all routes with a given tag.
Syntactic sugar, I know, but I think it would make for extremely readable code.
EDIT: You may want to perform the same is_string() check in the foreach loop in the addTags() function as you do in your addTag() function.
@RyanGittins - addTags just calls addTag so we're good with the is_string check! And sure, I'll toss in that app function for you if you want to tinker with it.
@danielgsims Doy, my bad! Must still be morni... 12:49pm.
I appreciate it, Daniel.
@RyanGittins you get a list of routes with a matching tag from the router. I didn't expose it through the app yet, so you'll have to fish the router from the container first!
@danielgsims - I just ran it through a little test on this end. Flawless!
I really think this will be a feature that'll help Slim projects scale (at least given the way I build them), as we can now apply the same middleware to routes that are _spread across files_ in a DRY way. Thanks!
@alexweissman the arguments are not part of the request. It is a property of the route. Exactly the same location as the proposed tags property.
You probably confused them with the attributes which are indeed at request level.
Looking at the solution @danielgsims proposed, why couldn't this be solved with what @JoeBengalen suggested? After all setArgument and getArgument is doing the same but thing just not as specific as this. There is also a getArguments method.
@JoeBengalen I think App will inject arguments returned from the dispatcher into the route's prepare function.
To expand, the arguments could be over written, which I think would be not ideal.
If you namespace it correctly, there is no way it should be getting over written.
The dispatcher does return route variables that get sent into the setArguments function of the route, right?
@silentworks is right that you could keep args from being over written by choosing argument / route names carefully, but that not may be immediately noticeable to users. While you could accomplish this with args, I think separating tags vs args clarifies intent and actively prevents collision.
I wonder whether or not this is something folks would use enough to warrant a solution that is more purposefully exposed in the API. What do you guys think?
Just to reference;
existing arguments will not be overwritten but url arguments will be appended:
https://github.com/slimphp/Slim/blob/3.x/Slim/Route.php#L275
There is already an example in the tests which used the arguments to set additional data:
https://github.com/slimphp/Slim/blob/3.x/tests/AppTest.php#L1032
Here the setter is used to specify a default value for a route argument:
https://github.com/slimphp/Slim/blob/3.x/example/index.php#L37
@JoeBengalen - You're right, the arguments can be used to set default values. When you dispatch, those values will be overwritten.
$app->get('/hello[/{name}]', function ($request, $response, $args) {
$response->write("Hello, " . $args['name']);
return $response;
})->setArgument('name', 'World!');
If you GET to /hello/Joe, name's value will be overwritten to Joe, right? If you used an argument for the tag and it is over written, that could cause some errors. That's the only concern I have. @alexweissman's idea speaks more to the intent of the usage.
You know upfront which argument name are gonna be set from the route parsing. So if you are not being very silly no problem can happen there.
If you really want a separate collection of stuff use a generic name like attribute (same as request), because it is a generic key value collection which can be used to store all kinds of stuff. No just tags.
Absolutely Joe, that's what @silentworks meant when he suggested using clever namespacing, like if you used an argument called "tags.api" you would probably be ok. Like I suggested, if this is a common feature folks would like, having a dedicated API would be convenient.
I just want to summarize the possible solutions that were suggested:
In all honesty, using arguments for something like middleware assignment seems like a big hack鈥攁t least from the perspective of someone who uses Slim and has done no work on Slim itself. I think @danielgsims nailed it when he said that the intent of usage matters.
I don't think that this needs to be a core feature.
You can do with $app->group like:
$app->group('', function () {
// Front page
$this->get('/', function (Request $request, Response $response, $args) {
...
});
// About page
$this->get('/about', function (Request $request, Response $response, $args) {
...
});
})->add('pageMiddleware');
$app->group('', function () {
// Retrieve flash messages (JSON format)
$this->get('/flash-messages', function (Request $request, Response $response, $args) {
...
});
// Log the user out
$this->get('/logout', function (Request $request, Response $response, $args) {
...
});
})->add('apiMiddleware');
Remember that group pattern can be empty.
If the PR didn't get merge and if you want to implement the tag on your application, you could extend \Slim\Route and implement the tag feature, then extend \Slim\Router and override the map function to use your Route.
@mathmarques using groups works in the simple case, but suppose I want to apply some authentication middleware to /about and /flash-messages. Groups constrain us to a many-to-one relationship. I'd have to choose which middleware is "important enough" to deserve groups, and any other middleware would have to be applied on the per-route level.
As @danielgsims and @RyanGittins mentioned, something like tags or properties would make a clear semantic distinction.
We could take a more broad approach and create a method like findRoutes and return all routes that match a given callback. You could throw standard class properties on the route at that point, as well as anything else you can think of.
Maybe something like this:
$app->get('/hello/{name}', function ($request, $response, $args) {
echo "Hello, " . $args['name'];
})->tags = [ 'mytag' ];
$routes = $app->findRoutes(function($route) {
return in_array('mytag', $route->tags);
});
It's not the best solution for this particular problem, but it's a broad solution that could help solve several problems.
And on my previous note, while it's not very nice to read, you can do that today by just getting the routes first from App.
$allRoutes = $app->getContainer()->get('router')->getRoutes();
$routes = array_filter($allRoutes, function($route) {
return in_array('mytag', $route->tags);
});
array_walk($routes, function($route) {
$route->add($mw);
});
I'm not totally sold on the need, as I don't see how this is different enough from route arguments to warrant the additional complexity in code, documentation and support.
I'm not sure about the semantic difference argument as, essentially, all that we're trying to do is identify a set of routes by some common property of these routes. I think it's as likely that you'd want to do that by a property set in the code as by a property set in the URI pattern.
I had a hard time deciding when and if I'd use this, I guess we'll just need more feedback.
Otherwise, I think it would be useful to filter routes more easily to help people solve these problems on their own. Maybe even $app->getRoutes() would help connect the dots without having to dig into the application's structure and avoid that "train-wreck" code.
Dynamically pulling the routes and performing operations on them is something I've done in the past in projects quite a bit, so I could see it being super useful to add a little bit of sugar on it.
class App {
public function getRoutes()
{
return $this->container->get('router')->getRoutes();
}
}
$routes = array_filter($app->getRoutes(), function($route) {
return $route->getArgument( 'custom.tag' ) !== null;
});
//doSomethingWithRoutes
I think one of the few areas where Slim projects seem to grow less DRY with time is in middleware assignment. When you have hundreds of different routes with different access levels, API endpoints, AJAX endpoints, etc, all split across multiple files, groups don't work as well for common middleware assignment.
As @alexweissman said, groups constrain us to a many-to-one relationship. When you have a route requiring many levels of middleware, grouping them only alleviates us from having to assign the ones they all have in common. Think of a Venn diagram with a dozen circles鈥攊f you group them all, you can only assign common middleware to the group.
Here's an example of the kind of functionality I'm vying for:
$app->getRoutes()->withTag('page')->add( $addHeaderFooterWrap );
$app->getRoutes()->withTag('popup')->add( $addEmailCaptureModal );
$app->getRoutes()->withTag('ads')->add( $addBannerAds );
$app->getRoutes()->withTag('auth')->add( $authenticationCheck );
$app->getRoutes()->withTag('admin')->add( $adminCheck );
$app->getRoutes()->withTag('superadmin')->add( $superAdminCheck );
$app->getRoutes()->withTag('secure')->add( $sslCheck );
$app->getRoutes()->withTag('ajax')->add( $xhrCheck )
->add( $rateLimiter );
$app->getRoutes()->withTag('api')->add( $apiKeyValidCheck )
->add( $apiKeySetCheck )
->add( $rateLimiter )
->add( $applyBlacklist );
I think the above accomplishes in fewer than fifteen consecutive, neatly-packaged lines what would normally be scattered across all routes in each route file. At a glance, you can see what middleware applies to which routes, as long you name your tags sanely. This allows for a readable, concise, encapsulated way to assign middleware鈥攖otally agnostic to how you structure your routes in different files and route groups.
Yes! @RyanGittins sums it up perfectly. Or, in the "properties" variation:
$app->getRoutes()->withProperty('isPage', true)->add( $addHeaderFooterWrap );
$app->getRoutes()->withProperty('isPopup', true)->add( $addEmailCaptureModal );
...
I could even imagine some crazy CMS that someone comes up with in the future, where they allow the user to dynamically enable middleware on certain types of routes.
if ($flagRateLimitAjax)
$app->getRoutes()->withTag('ajax')->add( $rateLimiter );
Maybe something like this would be better done in the middleware itself, but you get the idea.
withX is not acceptable if you ask me. withX implies modification if you compare it to psr7. If you create a method with such functionality if should be getRouteWithTag or getRouteMatchingTag or something like that.
I like using getRoutes and array_filter. If it's warranted, we could use the Collection class to house routes, and then add some of the simple array_* functionality to it.
$app->getRoutes()->filter($matchesTagCallback)->each($addMwCallback);
Is anyone else interested in continuing this discussion? It's been two months. Otherwise I am going to close my open PR associated with this discussion. Thanks!
I am! Though, I think I've pretty much expressed my ideas already.
After thinking about this a bit more, I really think its adding a feature that can be done through other means and don't think this change belong in the core of the framework itself. It might be better if someone contribute a Cookbook recipe to the Documentation website.
Anyone got news on this feature or a way to implement ourselves using custom recipe? It would be really useful for sending per route argument to a common middleware.
As a novice user making my first enterprise web application using SLIM, I really like the approach taken by @danielgsims and @RyanGittins. It seems the most clear for helping others who might follow me to understand the code I wrote. My use case that led me to search for this:
I have a bunch of GET routes for HTML content, with ajax calls embedded.
I have a bunch of GET routes for JSON content, in a route group setting JSON content type via middleware
Now I want to add a bunch of POST routes within the JSON content, and have a middleware just for those to check authentication or do some other stuff... oops, they have the same URL base as all the other JSON routes. Hit the 1-to-many issue. And I would prefer not to muck with my URL scheme which is already established and have to touch other stuff I already wrote. There is probably a better way to do it, a more clean way I should have designed it, but the tags and "get all routes with this tag" approach would seem an elegant way to put all the routes in one file and middleware in another file, and enable people to solve these problems without doing a major refactoring.
Most helpful comment
I'm not totally sold on the need, as I don't see how this is different enough from route arguments to warrant the additional complexity in code, documentation and support.
I'm not sure about the semantic difference argument as, essentially, all that we're trying to do is identify a set of routes by some common property of these routes. I think it's as likely that you'd want to do that by a property set in the code as by a property set in the URI pattern.