Let’s say, we have a web app, powered by Laravel and Postgres, that runs across different countries in South East Asia. Each venture will have their own users, products, categories etc. so to make it more manageable we want to have a separate database for each venture. But since we are using Postgres we can leverage schemas which I will describe shortly.
I am using Laravel 5.5 and PostgreSQL 9.5 for my setup. You will find me using the word
Ventureinterchangeably to represent the country.
The first thing we want to do in our multi-tenant web-app is to identify the venture/tenant from whom the request has been made.
Create a file called
config folder and add a mapping for your ventures. The left-hand side is the domain name and the right-hand side is the schema for respective domain.
Now, create a file called
app/Library folder (or you can use any other folder you like; adjust namespace as required). This class will have a list of all the available ventures and helper methods to resolve venture from request URL & to load venture configs.
resolveVenture method in above piece of code resolves venture from the request URL. The first line of the method gets request URL without the protocol (example.com instead of https://example.com). Then we get all valid venture domains we added earlier to compare from. Finally, we return the database schema if it’s available, otherwise, we switch to the
public by default (or any other value that is defined in your
loadVentureConfigs method loads or replaces configs that are venture specific. We will place all venture specific configs inside a folder called
venture_configs at the root level. loadVentureConfigs method will check if the config file is available for given venture and loads it. We are using second parameter
Repository $config because we do not want to override global config repository object. If the second parameter is provided, we will set respective config object instead of the global one. This will help us to get and set venture specific configs later.
Global config repository will always hold config from the current venture as per our design.
Inside these files, we will use dot notation provided by Laravel to override config values. This is how the entries in your venture specific configuration files will look like:
Note: You can also separate configs inside the same file using venture name as a prefix. I prefer to keep it separate.
Next thing we need is the config loader. When Laravel is loading configuration for the first time, we somehow need to tell it to load configuration for the current venture as well. We can do so by overriding
Illuminate\Foundation\Bootstrap\LoadConfiguration class which is responsible to load configuration files.
Create a file called
app/Bootstrap directory and add following code.
Here we are extending
Illuminate\Foundation\Bootstrap\LoadConfiguration class to override
loadConfigurationFiles method. Notice Line #19 that loads venture specific configs by calling
loadVentureConfigs method we added earlier.
$app['venture'] works here? Well, I just added an alias in
bootstrap/app.php to make it more readable.
One last remaining thing is to tell Laravel to use our ConfigLoader class instead of Illuminate\Foundation\Bootstrap\LoadConfiguration. We can do so by using ServiceProvider. No, not really! If you dive deep into Laravel’s code you will notice that configs are loaded prior to the service providers.
So, in order to override LoadConfiguration class, we need to use application bootstrapper (
bootstrap/app.php) which is loaded at the very beginning of application bootstrapping process.
Above statements appears just before
return $app; statement in
Since we can now identify tenants and load configurations for the specific tenant, the only thing remaining is database segregation. There are different ways to segregate data for a multi-tenant application. We can either use different databases for each tenant, or use key-value pairs in every table to represent tenant, or, use different schemas. Every method has their own pros and cons which I will not go through in this tutorial.
Since we are using Postgres, we can leverage Postgres Schemas. Schemas are like folders and can hold tables, views, functions, sequences, and other relations. By leveraging schema, we will limit cost and complexity while still maintaining performance and data security. So, instead of creating multiple databases, we can create one database and different schema for all ventures running the app.
VentureServiceProvider and register it in
register method, we will identify the venture and create database instance as necessary. In the first line (Line #18) we are extending the database object to use custom database object we provide. The second line changes
pgsql schema config to the venture we resolved based on URL. Finally, we create and return a new DatabaseManager object which points to the schema of current venture.
db alias points to the
Illuminate\Database\DatabaseManagerobject. So overriding db will override DatabaseManager as well.
At this point when you use database object with
DB:: façade or any other method provided by Laravel, you will always get the connection for current tenant or venture.
So, how can we access venture specific database schemas? Say, we need to perform some operation in
th schema while we are connected to
vn schema? or we need to run some job using the command line?
VentureServiceProvider, we can create a database object for all schemas and push it into a service container. We can do the same for configs as well.
You might think that in above implementation we should have changed the configuration of venture also while switching the database after Line #9 instead of setting it in a new key. For instance,
Now when we do
app('db.th') it will switch to config of a respective venture as well. But NO, we must not do this as it will change the state of your application without even realizing which can lead to errors. Moreover, we need to reset config for ventures every time we do some database operation on another venture.
The state of your app should be predictable at any given point of execution.
Continuing on our first implementation, we can get global or venture specific db and/or configurations as described in the following snippet.
Note that at this point we can only use
config() helper or
app('config') provided by Laravel to get configs of current venture.
This keeps our app in more predictable state.
I have a database in place with schema for all 6 ventures. I created a table named
search_path with a column
path inside that table for all ventures. The path column will have a value of venture representing the country. For eg,
th schema will have value
Here, we are using Laravel’s default
welcome.blade.php file as a view. The only thing changed is we have added value for the venture that we fetched in our route. And added the timezone config for respective venture.
mt.local.th which is our local domain for Thailand venture. We should get proper venture name from the database and proper timezone from the config.
Now, let’s try with another domain
mt.local.ph which is our local domain for the Philippines. Notice the venture name and timezone.
That’s it! You have successfully created a multi-tenant app using Laravel and PostgreSQL. It may not do a whole lot yet, but it creates a strong foundation for your multi-tenant application. Happy Coding!
In the next part, we will see how we can extend Laravel’s migration implementation to gracefully handle migration for all schemas of our multi-tenant app.
Create your free account to unlock your custom reading experience.