Laravel Multitenancy: Route Model Binding
Why write code like this
public function edit($articleId)
{
$article = Article::findOrFail($articleId);
return view('articles.edit', compact('article'));
}
When you can simply do
public function edit(Article $article)
{
return view('articles.edit', compact('article'));
}
I'm a huge fan of this Laravel feature since I discovered it. You just need to typehint the model in the function
declaration and the framework will handle the findOrFail
for you. That's it.
But it has a problem though, sometimes you need to check for other fields.
Using our previous example:
public function edit($articleId)
{
$article = Article::where('tenant_id', auth()->user()->tenant_id)->findOrFail($articleId);
return view('articles.edit', compact('article'));
}
Would look cleaner than
public function edit(Article $article)
{
abort_unless($article->tenant_id === auth()->user()->tenant_id);
return view('articles.edit', compact('article'));
}
This is a common scenario. You have an app to edit articles, and you have to prevent your users to edit other users's articles. Basic security rule. But you don't like that kind of code you need to read twice to understand what it does at a first glance.
Well, you could use Global Scopes for sure, but they have a problem: a global scope will be applied to all the queries on
that model, no matter where in the app they occur. So when you need to query all the articles for some statistics or maintenance,
you will need to add withoutGlobalScope
to that query every single time.
The solution my team and I found was to override the default behaviour of Route Model Binding. Of course Laravel offers you a simple solution, you just have to override a method in your model to have it up and running.
This is the default behaviour:
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value)
{
return $this->where($this->getRouteKeyName(), $value)->first();
}
And this is what we will need to declare in our Article
model:
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value)
{
return $this->where($this->getRouteKeyName(), $value)
->where('tenant_id', auth()->user()->tenant_id)
->first();
}
I know what you're about to say
But David, it's easy to understand what the code does if we check this in the controller
Yeah sure, I'll not do this for every kind of filtering like reedeming a PromoCode
with an expiration date. But when you
have a multitenancy app this kind of delegation is appreciated.
I'm with the philosophy of:
A Controller should receive only Entities which it can work without checking conditionals.
I apply this rule also for Request
validation using Form Requests.
You'll find times in which this rule cannot be applied due to some sort of complicated and arcane checks, of course, but
that's what programming is about
You may also like: Laravel Multinenacy: Jobs
Found a typo?
Edit this post.
Special thanks to Dieter for
the amazing code of this blog.