Multi Tenant App Design & Packages

Published 10 months ago by noddy

I would like to build my first multi tenant app, and am wondering how to go about doing this. The app itself will store sales data for each Organization (capture sales, customer details, reporting tools) etc.

Do I create a single tenant app prototype first, then implement multi-tenancy or start off with multi-tenancy from the beginning?

Also, while looking around at implementation ideas, I came across some packages that claim to facilitate multi-tenancy:

How has your experience been with these packages, and would you recommend any of them?

martinbean

@noddy No one can really answer how to architect your application. You need to decide what it is your application will do and the best approach to that.

nate.a.johnson

I use Landlord and it's really and simple. My advice, if you know your app is going to be multi tenant then build it in at the beginning. It's a lot easier than trying to go back and figure out how to add it later. When I first started, I built out my app and added two tenants so that I could test that my data and logins were complete segregated. My multi tenancy is based off of the url used to enter the application. It uses a single database and separates data with a tenant_id column in the tables where that makes sense (users, companies, etc).

Here's a middleware that ensures every request in my app has multi tenancy applied correctly.

<?php

namespace App\Http\Middleware;

use App\Tenant;

use Closure;
use Exception;
use Landlord;

class TenantSelector
{
    public function handle($request, Closure $next, $guard = null)
    {
        $domain = $request->root();

        $tenant = Tenant::where('domain', $domain)->first();

        if ( ! $tenant) {
            throw new Exception('Unknown Tenant');
        }

        Landlord::addTenant('tenant_id', $tenant->id);

        return $next($request);
    }
}
noddy

Hi @martinbean, thanks for your response. I agree with you - ultimately, I would have to decide on the most appropriate implementation for myself.

I will re-phrase my questions, in case I was not very clear. I am looking for feedback on two fronts, from those that may already have set up a multi tenant app:

  1. I have started building the "single tenant" version of application and am around 40% complete. Should I continue on this path and implement multi-tenancy once my application is complete OR do I need to implement multi tenancy from the start? Which approach have others followed, and would they do things differently if they had to do it again?

  2. If anyone has used the multi-tenancy packages I listed above, how has your experience been with them?

noddy

Hi @nate.a.johnson, thanks for your reply and explaining your implementation. I appreciate the feedback on Landlord. I have looked at the documentation and it does seem to be fairly straight forward to setup.

My advice, if you know your app is going to be multi tenant then build it in at the beginning. It's a lot easier than trying to go back and figure out how to add it later.

This is exactly what I wanted to know. Im around 40% completed with my application, and realized that I may have to re-visit portions of the application if we add multi tenancy later on. It makes sense to bake this in from the beginning.

ricardoarg

I am in the path of finishing my multi-tenant app. I didn't use any package... In the end it's pretty simple, the only thing I used is:

create a TenantScope

class TenantScope implements Scope
{

    public function apply(Builder $builder, Model $model)
    {        
        $id_tenant = 0;
        if (Auth::check()) {
            $id_tenant = Auth::user()->tenant_id;
        }           
        return $builder->where($model->getTable() . '.tenant_id',  '=', $id_tenant);        
    }
}

then, I have a ScopedModel base class, wich have something like this in the boot method:

    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new TenantScope);

    static::creating(function ($model) {
            if (!$model->tenant_id) {
               $model->tenant_id = Auth::user()->tenant_id;         
            }
    });
    }  

and it's bassicaly done, every eloquent query on the ScopedModel will be with a "AND table.tenant_id = XXX", and every created model will have the tenant_id set.

noddy

Hi @ricardoarg, Thanks for your reply. Your solution looks very easy to understand and simple to implement - I will look into doing something similar for my needs as well. Many thanks!

martinbean

I’m building a “multi-tenant” CMS at the minute, and the solution looks similar to yours, @nate.a.johnson and @ricardoarg.

I have a “domain resolver” service class with a resolveFromRequest() method. I call this in my app service provider which will attempt to the resolve the tenant based on the hostname, and then store that tenant model instance in the service container:

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        if (!$this->app->runningInConsole()) {
            $this->app[Resolver::class]->resolveFromRequest($this->app['request']);
        }
    }

    public function register()
    {
        $this->app->singleton(Resolver::class, function ($app) {
            return new Resolver($app);
        });
    }

    public function provides()
    {
        return [
            Resolver::class,
        ];
    }
}

The Resolver class looks something like this:

class Resolver
{
    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    public function resolveFromRequest(Request $request)
    {
        $domain = Domain::with('tenant')->whereName($host)->firstOrFail();

        // If domain found, store tenant in service container
        $this->app->instance(Tenant::class, $domain->tenant);
    }
}

By storing the Tenant in the service container, I can then inject it in my app where I need to, such as controllers and other service classes.

I also have repositories to access scoped data, by accessing it through the Tenant:

class ArticleRepository
{
    // Inject Tenant as it’s in the service container
    public function __construct(Tenant $tenant)
    {
        $this->articles = $tenant->articles();
    }

    public function paginate()
    {
        return $this->articles->paginate();
    }
}
class ArticleController extends Controller
{
    public function __construct(ArticleRepository $articles)
    {
        $this->articles = $articles;
    }

    public function index()
    {
        $articles = $this->articles->paginate();

        return view('article.index', compact('articles'));
    }
}

If I access data via these repositories, then I know I’m not going to get data polluted with other tenants’.

noddy

Hi @martinbean - thank you for taking time to reply. I really appreciate the detailed response and code examples you've provided. Im definitely digging through all the info here and going back to re-do portions of my application.

martinbean

@noddy Glad to have helped :)

Sign In or create a forum account to participate in this discussion.