Simple Laravel Multi-Tenancy with Single Trait

Multi-tenancy is pretty common in web-projects - when you want to give records access only for users who created those records. In other words, everyone manages their own data and don't see anyone else's data. This article will show you how to implement it in the most simple way, in single database.

Notice: there are way more complicated and flexible implementations of multi-tenancy, including various packages, I recommend you to read the slides from MultiTenantLaravel.com, but in this article I go for simplicity - the quickest way possible to achieve multi-tenancy.


What We're Creating Here

Imagine a web-project to manage Books, and every user may see only their own entered books. But also every book has origin country field from Country model, and that model needs to be accessible to everyone, without multi-tenancy.

This way we will mimic more complicated examples of real life, where part of models are multi-tenanted, and others are public.


Step 1. Logging User Who Created The Record

For all the models that we want to be multi-tenanted, we need extra field in their DB table.

php artisan make:migration add_created_by_user_id_to_books_table

And then migration code:

Schema::table('books', function (Blueprint $table) {
    $table->unsignedInteger('created_by_user_id');
    $table->foreign('created_by_user_id')->references('id')->on('users');
});

And then app/Book.php addition - see last line:

protected $fillable = [
    'title',
    'country_id',
    'created_at',
    'updated_at',
    'created_by_user_id',
];

Now, how do we fill in that field automatically? We could do that with Model Observer, but for this case let's create a Trait, which we will re-use for both saving record and filtering it.

We're creating a new folder app/Traits and file app/Traits/Multitenantable.php:

namespace App\Traits;

trait Multitenantable {

    protected static function bootMultitenantable()
    {
        if (auth()->check()) {
            static::creating(function ($model) {
                $model->created_by_user_id = auth()->id();
            });
        }
    }

}

Notice the method name bootMultitenantable() - name convention bootXYZ() means Laravel will automatically launch this method when trait is used. You can call it a trait "constructor".

So, what we're doing here? If the user is logged in, we add their ID to the field created_by_user_id, in whatever Model that is. Notice, nowhere here we mention Book or any other model name. So, we can add this trait to however many models we want.

But let's start with Books. It's easy to add this Trait - just two new lines of code in app/Book.php:

use App\Traits\Multitenantable;

// ...

class Book extends Model
{
    use Multitenantable;
    // ...
}

And that's it, field created_by_user_id will be filled in automatically when you call Book::create().


Step 2. Filtering Data By User

Another step is filtering data whenever someone access Book list, or tries to get the Book by its ID field.

We need to add Global Scope on all the queries on that model. And it's also possible to do flexibly in the same app/Traits/Multitenantable.php. Here's the full code:

namespace App\Traits;

use Illuminate\Database\Eloquent\Builder;

trait Multitenantable {

    protected static function bootMultitenantable()
    {
        if (auth()->check()) {
            static::creating(function ($model) {
                $model->created_by_user_id = auth()->id();
            });

            static::addGlobalScope('created_by_user_id', function (Builder $builder) {
                $builder->where('created_by_user_id', auth()->id());
            });
        }
    }

}

We've included Eloquent/Builder class here, and then used static::addGlobalScope() to filter any query with current logged in user.

That's it, every user will see their own book.

In addition to that, this filter will work whenever someone gets a book list wherever in the project, for example if in the future book_id would be the field for some other tables like chapters - they will still be able to choose from their own books only.


Step 3. Adding Trait To Other Models

Remember, our task was to leave Countries accessible to all users? Guess what, the solution is that we just don't use the trait in the app/Country.php model.

In general, the rule is simple: for those models that you want to be "multi-tenantable", use the trait. That's it.


Step 4. What About Administrator To See All Entries?

Obviously, some "super user" of the system should still see all books, right? Now, this functionality will depend on your user-role-permission implementation, but generally I will show you the place where you can add this permission.

In the Trait's filter, we should add this:

protected static function bootMultitenantable()
{
    if (auth()->check()) {
        static::creating(function ($model) {
            $model->created_by_user_id = auth()->id();
        });

        // if user is not administrator - role_id 1
        if (auth()->user()->role_id != 1) {
            static::addGlobalScope('created_by_user_id', function (Builder $builder) {
                $builder->where('created_by_user_id', auth()->id());
            });
        }
    }
}

Yes, it's that simple - just add if-statement for those users who are exceptions for the filter.

See, easy, told ya? I've also created a video-version of the same concept and a little different demo-project, view here:

avatar

Wow that trait thing is damn easy, I'm going to use this in my upcoming project

avatar

What if a user has multiple roles, and in one role they are allowed to post articles, while in another role, they can only view/select?

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 68 courses (1183 lessons, total 43 h 18 min)
  • 90 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent New Courses