Convert your Laravel application to SaaS
Tags: laravel, php, programming
Published: 2021-04-19 02:54:19

You had made an application with Laravel for a customer. Thanks for the rapid development speed provided by Laravel, you finished the job quickly and your customer was quite satisfied, and this customer even recommended the application to others who have the same problem. Although the application was designed for a single customer at the beginning, that’s not a problem: you just need to deploy a new one when a new customer comes.

You were lucky enough that more and more customers came, but you also found it’s harder and harder to maintain many deployments. Then you wanted to resolve this happiness trouble, you searched “Laravel multi-tenancy” or “Laravel SaaS” via Google and found some packages that may help, but unfortunately, they might require your table have a column like “tenant_id” …
If you are in the same situation as I was once, I hope this article can give you some idea on how to convert your application to a multi-tenancy one without touching your database.
The Application
Several years ago, a friend of mine asked me to develop an application to simplify his work at the hospital. This application would be used for managing education activities, should have an admin UI for administrators to manage data, and a mobile web UI for students’ usage.
The architecture of the application was rather straight forward:
The frontend was in AngularJs 1.x;
The backend was in Laravel 4.2;
A single MySQL 5.5 database.
Redis, Queues, … all were unnecessary. The development was finished in a couple of days. The application worked well for the hospital, word of mouth helped us gained more hospitals over time. Although many features have been added over the years, the architecture remains the same. It’s simple.

The unsexy application architecture
Convert It to SaaS
With more deployments done, we started to learn how others do their SaaS applications. Microsoft has a good article on this topic here (I can’t find the original article we read, but this one has similar content), we identified that we were actually in the “Standalone single-tenant” model, and we wanted to migrate to “Database-per-tenant” model. “Sharded-multi-tenant” requires column like “tenant_id” to be added in tables, that could involve too much modifications and we don’t want this.
Our target was to deploy a single copy of application per server. When serving requests, the application could load proper database and application configuration dynamically for the requested hospital. We found there are Laravel packages trying to resolve multi-tenancy issue, but after some try we decided to do it without 3-rd party packages to make the solution simple and clear.
The following sections describe how we did it.
Detect Tenant Information At Runtime
First we need to detect the tenant information to correctly load tenant configuration. There are two points to check:
HTTP request handling;
Console command execution (e.g., artisan commands, queue workers, scheduled jobs).
For HTTP request handling, we can detect the tenant by keywords in the URL. In our case every customer has its own domain, so we use domain to make this detection.
For console commands, we can provide commands an extra option (--tenant) to specify the tenant manually.

Detect tenant from web domain or command option
Adding “--tenant” Option to All Artisan Commands
Some Laravel multi-tenancy packages provide commands to migrate database. For example, some package has the following command:
php artisan tenants:migrate --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
This is ok but the syntax is not that elegant to us. What if we can have these:
php artisan --tenant=A migrate
php artisan --tenant=B queue:work
php artisan --tenant=C schedule
php artisan --tenant=D <any customized command>
This syntax is much easier to remember as all Laravel developers are familiar with it :).
“--tenant” option can be added by overriding getArtisan method in App\Console\Kernel class. This is inspired by digging into the implementation of “--env” option. Now all artisan commands have “--tenant” option, the detectConsole method of TenantDetector class above demonstrates how to read it.

Add — tenant option to all Laravel commands
Load Application Configuration Dynamically
Now tenant can be detected, the next step is to load the tenant’s configuration. Laravel’s Config facade could be used for changing configurations at runtime, like below:
$tenant = (new TenantDetector)->detect(app());
$tenantConnection = $this->getTenantConnection($tenant);
Config::set('database.connection', $tenantConnection);
However, this means you have to add such snippet to all HTTP request or command handling logic. Use HTTP middleware or service provider can simplify this work, but that’s far from ideal:
We need to be careful on the order of HTTP middlewares or service providers, the configuration loading logic should be ran before any other codes that depend on the configuration;
We can’t add this to Laravel builtin commands, such as php artisan migrate.
Laravel uses Illuminate\Foundation\Bootstrap\LoadConfiguration class to load configuration on each time it bootstraps. This class is added into the bootstraper arrays of HTTPKernel and ConsoleKernel. Fortunately, bootstraper arrays of both kernels are customizable, our best choice is to add a LoadTenantConfiguration class, and add it into the bootstrap arrays right after LoadConfiguration class.
HttpKernel:

Bootstrap Laravel Http Kernel With Tenant Configuration Loader
ConsoleKernel:

Bootstrap Laravel Console Kernel With Tenant Configuration Loader
Implement LoadTenantConfiguration bootstraper class:

Load configuration dynamically based on detected tenant
In our case the database and password are configured in a fixed pattern, so this method is simple here. You can customize loadConfiguration as you wish, e.g., load the configuration from a central database or load any other tenant specific configurations.
The bootstrap code above checks if the application is running artisan config:cache command. We won’t want tenant specific configuration be saved in the config cache, as config cache should only contains common configurations for all tenants.
Command Scheduling
Now you can add tenant in schedule command like below:

Laravel schedule cron table
As Laravel start a new process to execute scheduled command, tenant parameter needs to be passed into schedule command manually. So we need to update App\Console\Kernel like below:

Pass tenant to scheduled command
Queue Workers
As queue workers are started by artisan command, we also need to add tenant information in the command to make sure it loads proper configurations: