Laravel: Scoping queries in the URL
One of the projects I worked on had small but important problematic: users should be able to manage multiple subscriptions through my on-premises Subscriptions library.
GitHub - Laragear/Subscriptions: Subscriptions on-premises, without any payment system!
Manage subscriptions on-premises, without any payment systems! Become a Sponsor and get instant access to this package…
This app was built around a simple idea: an User should be able to have multiple Subscriptions, be able to change the one to manage, and scope all queries to the related Subscription.
My library already solves the problem of “multiple subscriptions”, but no the scoping. Like everything in the world of software, you have multiple ways to solve a problem. I just started with the bad one.
Who need sessions anyway?
The first idea was to let the user pick which Subscription, and save that value into the session. Then I would take out the Subscription ID from the session itself, and scope each query dynamically.
It was a total mess because I had to do it everywhere I needed to scope something by the Subscription. In other words, I had to edit all queries.
Take for example this simple “show all products for the subscription” controller.
Going around with the same idea, I thought it would be convenient to create a Global Scope for each request, but since the scopes are static, doing so would break the application when using Laravel Octane.
Trying to tie anything from an ephemeral state (the HTTP Request) into/from a global state (session/cache/database/storage), or even outside controllers, it’s a very bad idea. I had to scrap that approach, and then I figured out that the simpler the solution, the better.
Named parameters is all you need
I needed to know which Subscription was active across all requests. Session was the first thing I thought to solve the problem, but as you saw, it wasn’t the best solution.
Then, I decided on using the Subscription as a named parameter at the root of the Route declaration. This would make the Subscription somewhat transparent to the user.
After the user picked up the subscription to work on in at login time, he would be redirected to the subscription respective URL
All the controllers would receive the instance of the
Subscription Model we bound to the route, and authorize it.
One neat thing about this approach was its simplicity, since instead of dealing with the session, I only had to check the user wasn’t checking a Subscription that wasn’t subscribed to.
Soon enough I figured out that Authorizing each controller to check what the user could see (or manage) would became tedious.
For that cumbersome logic, I remembered the authorization middleware, which will magically execute the Authorization for the named parameter we give. As you can guess, this is done before hitting the controller at all.
That way we can skip authorizing something on multiple controllers, leaning their code. We can also use this middleware inside a group, so we can save declaring it in every route declaration.
The only “let down” from using the middleware are the controllers. If the signature of the Controller doesn’t type-hints the
Subscription as parameter, the binding won’t resolve, and the gate will receive a string instead.
To solve that, we can change the signature of the Gate to find the model if the parameter is just a string.
In any case, the last thing looks like a stretch if you consider you shouldn’t need to authorize something you won’t use, but just beware of the caveats.
Now you know why most web apps decide to make heavy use of the URL: is straight forward, globally available for the app, and is just easier to manage than anything else.
While you’re it, you may check my on-premises Subscription package if you need to deal with Plans, Subscriptions and what not, and you don’t have a month to implement it.