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.

Got any hints?
No, I'm afraid not.
However if you search within yourself, then...
Cut it out, ok?
I asked for a hint not the crazy philosophies of a Father Christmas look-a-like.
Heed my words, bold adventurer, and your path will lead to wisdom and..
There's just no reasoning with him...
I'm totally stuck as what to do.
I really wish I could help you but I'm just a clueless old fool.
Never a truer word was spoken.
Still clueless?
I'm afraid so.
Still look like a Father Christmas?
I'm afraid so.
Good.
Calypso from Fleur de Lys Simon The Sorcerer