uxweb's avatar
Level 20

Hitting multiple databases dinamically with Laravel

Hi friends, this year i discovered the powerful Laravel is and made me think how i am doing things as a developer.

This thinking took me to lear a lot of great stuff about PHP, Laravel, Architecture, Testing and more (Thanks to Jeffrey Way! @jeffreyway).

Recently i looked at a project i made for the company i work for and said to myself "Maan, this is really bad".

With this idea in mind i decided to recreate the app using Laravel and started to think in the architecture.

Im new to architecture and patterns, but i want to do this the best possible, so i ask you for some ideas or advices on how to solve this requirement :).

The requirement involves the application should be able to hit multiple databases with the exact same schema, but everyone of the db's stores different information.

Each database can contain some "projects" and the next db can contain other projects. A project is never equal in two different db's.

Each database contains a "users" table and a "projects_users" table too. This tables store which users has access to which projects. The only exception with the information through the databases is the "users" table which can contain the same users for all the db's (for consistency) of course.

This way we know that a user can see certain projects on the db's.

Now, the thing with this is that i don't have a clue on how to show the user all the projects associated with him through the db's (of course, only the ones defined in the database.php config file) then the user should be able to select one.

After this, the application must keep knowing that the selected project belongs to a certain database and for any process the user does, the app will hit the right database.

And for the record, "a logged user can't access some routes of the app if he has not selected a project", so after the users signs in, the app will take him to the route "/projects", where he will have the option to select one project.

I wrote a functional test that goes like this to better understand the feature:

  • as a user of the application
  • I want to select a project to use the application
  • I sign in
  • I see the current url is "/projects"
  • I click "Project A"
  • I see the current url is "/"
  • I see "Project A Selected"

Finally i have an image of what i am trying to explain, hope it helps to understand more this.

http://twitpic.com/ea6pz3

Thanks guys hope you can give me some advice.

0 likes
13 replies
uxweb's avatar
Level 20

Thanks @jamieshiers, i'm reading all the links, unfortunately, i have not found a well crafted and architected solution yet. I am developing a solution to this, but i think it is getting coupled with Laravel. I will post here the GitHub repo of my solution to a Multi Tenant Multi DB App with Laravel, maybe some experienced developers could help to refactor this idea into a package that can be shared and plugged into Laravel through composer.

psmail's avatar

I have looked into this quite a lot and I am the person who started the thread mentioned by @jamieshiers, above.

I do not believe there to be any out-of-the-box solutions out there as yet. So if you are waiting for ready to go solution, you may be waiting for a while. It will happen as more people take up Laravel. I say this because the Rails community - which has been around for longer and has more participants at the moment - is well on top of this.

I can say that you will need to decide what approach you will take: separate database, separate schema (only supported by Postgres), or in-app logic using tenant_ids on each record. I'll go through the pros and cons if you wish, but they are readily available elsewhere.

In terms of who is doing what in the Laravel community, most are going for the in-app approach (like Culttt), some are going for the separate DB approach and a few - like my tortured self - are going for the separate schema approach.

My experience is that the code itself is not that hard and there are plenty of examples on the web to follow. The hard bit is testing and maintenance - well, at least for the separate DB and separate schema approach. But, again, there is nothing off the shelf yet.

1 like
uxweb's avatar
Level 20

As @psmail says, there is a list of possible ways to achieve this:

  • Separate Database
  • Separate Schema (this is also supported by sql server)
  • One Schema based on a tenant_id
  • A mix of separate database and schema based on tenant_id

What in common all these approaches have?. Database and Schema

So if you go for the separate database approach you will need to have a manually or automated way to add a new connection in the database.php conf file each time you need to have a new database.

From a perspective of data security and database administration i think that the separate schema is not really good, it will bloat your database with lots of tables and after some time the database will become huge, and maybe will take a lot of time to backup and restore, and what if it fails?, it will not allow any of your tenants to access his information.

The one schema based on a tenant_id is better because you keep once the same schema, obviously after some time could have the same issues of the separate schema, but i think this is more clean.

In terms of what i'm doing, the best approach is the separate database, it keeps a tenant's information isolated from other tenants, and maybe isolated in its own environment (server, cloud service, ...).

To start, i think that for a separate database solution 2 pieces of data needs to live in the user's session: 'tenant_connection' and 'tenant_id' (in case you are mixing separate db with schema based on tenant_id).

Having this idea, the next question is: Where do i set this data?

For a simple approach, it could be set when the user is logged in, there you will set the name of the connection and his tenant_id or whatever id that makes sense to filter his data.

The data will be available through the duration of his session.

In my approach, a user can access only to some parts of the data on a database, maybe in one db he can access some projects, and in other database to none of them.

This means that the user should choose in which project context he wants to use the app, and route all the operation to that project's database.

This requirement leaves me to show the user a list of all the projects in all the databases he has access, and let him choose his context.

So after login, the immediate page he will see is his projects list.

Of course, i should create a filter to avoid a user to access those parts of the app that requires having a tenant_connection and/or tenant_id selected.

Route::filter('tenant_connection_context', function()
{
    if ( ! Session::get('tenant_connection') )
    {
        return Redirect::guest('projects/');
    }
});

Ok, now that the user is on context of the data he needs, the next question is:

How to tell an Eloquent model to set its connection and context to the one stored on session?

Well, after thinking a lot about this and, sure, i'm not an expert on architecture, my approach was to create a base Model that inherits from Eloquent and make any new model inherit from it to be able to get the connection and context stored in session.

Here is the class that my models should extend:

class TenantModel extends \Eloquent {

    public function __construct(array $attributes = array())
    {
        parent::__construct($attributes);

        // sets the model's connection from the one stored in session when it is created
        $this->setConnection(\Session::get('tenant_connection'));
    }
}

Of course, as i said, i'm not an expert on architecture and i see that this couples a little bit with the Session.

All right, let's keep going.

I know several databases can be used, so my database.php conf file looks like this in the connections key:

'connections' => array(

       'tenant1' => array(
           'driver' => 'mysql',
           'host' => getenv('DB_HOST'),
           'database' => 'tenant1',
           'username' => getenv('DB_USERNAME'),
           'password' => getenv('DB_PASSWORD'),
           'charset' => 'utf8',
           'collation' => 'utf8_unicode_ci',
           'prefix' => '',
           'type' => 'tenant',
  ),

        'tenant2' => array(
            'driver' => 'mysql',
            'host' => getenv('DB_HOST'),
            'database' => 'tenant2',
            'username' => getenv('DB_USERNAME'),
            'password' => getenv('DB_PASSWORD'),
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
            'type' => 'tenant',
        ),

and the list can grow to any databases you need.

A thing to note in here is the 'type' property i have added to only the connections beign used as separate db's for a tenant, this will let me filter the connections to put in context when i need to read through all of them in search for every project the current user has access and show them as explained before.

In this example, the "projects" are what the user can set in context to, for a better understand, i would say:

  • A user can only be in context of one project at a time, later if he decides, he can change his context to another project, the switch of database should be transparent for him if he chooses a project stored in other database.

All right, we need to show the projects list and for this i'm going to use a repository, actually the ProjectRepository, the "getProjectsOfUser" method will return all the projects the user has access in all the databases set in the connections configuration which type is equals to 'tenant':

use Illuminate\Support\Collection;
use MultiTenant\Users\User;

class ProjectRepository {

    public function getAllProjectsOfUser(User $user)
    {
        // Get all the database connections
        $connections = \Config::get('database.connections');

        // Create a collection to store the projects a user has access
        // to put in context
        $projects = new Collection;

        // If the user is now in context of a project, we store it
        // to restore his context at the end
        $currentTenantDb = \Session::get('tenant_connection');

        foreach ($connections as $connectionName => $connection)
        {
            // We iterate on each connection of type 'tenant'
            if (isset($connection['type']) && $connection['type'] == 'tenant')
            {
                // Set the context connection
                \Session::put('tenant_connection', $connectionName);

                // Find the user in this context
                $tenantContextUser = User::find($user->username);

                // If we find the user, it means that in this context
                // has access to at least one project
                if ( $tenantContextUser )
                {
                    // add each of the projects to the collection
                    // that the user has access in this context
                    foreach ($tenantContextUser->projects as $project)
                    {
                        $projects->push( $project);
                    }
                }
            }
        }
        // restore the context the user was in
        \Session::put('tenant_connection', $currentTenantDb);

        // return the list of projects
        return $projects;
    }
} 

After this, we can ask for this in the ProjectsController and pass the data to the View.

I'll keep updating this post ...

3 likes
puzbie's avatar

I am in the process of writing in Laravel a shared back end for a number of sites which share a common data structure but reside on different sites. Consequently, I have to access multiple databases simultatneously. I found the easiest way to do this was as follows:

First, I stored the current site in the session variable "connection". Then I use its value in global.php to set some config settings. So each site gets its own config. The config settings point to valid configs you declare in database.php

In global.php:

define('MY_CONNECTION',Session::get('connection'));

Then, when you have to access a specific connection, you just use:


class Blog extends BaseModel { protected $connection = MY_CONNECTION;

And for when you aren't using models, you use:


$activeConnection = DB::connection(MY_CONNECTION)->whatever;
tjames's avatar

Here is a composer package that is meant as a multi-tenant schema/migration manager but also has multi-database functionality built in as well. I haven't used it but it might be worth checking out, or might even give you a good head start at what you're wanting to do.

https://github.com/orchestral/tenanti

1 like
psmail's avatar

Here is a composer package that is meant as a multi-tenant schema/migration manager but also has multi-database functionality built in as well.

Now you tell me ... @tjames :)

I attempted to roll my own. I found it difficult to run migrations across multiple tenants, using a shared connection (the schema name would change worth each tenant, of course). I did a lot to make sure that the right schema name was being used ... to no avail. Testing also proved tricky.

uxweb's avatar
Level 20

Thanks @tjames, i have checked this package, but it is meant to manage the schema migration only.

uxweb's avatar
Level 20

Hi everybody, today i'm very happy because after reading several information about multi tenant apps, finally i think i'm arriving to a better approach.

Following the approach of Phillip Brown (http://culttt.com/2014/03/31/multi-tenancy-laravel-4/) could make my mind to change the code i've posted before.

And of course, i want to share it with you, but as i said, i'm not an expert in architecture and still don't know how to package this to be shareable and testable.

All right, let's go!:

Requirements:

  • Need for just one instance of an app that can work with many databases
  • The user has access to some information in these databases (maybe all, maybe some of them)
  • The user must be able to change the context (database and/or tenant) while he is in session, no need to logout.
  • While the app is in context of some db and tenant, all information must be filtered to meet with the context.

Well, i think this is the very basic requirements.

Solution:

How to solve this?, the first step to get to a solution is that some way or another we need some place to store the current context the app is in. We can use database, the filesystem, i'm not sure if cache and session.

Anyone having experience with sessions in PHP will say "aha", sessions looks the more easy and maybe reliable way of storing the context, why?, because the session is "bound" to our database/app user.

Laravel has already a service to access the current session, also has a Facade called Session, which we can use like this:

// To store a value in session
Session::put('key_name', value);

// To get a value from session
Session::get('key_name', opt_def_value_if_not);

All right, we can go ahead an use the Session facade everywhere we need it, maybe in repositories, maybe in all the places where we need to filter data for the current context, but, i did it before and all my code got coupled with the framework, not a good thing if i'm planning to package this solution and share it through composer!.

Well, so, how to solve it?. The post from culttt.com helped me to think better and be able to separate this.

First we are going to create an interface, i called it TenantContextInterface. This interface will let us get and set the data related to the current context, no matter how it is implemented (database, files, cache, session, ...):

<?php namespace MultiTenant\Contexts;

interface TenantContextInterface {

    /**
     * Set the connection name for the actual context
     * this tenant
     * @param $name
     * @return mixed
     */
    public function setConnectionName($name);

    /**
     * Get the name of the current connection in context
     * @return mixed
     */
    public function getConnectionName();

    /**
     * Set the id value filter data in the current context
     * @param $id
     * @return mixed
     */
    public function setTenantId($id);

    /**
     * Get the tenant id value for the current context
     * @return mixed
     */
    public function getTenantId();

}

As you can see, the interface has a getConnectionName method, this method will return the name of the database connection in context if any, the same for getTenantId method, it will let you get the value to help you filter the data in the current context, later will get to the point to set this values!.

A note on this: if there is need to store different values for the context we can declare more methods!, this is flexibility!.

Now, we have an interface, we know we can't instantiate it, so, let code to an interface by implementing it. Here i have created a TenantContextSession class that implements the interface using Laravel Session:

<?php namespace MultiTenant\Contexts;

class TenantContextSession implements TenantContextInterface {

    /**
     * Sets the connection name for the actual context
     * this tenant
     * @param $name
     * @return mixed
     */
    public function setConnectionName($name)
    {
        \Session::put('tenant_connection', $name);
    }

    /**
     * Get the name of the current connection in context
     * @return mixed
     */
    public function getConnectionName()
    {
        return \Session::get('tenant_connection');
    }

    /**
     * Sets the id value filter data in the current context
     * @param $id
     * @return mixed
     */
    public function setTenantId($id)
    {
        \Session::put('tenant_id', $id);
    }

    /**
     *
     * @return mixed
     */
    public function getTenantId()
    {
        return \Session::get('tenant_id');
    }
}

The purpose of this class is to get the context through the session. As you can see in the getConnectionName method is implemented with this:

 return \Session::get('tenant_connection');

Here we are using the Laravel Session facade and this is the only place we do this. Now we have decoupled a little bit more from the framework!.

Now we are able to set and get our context data, yay!.

At this point i think you must have authentication ready to let our users to log in.

But wait, if i have many databases, which one i am going to use to log in the user??. Well this question is totally relevant and we can defer on how to implement this:

the best schema i think we can get without mess is:

  • have many databases with the same schema to store the user/tenant data. These databases must have a schema to map a user with the data, i think of a projects_users table on each of these databases, this will let us know if a user has access to data on the database, and of course, show the project to let the user be in the context of it.

  • have one separate database where we just store data about our users, like the username, remember_token, email, password or any data is needed.

If this make sense to you, the database where our users reside will be de default configured on the database connections configuration file, like this:

'default' => 'users',

    'connections' => array(

        'users' => array(
            'driver' => 'mysql',
            'host' => getenv('DB_HOST'),
            'database' => 'users',
            'username' => getenv('DB_USERNAME'),
            'password' => getenv('DB_PASSWORD'),
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
    ),

This way, no matter what, our code will fetch data from this connection, like when we are authenticating the user, in that time will not be a context already.

All right, let's get going.

After a user logs in, we can or not show the data related to set the context, maybe you can redirect him to the homepage or to other place of the app where a context is not needed. I prefer to redirect him to the page where he can set his context. To do this, i have created the next route:

  • a route:
Route::post('tenant', [
    'as' => 'tenants_path',
    'uses' => 'TenantsController@store'
]);

If the name makes no sense to you and want something more meaningful, we can use "/context/set" or another instead, that's easy. Actually, the user is never going need to see this url because after the method executes it will be redirected to any other route we need or the intended.

  • a controller:
class TenantsController extends \BaseController {

    /**
     * Show the form for creating a new resource.
     *
     * @return Response
     */
    public function store()
    {
        TenantContext::setConnectionName(Input::get('tenant_connection'));
        TenantContext::setTenantId(Input::get('tenant_id'));

        return Redirect::intended('/');
    }
}

As you can see the only method used here is the store() one, here is where we are going to "create" the current context for our app. But wait!, what is that facade like TenantContext there?!, ahh, well, we will review that part later but, yes!, we made a facade for the context!. The purpose of this controller's method is to set the data we need for the current context, note we use the setConnectionName and setTenantId methods to store values coming from the page where we show the user his options to set his context. Finally after setting the context, we redirect to the intended route or to the home route, easy!.

Now we have set our application in context, but wait, how are we going to tell our Eloquent models to be in context?, i don't want to mess with all the code of the Illuminate/Database and reinvent the wheel. Eloquent in this right moment makes its work great on a database connection, we only need to tell it to change the model's connection to the one in our context.

How we achieve this?, certainly i'm going to tell you that i have to review the code of Eloquent to find that we can change the model's connection in the constructor.

Knowing this, we can create a TenantEloquent class:

<?php namespace MultiTenant\Core;

class TenantEloquent extends \Eloquent {

    /**
     * @param array $attributes
     */
    function __construct(array $attributes = array())
    {
        parent::__construct($attributes);

        $context = \App::make('MultiTenant\Contexts\TenantContextInterface');

        $this->setConnection($context->getConnectionName());
    }
} 

Here we just call the parent's constructor and then using the IoC we get an instance of our context implementation. I tried the IoC inject the context as a dependency, but for some reason it couldn't do it, it never resolver it to a TenantContextSession instance, even when it was binded, that's why i get the instance like it is. Finally we set the model's connection to the one in the context and when the IoC creates an instance of any model that extends this class, it will be in te context connection scope, so our queries will hit the right database.

Now, how do we set the tenant_id to filter data from the model?, we can define a global scope (http://laravel.com/docs/eloquent#global-scopes) in this base class, i'm not going to use it here because in my database schema there is not a tenant_id on every table so i will have to filter my data in the repository. I just wanted to show that you can do it if you have a schema like that.

Now we can use our Eloquent models in the current context, here is a demo of my Project and Product models:

<?php namespace MultiTenant\Projects;

use MultiTenant\Core\TenantEloquent;

class Project extends TenantEloquent {

    protected $fillable = [];

    public function products()
    {
        return $this->hasMany('MultiTenant\Products\Product');
    }
}
<?php namespace MultiTenant\Products;

use MultiTenant\Core\TenantEloquent;

class Product extends TenantEloquent {

 protected $fillable = [];

    /**
     * Relationship with the project the product belongs to
     * @return mixed
     */
    public function project()
    {
        return $this->belongsTo('MultiTenant\Projects\Project');
    }
}

For this schema i made a "products" table related to the "projects" table to be able to show you how easy and transparent is to work in the context with the models.

Wait, i forgot to show you the code that shows the user his list of projects and lets him choose one to set the context, but before we get there, we are going to cover the Service Provider and Facade for the context.

As you know, we can use dependency injection and resolution that the IoC gives us. Knowing this we can create a ServiceProvider to let the container inject an instance of our context anywhere we need it, specially in our controllers.

<?php namespace MultiTenant\Providers;

use Illuminate\Support\ServiceProvider;
use MultiTenant\Contexts\TenantContextSession;

class TenantContextServiceProvider extends ServiceProvider {

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app['tenant.context'] = $this->app->share(function($app)
        {
            return new TenantContextSession;
        });

        $this->app->bind('MultiTenant\Contexts\TenantContextInterface', function($app) {
            return $app['tenant.context'];
        });
    }
}

Here is our TenantContextServiceProvider that will bind our TenantContextSession implementation of TenantContextInterface in the container, so we can access with these options:

App::make('MultiTenant\Contexts\TenantContextInterface');
$app['tenant.context']
or using it as a dependency on other class like this

public function __construct(TenantContextInteface $context) 

of course, don't forget to add 'MultiTenant\Providers\TenantContextServiceProvider' to the list of providers in the app.php configuration file!.

Well it will be good to have a Facade of this so, let's do it:

If you know nothing about laravel facades, well, it's easy, i didn't know how to create them like 30 minutes ago, but the laravel documentation is amazing so i could learn it at the speed of light (http://laravel.com/docs/facades)

<?php namespace MultiTenant\Facades;

use Illuminate\Support\Facades\Facade;

class TenantContext extends Facade {

    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor() { return 'tenant.context'; }
} 

You just need to have 3 things: - A binded implementation of the class you need to access with the facade - A class that extends the Facade laravel class - Add an alias for the facade in the app.php configuration file

Actually the binding is already done by our service provider, it put the context instance in the 'tenant.context' key of the container.

'aliases' => array(
    'Validator' => 'Illuminate\Support\Facades\Validator',
    'View' => 'Illuminate\Support\Facades\View',

    'TenantContext' => 'MultiTenant\Facades\TenantContext',
 ),

Now we can access our context with:

    $conn = TenantContext::getConnectionName();
// or any method our container has
    TenantContext::setConnectionName($connection);

As you can see, i'm trying to avoid tight coupling within the code, but i'm not sure if this is true, so hope some more experienced developers have better ideas for this.

Now we can access our context anywhere, yeeha! (i'm trying not to be a cowboy @jeffreyway).

Well, now it is time to put the code to show a user a list of all the projects he has access trhough all the connections/databases:

  • here is the route:
Route::get('projects', [
    'before' => 'auth',
    'as' => 'projects_path',
    'uses' => 'ProjectsController@index'
]);
  • Here is the controller:
use MultiTenant\Projects\ProjectRepository;

class ProjectsController extends \BaseController {

    /**
     * @var
     */
    protected $projectsRepository;

    /**
     * @param ProjectRepository $projectsRepository
     */
    function __construct(ProjectRepository $projectsRepository)
    {
        $this->projectsRepository = $projectsRepository;
    }

    /**
    * Display a listing of projects the current user has access
    * through all the contexts (connections/databases).
    *
    * @return Response
    */
    public function index()
    {
        $projects = $this->projectsRepository->getAllProjectsByUser(Auth::user());

        return View::make('projects.index')->withProjects($projects);
    }
}

Here i have injected the ProjectRepository which we'll review now!:

<?php namespace MultiTenant\Projects;

use Illuminate\Support\Collection;
use MultiTenant\Core\BaseRepository;
use MultiTenant\Users\User;

class ProjectRepository extends BaseRepository{

    /**
     * Get all the projects a user has access through all the connections/databases
     *
     * @param User $user
     * @return Collection
     */
    public function getAllForUser(User $user)
    {
        // Get all the database connections
        $connections = \Config::get('database.connections');

        // Create a collection to store the projects a user has access
        // to put in context
        $projects = new Collection;

        // If the user is now in context of a project, we store it
        // to restore his context at the end
        $currentTenantDb = $this->context->getConnectionName();

        foreach ($connections as $connectionName => $connection)
        {
            // We iterate on each connection of type 'tenant'
            if (isset($connection['type']) && $connection['type'] == 'tenant')
            {
                // Set the context connection
                $this->context->setConnectionName($connectionName);

                // Find the user in this context
                $tenantContextUser = User::find($user->username);

                // If we find the user, it means that in this context
                // has access to at least one project or maybe none
                if ( $tenantContextUser )
                {
                    // add each of the projects to the collection
                    // that the user has access in this context
                    foreach ($tenantContextUser->projects as $project)
                    {
                        $projects->push( $project);
                    }
                }
            }
        }
        // restore the context the user was in
        $this->context->setConnectionName($currentTenantDb);

        // return the list of projects
        return $projects;
    }
} 

The repository has a special method called getAllForUser(), this method is in charge of go for each connection in the database.php configuration file and set the context to fetch the projects a user has access, every time it finds projects on a context, push the projects to a collection that finally is returned to be consumed by a view. Here i'm going to try to be more clear, because some questions like this can raise:

will it set the context for every connection stored in the database.php configuration file?, and the answer is: if we have not a way to choose only some of the connections, yes. And how we can choose only the ones that represent these "tenant" databases?, i don't want any other database be put in context because maybe we have a different schema and the queries will die!, yes they will die dramatically.

A solution for this is to "tag" the connections we are interested to be set in context, how?:

'connections' => array(

  'tenant1' => array(
   'driver' => 'mysql',
   'host' => getenv('DB_HOST'),
   'database' => 'tenant1',
   'username' => getenv('DB_USERNAME'),
   'password' => getenv('DB_PASSWORD'),
   'charset' => 'utf8',
   'collation' => 'utf8_unicode_ci',
   'prefix' => '',
            'type' => 'tenant',
  ),

        'tenant2' => array(
            'driver' => 'mysql',
            'host' => getenv('DB_HOST'),
            'database' => 'tenant2',
            'username' => getenv('DB_USERNAME'),
            'password' => getenv('DB_PASSWORD'),
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
            'type' => 'tenant',
        ),

        'tenant3' => array(
            'driver' => 'mysql',
            'host' => getenv('DB_HOST'),
            'database' => 'tenant3',
            'username' => getenv('DB_USERNAME'),
            'password' => getenv('DB_PASSWORD'),
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
            'type' => 'tenant',
        ),

 ),

As you can see, we have added a 'type' => 'tenant' element on each connection that represents a tenant, this way we are able to fetch data only from those. We are doing it in the repository already!, and if you remember, we have not this element in our default connection, our "users" connection.

Now here is a simple view that shows the projects:

@extends ('layouts.default')

@section ('content')
    <h1>Project Selection</h1>

    @foreach ($projects as $project)
        <li>
            {{ Form::open(['route' => 'tenants_path']) }}
                {{ Form::hidden('tenant_connection', $project->getConnectionName()) }}
                {{ Form::hidden('tenant_id', $project->id) }}
                {{ Form::submit('Select ' . $project->name, ['class' => 'btn btn-primary']) }}
            {{ Form::close() }}
        </li>
    @endforeach
@stop

As you can see is a simple form for each project a user has access with a button showing the name for the project, and 2 hidden fields that will be used to set the context data:

  • tenant_connection
  • tenant_id

tenant_connection gets the value from the project's getConnectionName() method (fortunately this is part of Eloquent), and tenant_id gets its value from the project id field. When the button is clicked it will post the data for the project to out tenants route, this will set the context and then will redirect to any intended route or home.

We are getting to the final point of this implementation, now we are going to write a filter to avoid a user gets access to a route that needs the context to be selected, otherwise it will fail dramatically and we don't want that.

/**
 * Checks that a connection is in context
 * to fetch data from the database using Eloquent.
 * A redirect response will be issued to /projects
 * if no connection exists in context to let the user choose a context
 */
Route::filter('tenant_context', function()
{
    if ( ! TenantContext::getConnectionName() )
    {
        return Redirect::guest('projects/');
    }
});

Here we leaverage our facade to get the connection name of the current context, if no connection name is set, then we redirect the user to the page where he must set a context.

Finally i want to create that part of the app that needs a context before it is accessed:

  • The products route
Route::get('products', [
    'before' => 'tenant_context',
    'as' => 'products_path',
    'uses' => 'ProductsController@index'
]);
  • The ProductsController
use MultiTenant\Products\ProductRepository;

class ProductsController extends \BaseController {

    protected $productRepository;

    function __construct(ProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    /**
    * Display a listing of the resource.
    *
    * @return Response
    */
    public function index()
    {
        $products = $this->productRepository->all();

        return View::make('products.index')->withProducts($products);
    }

}
  • The ProductRepository
namespace MultiTenant\Products;

use MultiTenant\Core\BaseRepository;
use MultiTenant\Projects\Project;

class ProductRepository extends BaseRepository {

    /**
    * @param Project $project
    * @return mixed
    */
    public function findForProject(Project $project)
    {
        return $project->products();
    }

    /**
    * Find the products of the current tenant in context
    * @return mixed
    */
    public function all()
    {
        return Project::findOrFail($this->context->getTenantId())->products;
    }
} 
  • The BaseRepository
namespace MultiTenant\Core;

use MultiTenant\Contexts\TenantContextInterface;

abstract class BaseRepository {

    /**
     * @var
     */
    protected $context;

    /**
     * @param $context
     */
    function __construct(TenantContextInterface $context)
    {
        $this->context = $context;
    }

} 
  • The View
@extends ('layouts.default')

@section ('content')
    <h1>Products List</h1>
    <ul>
        @foreach ($products as $product)
            <li>{{ $product->name }}</li>
        @endforeach
    </ul>
@stop

Phew!!, i'm very tired right now, i will finish this later, but for now is almost everything we need.

If someone have ideas about this or want to review all the code, please reply!.

Thanks.

14 likes
ral_007's avatar

@uxweb, i am new with php and laravel, i really need this demo, i tryed this code but not able to configured so can you please provide me demo code for multi tenent with diffrent databse.

genghis-tuan's avatar

This is a old thread, but I wanted to contribute a bit of what I learned from some trial and error at the office today related to this.

I have a single Laravel 5.2 instance deployed that houses multiple applications. For example, on hostname/ is a page listing links to the other applications, which are: hostname/app1 hostname/app2 ...

The routing for the application was setup by modifying the app/Providers/RouteServiceProvider.php reference: http://laravel.io/forum/11-22-2014-multiple-routes-files Example:

Setup multiple routing files: --Create a routes.app1.php file under /app/Http/. (Just copy the existing routes.php file and rename it) --Wrap the routes in a group, for example:

Notice the prefix is the name of the application. this prepends "app1" to the routes. So the first route below is "", but the prefix makes the route "app1", which is the application root for app1. "route-somewhere2" would be routed to "app1/route-somewhere2"...

Route::group(['prefix' => 'app1', 'middleware' => 'web'], function() 
{ 
    //index, somewhere2 and somewhere3 are functions in the controller 
    Route::get("", ['as'=>'app1-somewhere', 'uses' =>'App1Controller@index']); 
    Route::get("route-somewhere2", ['as'=>'app1-somewhere2', 'uses' =>'App1Controller@somewhere2']); 
    Route::get("route-somewhere3", ['as'=>'app1-somewhere3', 'uses' =>'App1Controller@somewhere3']); 
});//end route group

Edited /app/Providers/RouteServiceProvider to read new route files Added:

... 
protected $app1Namespace = 'App\Http\Controllers\App1'; 
protected $App2Namespace = 'App\Http\Controllers\App2'; 
protected $namespace = 'App\Http\Controllers'; //This line already existed, it is for the main application that displays the links to the "sub" applications.      
...
public function map(Router $router) 
{ 
//$this->mapWebRoutes($router); 
/* |-------------------------------------------------------------------------- 
   | App2 Router 
   |-------------------------------------------------------------------------- */ 
   //created routes.app1.php under app/Http/
   $router->group(['namespace' => $this->app1Namespace], function ($router) { require app_path('Http/routes.app1.php');  }); 
   ...

Created controllers for each different application, so 
    php artisan make:controller App1Controller --resource 
    php artisan make:controller App2Controller --resource

Now the Laravel deployment can route to different "sub" applications.

To setup the connection to different databases for each application, edit **app/config/database.php** by adding a connection for each application. For example:     
'mysql_app1' => [ 
    'driver' => 'mysql', 
    'host' => env('DB_HOST', 'localhost'), 
    'port' => env('DB_PORT', '3306'), 
    'database' => 'DB_name', 
    'username' => 'username', 
    'password' => "secretPassword", 
    'charset' => 'utf8', 
    'collation' => 
    'utf8_unicode_ci', 
    'prefix' => '', 
    'strict' => false, 
    'engine' => null, ], 
'mysql_app2' => [ 
    'driver' => 'mysql', 
    'host' => env('DB_HOST', 'localhost'), 
    'port' => env('DB_PORT', '3306'), 
    'database' => 'DB_name', 
    'username' => 'username', 
    'password' => "secretPassword", 
    'charset' => 'utf8', 
    'collation' => 'utf8_unicode_ci', 
    'prefix' => '', 
    'strict' => false, 
    'engine' => null, ],

Now, in the App1Controller, add/edit the constructor to use the connection appropriate to the application, for example:

public function __construct() { \DB::setDefaultConnection('mysql_app1'); }//constructor

Now, in the App2Controller, add/edit the constructor to use the connection appropriate to the application, for example:

public function __construct() { \DB::setDefaultConnection('mysql_app2'); }//constructor

So every controller that uses a database connection must have a constructor with the appropriate setDefaultConnection invocation as shown above.

I tested out both applications by simply returning query results in JSON format then the route was hit. The applications returned data from their correct respective databases and tables.

Hope this is also useful to someone.

1 like

Please or to participate in this conversation.