Introduction
Multi-tenancy is a software architecture where a single instance of an application serves multiple tenants. Each tenant is a separate group of users who share common access with specific privileges to the software instance. Multi-tenancy is essential for Software as a Service (SaaS) applications where different clients (organizations or individuals) use the same application but need data isolation and customization.
This comprehensive guide will walk you through the concepts, setup, and implementation of multi-tenancy in Laravel, covering different strategies, practical examples, and best practices.
1. Introduction to Multi-Tenancy
What is Multi-Tenancy?
Multi-tenancy refers to a software architecture in which a single instance of a software application serves multiple customers (tenants). Each tenant’s data is isolated and remains invisible to other tenants.
Benefits of Multi-Tenancy
- Cost Efficiency: Shared infrastructure reduces costs.
- Maintenance: Easier to maintain and update a single application instance.
- Scalability: Scales better with increasing number of tenants.
- Customization: Allows tenant-specific customizations.
Challenges of Multi-Tenancy
- Data Isolation: Ensuring that tenants’ data is isolated and secure.
- Complexity: Increased complexity in application design and management.
- Performance: Maintaining performance and resource allocation for multiple tenants.
2. Strategies for Multi-Tenancy
Single Database, Shared Schema
All tenants share the same database and schema. Tenant data is differentiated using a tenant identifier (e.g., a tenant_id
column in each table).
Advantages:
- Simple to implement.
- Lower infrastructure costs.
Disadvantages:
- Potential for data leaks if tenant separation is not handled correctly.
- Can become difficult to manage as the number of tenants grows.
Single Database, Separate Schemas
Each tenant has a separate schema within the same database.
Advantages:
- Better data isolation compared to a shared schema.
- Easier to manage tenant-specific customizations.
Disadvantages:
- More complex to implement and maintain.
- May have limitations based on the database system’s schema management capabilities.
Multiple Databases
Each tenant has a separate database.
Advantages:
- Excellent data isolation.
- Easy to scale and maintain tenant-specific customizations.
Disadvantages:
- Higher infrastructure costs.
- More complex to manage and deploy changes across multiple databases.
3. Setting Up the Development Environment
Ensure you have the following installed:
- PHP (>=7.3)
- Composer
- Laravel (>=8.x)
- Database (MySQL, PostgreSQL, etc.)
Create a new Laravel project:
composer create-project --prefer-dist laravel/laravel multi-tenancy
cd multi-tenancy
4. Implementing Single Database, Shared Schema
Configuring Middleware for Tenant Identification
Create a middleware to identify the tenant based on the subdomain or request data:
php artisan make:middleware IdentifyTenant
In app/Http/Middleware/IdentifyTenant.php
:
namespace App\Http\Middleware;
use Closure;
use App\Models\Tenant;
class IdentifyTenant
{
public function handle($request, Closure $next)
{
$host = $request->getHost();
$tenant = Tenant::where('domain', $host)->first();
if (!$tenant) {
abort(404, 'Tenant not found');
}
// Set the tenant globally
app()->instance('tenant', $tenant);
return $next($request);
}
}
Register the middleware in app/Http/Kernel.php
:
protected $middlewareGroups = [
'web' => [
// Other middleware...
\App\Http\Middleware\IdentifyTenant::class,
],
];
Setting Up Tenant Scopes
Create a trait for applying tenant scopes to models:
namespace App\Traits;
trait TenantScoped
{
protected static function bootTenantScoped()
{
static::creating(function ($model) {
$tenant = app('tenant');
$model->tenant_id = $tenant->id;
});
static::addGlobalScope('tenant_id', function ($builder) {
$tenant = app('tenant');
$builder->where('tenant_id', $tenant->id);
});
}
}
Use the trait in your models:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\TenantScoped;
class Post extends Model
{
use TenantScoped;
}
Managing Tenant-Specific Data
Ensure that all your models and queries respect the tenant scope by using the TenantScoped
trait.
5. Implementing Single Database, Separate Schemas
Configuring Dynamic Database Connections
Create a middleware to switch database schemas dynamically:
php artisan make:middleware SwitchTenantDatabase
In app/Http/Middleware/SwitchTenantDatabase.php
:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\DB;
use App\Models\Tenant;
class SwitchTenantDatabase
{
public function handle($request, Closure $next)
{
$host = $request->getHost();
$tenant = Tenant::where('domain', $host)->first();
if (!$tenant) {
abort(404, 'Tenant not found');
}
// Set the database connection to the tenant's schema
DB::purge('tenant');
config(['database.connections.tenant.database' => $tenant->database_name]);
DB::reconnect('tenant');
// Set the tenant globally
app()->instance('tenant', $tenant);
return $next($request);
}
}
Configure the tenant database connection in config/database.php
:
'connections' => [
// Other connections...
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => null,
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
],
Managing Migrations and Seeders
Create a command to run migrations for each tenant schema:
php artisan make:command MigrateTenant
In app/Console/Commands/MigrateTenant.php
:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Models\Tenant;
class MigrateTenant extends Command
{
protected $signature = 'migrate:tenant {tenant?}';
protected $description = 'Run migrations for tenants';
public function handle()
{
$tenants = Tenant::all();
if ($tenantId = $this->argument('tenant')) {
$tenants = $tenants->where('id', $tenantId);
}
foreach ($tenants as $tenant) {
DB::purge('tenant');
config(['database.connections.tenant.database' => $tenant->database_name]);
DB::reconnect('tenant');
$this->call('migrate', ['--database' => 'tenant']);
}
}
}
6. Implementing Multiple Databases
Setting Up Database Connections
Configure multiple database connections in config/database.php
:
'connections' => [
// Default connection...
'tenant1' => [
'driver' => 'mysql',
'host' => env('TENANT1_DB_HOST', '127.0.0.1'),
'port' => env('TENANT1_DB_PORT', '3306'),
'database' => env('TENANT1_DB_DATABASE', 'tenant1'),
'username' => env('TENANT1_DB_USERNAME', 'forge'),
'password' => env('TENANT1_DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
'tenant2' => [
'driver' => 'mysql',
'host' => env('TENANT2_DB_HOST', '127.0.0.1'),
'port' => env('TENANT2_DB_PORT', '3306'),
'database' => env('TENANT2_DB_DATABASE', 'tenant2'),
'username'
=> env('TENANT2_DB_USERNAME', 'forge'),
'password' => env('TENANT2_DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
],
Database Switching Logic
Create a middleware to switch databases based on the tenant:
php artisan make:middleware SwitchTenantDatabase
In app/Http/Middleware/SwitchTenantDatabase.php
:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\DB;
use App\Models\Tenant;
class SwitchTenantDatabase
{
public function handle($request, Closure $next)
{
$host = $request->getHost();
$tenant = Tenant::where('domain', $host)->first();
if (!$tenant) {
abort(404, 'Tenant not found');
}
// Set the database connection to the tenant's database
DB::purge('tenant');
config(['database.connections.tenant.database' => $tenant->database_name]);
DB::reconnect('tenant');
// Set the tenant globally
app()->instance('tenant', $tenant);
return $next($request);
}
}
7. Tenant Registration and Management
Tenant Creation
Create a form and a controller for registering new tenants. In your web.php
:
Route::get('/register-tenant', 'TenantController@create');
Route::post('/register-tenant', 'TenantController@store');
In app/Http/Controllers/TenantController.php
:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Tenant;
class TenantController extends Controller
{
public function create()
{
return view('register-tenant');
}
public function store(Request $request)
{
$tenant = Tenant::create([
'name' => $request->name,
'domain' => $request->domain,
'database_name' => 'tenant_' . strtolower($request->name),
]);
// Run tenant-specific setup (e.g., migrations, seeders)
// ...
return redirect('/')->with('success', 'Tenant registered successfully.');
}
}
Tenant Isolation
Ensure that each tenant’s data is isolated by applying tenant scopes and using the correct database connections.
Middleware for Tenant Context
Ensure tenant context is set up correctly using middleware.
In your web.php
, group routes that require tenant context:
Route::middleware('tenant')->group(function () {
// Tenant-specific routes
});
8. Testing Multi-Tenant Applications
Unit Testing
Write unit tests to ensure that tenant-specific functionality works correctly.
Example test in tests/Feature/TenantTest.php
:
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Tenant;
class TenantTest extends TestCase
{
public function testTenantCreation()
{
$tenant = Tenant::create([
'name' => 'Test Tenant',
'domain' => 'test.localhost',
'database_name' => 'tenant_test',
]);
$this->assertDatabaseHas('tenants', ['name' => 'Test Tenant']);
}
}
Integration Testing
Write integration tests to ensure that tenant context is correctly applied across the application.
Example test in tests/Feature/TenantIntegrationTest.php
:
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Tenant;
use App\Models\User;
class TenantIntegrationTest extends TestCase
{
public function testTenantDataIsolation()
{
$tenant1 = Tenant::create([
'name' => 'Tenant1',
'domain' => 'tenant1.localhost',
'database_name' => 'tenant1',
]);
$tenant2 = Tenant::create([
'name' => 'Tenant2',
'domain' => 'tenant2.localhost',
'database_name' => 'tenant2',
]);
// Set tenant context
app()->instance('tenant', $tenant1);
User::create(['name' => 'User1', 'email' => '[email protected]', 'password' => bcrypt('password')]);
app()->instance('tenant', $tenant2);
User::create(['name' => 'User2', 'email' => '[email protected]', 'password' => bcrypt('password')]);
$this->assertDatabaseHas('users', ['email' => '[email protected]'], 'tenant1');
$this->assertDatabaseHas('users', ['email' => '[email protected]'], 'tenant2');
}
}
9. Advanced Techniques
Customizing Tenant Behavior
Override tenant behavior using model events and custom logic.
In your Tenant
model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
protected static function booted()
{
static::created(function ($tenant) {
// Run tenant-specific setup (e.g., create database, run migrations)
});
}
}
Handling Tenant-Specific Configuration
Store tenant-specific configurations in the database and load them dynamically.
In your IdentifyTenant
middleware:
$tenant = Tenant::where('domain', $host)->first();
config(['app.name' => $tenant->name]);
Performance Optimization
Optimize performance by using caching and indexing for tenant-specific queries.
Example: Cache tenant settings in the IdentifyTenant
middleware:
$tenant = Cache::remember("tenant_{$host}", 60, function () use ($host) {
return Tenant::where('domain', $host)->first();
});
10. Best Practices
Data Security
Ensure that tenant data is isolated and secure. Use encryption and secure database connections.
Automated Tests
Write comprehensive tests to cover tenant-specific functionality and data isolation.
Resource Management
Monitor and manage resources (e.g., databases, schemas) to ensure scalability and performance.
Documentation
Document the multi-tenancy setup and usage instructions for future reference and team members.
Regular Audits
Regularly audit tenant data and application logs to ensure data integrity and security.
11. Conclusion
Multi-tenancy is a powerful architecture for building scalable and cost-efficient applications. Laravel provides the flexibility and tools necessary to implement multi-tenancy effectively. By following this comprehensive guide, you can create robust multi-tenant applications with Laravel, catering to different tenant requirements while ensuring data isolation, security, and performance. Embrace the power of multi-tenancy to take your Laravel applications to the next level.