Laravel Saas - Création d'une Application Laravel 12 Multi-Tenant, dynamique thèmes et développement modulaire de A à Z
: Novembre 2025
Partie 1 : Initialisation et Prérequis
Partie 2 : Architecture Multi-Tenant
Partie 3 : Gestion des Tenants
Partie 4 : Modularisation de l'Application
Partie 5 : Thèmes Dynamiques par Tenant
Partie 1 : Initialisation du Projet et Prérequis
Ce cours est accompagné d'un projet de démonstration complet et open-source. Vous pouvez consulter, cloner ou contribuer au code source directement sur GitHub. Voir le projet sur GitHub →
Chapitre 1 : Installation de Laravel 12 et du Layout de base
Nous commençons par la base : la création d'un projet Laravel 12 frais, la configuration de la base de données et la mise en place d'une structure de layout simple avec un composant Blade pour être prêts à construire notre interface.
1.1. Création du projet Laravel
composer create-project laravel/laravel multitenants
1.2. Configuration de l'environnement (.env)
Modifiez votre fichier .env pour connecter le projet à votre base de données MySQL centrale.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_multitenant_central
DB_USERNAME=root
DB_PASSWORD=
1.3. Lancement de la première migration
Exécutez la migration initiale pour créer les tables de base de Laravel (utilisateurs, etc.) dans la base de données centrale.
php artisan migrate
1.4. Création du composant de Layout
Nous créons un composant AppLayout qui servira de structure principale pour les pages de notre application.
php artisan make:component AppLayout
Cela génère App/View/Components/AppLayout.php et resources/views/components/app-layout.blade.php. Vous pouvez ensuite structurer votre layout en y incluant des partiels comme un header et une sidebar (main-sidebar.blade.php).
Chapitre 2 : Intégration de Redis pour le Cache & Sessions
Pour améliorer les performances, nous configurons Redis comme pilote pour le cache, les sessions et les files d'attente.
2.1. Installation du client Predis via Composer
composer require predis/predis
2.2. Mise à jour du fichier .env
Modifiez ces variables pour que Laravel utilise Redis. Attention, si la ligne CACHE_STORE=database est présente, elle doit être supprimée pour éviter les conflits.
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
REDIS_CACHE_DB=1
2.3. Vérification des fichiers de configuration
Assurez-vous que les fichiers config/database.php et config/cache.php sont bien configurés pour utiliser ces variables d'environnement Redis.
Voir la configuration type
Dans config/database.php :
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1),
],
],
Dans config/cache.php :
'stores' => [
// ...
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
// ...
],
2.4. Tester la connexion Redis
Utilisez l'interface en ligne de commande de Redis pour vous assurer que le serveur fonctionne et répond.
redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> exit
Partie 2 : Architecture Multi-Tenant
Chapitre 3 : Mise en place de Stancl/Tenancy
Nous installons le package stancl/tenancy, qui va nous fournir tous les outils pour gérer des bases de données séparées pour chaque client (tenant).
3.1. Installation du package
composer require stancl/tenancy
php artisan tenancy:install
3.2. Création du modèle Tenant
Créez le fichier app/Models/Tenant.php. Ce modèle Eloquent représentera nos clients dans la base de données centrale.
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
protected $fillable = ['id', 'data'];
protected $casts = [
'data' => 'array',
];
}
3.3. Configuration et migration centrale
Ouvrez config/tenancy.php et assurez-vous que la clé tenant_model pointe vers votre nouveau modèle. Ensuite, lancez la migration pour créer la table des tenants.
// In config/tenancy.php
'tenant_model' => \App\Models\Tenant::class,
php artisan migrate
Chapitre 4 : Migrations, Seeders et Routage Tenant
Nous allons maintenant structurer notre application pour gérer les migrations et les routes spécifiques à chaque tenant.
4.1. Migrations des Tenants
Les migrations pour les tables des tenants (ex: produits, factures...) doivent être placées dans database/migrations/tenant. Pour en créer une :
php artisan make:migration create_eleves_table --path=database/migrations/tenant
Pour exécuter ces migrations sur tous les tenants existants ou sur un tenant spécifique :
# Pour tous les tenants
php artisan tenants:migrate
# Pour un seul tenant
php artisan tenants:migrate --tenant=ecole1
# Pour réinitialiser et ré-exécuter les migrations pour tous les tenants
php artisan tenants:migrate-fresh
4.2. Seeders des Tenants
Pour que les commandes de seeding ciblent la bonne classe, configurez config/tenancy.php :
// Dans config/tenancy.php
'seeder_parameters' => [
'--class' => 'TenantDatabaseSeeder', // root seeder class
],
Ensuite, lancez le seeding :
php artisan tenants:seed
4.3. Créer et configurer le TenancyServiceProvider
Ce Service Provider, fourni par le package, est crucial. Assurez-vous qu'il est bien créé (app/Providers/TenancyServiceProvider.php) et enregistré dans bootstrap/providers.php.
<?php
// Dans bootstrap/providers.php
return [
App\Providers\AppServiceProvider::class,
App\Providers\TenancyServiceProvider::class, // Ajoutez cette ligne
];
4.4. Séparation des routes
Le fichier routes/tenant.php contiendra les routes accessibles via un domaine de tenant. Le fichier routes/web.php est pour l'application centrale.
Exemple pour routes/tenant.php:
<?php
// routes/tenant.php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::middleware('guest')->group(function () {
Route::get('/login', [\App\Http\Controllers\AuthController::class, 'loginView'])->name('login');
Route::post('/login', [\App\Http\Controllers\AuthController::class, 'login'])->name('login');
});
Route::middleware('auth')->group(function () {
Route::post('/logout', [\App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
Route::get('/', [\App\Http\Controllers\PagesController::class, 'index'])->name('index');
});
});
Exemple pour routes/web.php:
<?php
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('/loginadmin', [\App\Http\Controllers\AuthController::class, 'loginadmin'])->name('loginadmin');
Route::post('/loginadmin', [\App\Http\Controllers\AuthController::class, 'loginadminpost'])->name('loginadminpost');
});
Route::middleware('auth')->group(function () {
Route::post('/logoutadmin', [\App\Http\Controllers\AuthController::class, 'logoutadmin'])->name('logoutadmin');
Route::get('/indexadmin', [\App\Http\Controllers\PagesController::class, 'tenants'])->name('indexadmin');
// Ajout des routes pour le CRUD des tenants
Route::resource('tenants', \App\Http\Controllers\TenantController::class)->except(['show']);
});
4.5. Créer un premier Tenant pour tester
Utilisons Tinker pour créer manuellement notre premier tenant.
php artisan tinker
# Dans Tinker
use App\Models\Tenant;
$tenant = Tenant::create(['id' => 'ecole1']);
$tenant->domains()->create(['domain' => 'ecole1.manar.com']);
exit;
N'oubliez pas d'ajouter 127.0.0.1 ecole1.manar.com à votre fichier hosts pour pouvoir y accéder localement via `http://ecole1.manar.com:8000`.
Partie 3 : Gestion des Tenants (Application Centrale)
Chapitre 5 : Création du CRUD pour les Tenants
Nous construisons l'interface dans notre application centrale pour lister, créer, modifier et supprimer des tenants. Cela inclut le contrôleur et les vues Blade.
5.1. Le contrôleur : TenantController.php
Ce contrôleur gère la logique pour le CRUD des tenants, incluant la recherche, le tri, la pagination et l'export Excel.
<?php
namespace App\Http\Controllers;
use App\Models\Tenant;
use Illuminate\Http\Request;
use App\Exports\TenantsExport;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Validation\Rule;
class TenantController extends Controller
{
public function index(Request $request)
{
$filters = [
'search' => $request->input('search', ''),
'sort_by' => $request->input('sort_by', 'created_at'),
'sort_dir' => $request->input('sort_dir', 'desc'),
'per_page' => $request->input('per_page', 10),
];
$query = Tenant::with('domains');
if (!empty($filters['search'])) {
$query->where(function ($q) use ($filters) {
$q->where('id', 'like', '%' . $filters['search'] . '%')
->orWhere('data->name', 'like', '%' . $filters['search'] . '%')
->orWhereHas('domains', function ($domainQuery) use ($filters) {
$domainQuery->where('domain', 'like', '%' . $filters['search'] . '%');
});
});
}
$query->orderBy($filters['sort_by'], $filters['sort_dir']);
$tenants = $query->paginate($filters['per_page'])->withQueryString();
return view('tenants.index', compact('tenants', 'filters'));
}
public function create()
{
return view('tenants.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'id' => 'required|string|unique:tenants|alpha_dash|min:3',
'domain' => ['required', 'string', 'unique:domains,domain', 'regex:/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'],
'data' => 'required|array',
'data.name' => 'required|string|max:255',
'data.plan' => 'nullable|string',
]);
$tenant = Tenant::create($validated);
$tenant->createDomain(['domain' => $validated['domain']]);
return redirect()->route('tenants.index')->with('success', 'Tenant créé avec succès.');
}
public function edit(Tenant $tenant)
{
$tenant->load('domains');
return view('tenants.edit', compact('tenant'));
}
public function update(Request $request, Tenant $tenant)
{
$validated = $request->validate([
'domain' => [
'required', 'string',
Rule::unique('domains')->ignore($tenant->domains->first()?->id),
'regex:/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'
],
'data' => 'required|array',
'data.name' => 'required|string|max:255',
'data.plan' => 'nullable|string',
]);
$tenant->update(['data' => $validated['data']]);
if ($tenant->domains->first()) {
$tenant->domains->first()->update(['domain' => $validated['domain']]);
} else {
$tenant->createDomain(['domain' => $validated['domain']]);
}
return redirect()->route('tenants.index')->with('success', 'Tenant mis à jour avec succès.');
}
public function destroy(Tenant $tenant)
{
$tenant->delete();
return redirect()->route('tenants.index')->with('success', 'Tenant supprimé avec succès.');
}
public function export()
{
return Excel::download(new TenantsExport, 'tenants.xlsx');
}
}
5.2. Les Vues Blade
Nous avons besoin de plusieurs vues pour le CRUD : une pour la liste (index.blade.php), une pour la création (create.blade.php), une pour l'édition (edit.blade.php), et un formulaire partiel (_form.blade.php).
resources/views/tenants/index.blade.php:
<x-app-layout title="Gestion des Tenants">
<!-- Code complet de la vue index.blade.php -->
</x-app-layout>
@push('scripts')
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('columnsManager', () => ({
// ... Code Alpine.js complet ...
}));
});
</script>
@endpush
resources/views/tenants/create.blade.php:
<x-app-layout title="Créer un Tenant">
<div class="py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto">
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
<div class="p-6 md:p-8">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-200">
Créer un nouveau tenant
</h2>
<form action="{{ route('tenants.store') }}" method="POST" class="mt-8">
@csrf
@include('tenants._form')
<div class="flex items-center justify-end space-x-4 mt-8">
<a href="{{ route('tenants.index') }}" class="btn bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-white">Annuler</a>
<button type="submit" class="btn bg-primary text-white">Créer le Tenant</button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
resources/views/tenants/_form.blade.php:
@if ($errors->any())
<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- ID du Tenant (uniquement à la création) -->
@unless(isset($tenant))
<div class="md:col-span-2">
<label for="id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">ID du Tenant</label>
<input type="text" name="id" id="id" value="{{ old('id') }}"
class="mt-1 p-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"
placeholder="ex: acme, societex" required>
<p class="mt-2 p-2 text-xs text-gray-500">Doit être unique, sans espaces ni caractères spéciaux (sauf tirets).</p>
</div>
@endunless
<!-- Nom de l'entreprise (dans data) -->
<div>
<label for="data_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom de l'entreprise</label>
<input type="text" name="data[name]" id="data_name" value="{{ old('data.name', isset($tenant) ? $tenant->data['name'] ?? '' : '') }}"
class="mt-1 p-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200" required>
</div>
<!-- Domaine principal -->
<div>
<label for="domain" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Domaine principal</label>
<input type="text" name="domain" id="domain" value="{{ old('domain', isset($tenant) ? $tenant->domains->first()?->domain ?? '' : '') }}"
class="mt-1 p-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"
placeholder="ex: client.mondomaine.com" required>
</div>
</div>
Chapitre 6 : Automatisation de la création des BDD
Lorsque nous créons un tenant via notre interface, sa base de données, ses tables et ses données initiales doivent être créées automatiquement en arrière-plan. Nous utilisons les événements et les jobs de Laravel pour cela.
6.1. Le Listener `SetupTenantDatabaseListener`
Ce listener écoute l'événement TenantCreated et délègue le travail à un job en file d'attente.
<?php // app/Listeners/SetupTenantDatabaseListener.php
namespace App\Listeners;
use Stancl\Tenancy\Events\TenantCreated;
use App\Jobs\SetupTenantDatabase;
class SetupTenantDatabaseListener
{
public function handle(TenantCreated $event)
{
$tenantModel = $event->tenant;
if ($tenantModel->database_initialized) {
return; // déjà traité
}
SetupTenantDatabase::dispatch($tenantModel);
}
}
6.2. Le Job `SetupTenantDatabase`
Ce job est responsable de l'exécution des migrations et des seeders pour le nouveau tenant.
<?php // app/Jobs/SetupTenantDatabase.php
namespace App\Jobs;
use Stancl\Tenancy\Jobs\MigrateDatabase;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Database\Seeders\tenant\TenantDatabaseSeeder;
class SetupTenantDatabase implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $tenant;
public function __construct($tenant)
{
$this->tenant = $tenant;
}
public function handle()
{
// 1. Migration
MigrateDatabase::dispatchSync($this->tenant);
// 2. Initialiser le contexte du tenant
tenancy()->initialize($this->tenant);
// 3. Lancer le Seeder
(new TenantDatabaseSeeder())->run();
// 4. Terminer le contexte
tenancy()->end();
}
}
6.3. Enregistrement de l'événement et configuration
Nous enregistrons le listener dans AppServiceProvider et nous configurons les domaines centraux pour la production.
<?php // app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\Events\TenantCreated;
use App\Listeners\SetupTenantDatabaseListener;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Event::listen(
TenantCreated::class,
SetupTenantDatabaseListener::class,
);
}
}
N'oubliez pas d'ajouter votre domaine de production à la liste des domaines centraux dans config/tenancy.php pour éviter qu'il soit traité comme un tenant.
// config/tenancy.php
'central_domains' => [
'127.0.0.1',
'localhost',
'central.manar.com', // Très important pour la production
],
Partie 4 : Modularisation de l'Application
Chapitre 7 : Intégration de Laravel-Modules (Nwidart)
Pour les grosses applications, il est préférable de diviser le code en modules (ex: Facturation, Scolarité). Nous utilisons le package nwidart/laravel-modules pour y parvenir.
7.1. Installation et publication
composer require nwidart/laravel-modules
php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider"
7.2. Configuration de composer.json
Ajoutez le plugin `merge-plugin` pour que Composer découvre automatiquement les dépendances de chaque module.
"extra": {
"laravel": {
"dont-discover": []
},
"merge-plugin": {
"include": [
"Modules/*/composer.json"
]
}
},
7.3. Création d'un premier module
Cette commande va créer un dossier Modules/School avec toute une arborescence (Contrôleurs, Modèles, Vues, etc.).
php artisan module:make School
7.4. Recharger l'autoload de Composer
Après avoir créé un module, il est crucial de mettre à jour le chargement automatique des classes.
composer dump-autoload
php artisan optimize:clear
Chapitre 8 : Migrations et Routes dans les Modules
Travailler avec des modules change la façon dont nous gérons les migrations et les routes, surtout dans un contexte multi-tenant.
8.1. Migrations des tenants dans un module
Pour que tenancy découvre automatiquement les migrations des tenants de nos modules, nous devons mettre à jour config/tenancy.php.
// dans config/tenancy.php
'migration_parameters' => [
'--force' => true,
'--path' => [
'database/migrations/tenant',
'Modules/*/database/migrations/tenant' // Ajout de cette ligne
],
'--realpath' => true,
],
Ensuite, pour créer une migration de tenant pour un module, spécifiez le chemin :
php artisan make:migration create_test_table --path="Modules/School/database/migrations/tenant"
8.2. Seeders des tenants dans un module
Votre seeder principal de tenant (ex: `TenantDatabaseSeeder.php`) peut appeler les seeders de chaque module.
<?php // database/seeders/TenantDatabaseSeeder.php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class TenantDatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()->create([
'name' => 'Admin Tenant',
'email' => 'admin@tenant.com',
'password' => bcrypt('password'),
]);
// Appeler les seeders des modules
$this->call(\Modules\School\Database\Seeders\SchoolDatabaseSeeder::class);
// $this->call(\Modules\OtherModule\Database\Seeders\OtherModuleSeeder::class);
}
}
8.3. Routes des modules et contexte du tenant
Pour que les routes d'un module soient conscientes du tenant actuel, créez un alias pour le middleware de `tenancy` dans bootstrap/app.php.
<?php // dans bootstrap/app.php
// ...
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'tenant' => \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
]);
// ...
})
// ...
Ensuite, dans le fichier de routes de votre module (ex: Modules/School/routes/web.php), protégez vos routes ainsi :
<?php
use Illuminate\Support\Facades\Route;
use Modules\School\Http\Controllers\SchoolController;
Route::middleware(['web', 'tenant', 'auth'])->prefix('admin')->group(function () {
Route::get('/schools', [SchoolController::class, 'index'])->name('schools.index');
});
Part 5: Dynamic Themes per Tenant
Chapter 9: Configuring Vite.js and Theme Helpers
To offer deep customization, we will allow each tenant to have their own theme. The first step is to configure our asset bundler, Vite.js.
9.1. Update `vite.config.js`
We need to list the entry points (JS/CSS files) for each theme in Vite's configuration file.
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from 'tailwindcss';
export default defineConfig({
plugins: [
laravel({
input: [
// Central theme
'resources/css/app.css',
'resources/js/app.js',
// Theme minimalist
'resources/views/themes/minimalist/assets/css/app.css',
'resources/views/themes/minimalist/assets/js/app.js',
// Theme modern
'resources/views/themes/modern/assets/css/app.css',
'resources/views/themes/modern/assets/js/app.js',
],
refresh: true,
}),
tailwindcss(),
],
});
9.2. Create a `theme_vite()` helper
To load the correct assets in our views, we create a custom helper function. Create the app/helpers.php file and register it in composer.json to be autoloaded.
<?php
use App\Services\ThemeManager;
use Illuminate\Foundation\Vite;
use Illuminate\Support\HtmlString;
if (! function_exists('theme_vite')) {
function theme_vite(string|array $entrypoints): HtmlString
{
$themeSlug = app(ThemeManager::class)->getCurrentThemeSlug();
if (!$themeSlug) {
return new HtmlString('');
}
$projectRelativeEntrypoints = collect($entrypoints)->map(function ($entrypoint) use ($themeSlug) {
return 'resources/views/themes/' . $themeSlug . '/' . ltrim($entrypoint, '/');
})->all();
return app(Vite::class)($projectRelativeEntrypoints);
}
}
Chapter 10: Dynamic Provider and Layout Components
The brain of our theming system will be a `ThemeManager` and a `ThemeServiceProvider`. They will determine which theme to load and configure Blade accordingly.
10.1. The `ThemeManager` Service
This service's sole responsibility is to determine the current theme's slug based on a tenant's configuration.
<?php
namespace App\Services;
use App\Models\Setting;
use Illuminate\Support\Facades\Cache;
class ThemeManager
{
public function getCurrentThemeSlug(): string
{
if (!tenancy()->initialized || !tenancy()->tenant) {
return 'minimalist'; // Default theme for central app or if no tenant
}
$cacheKey = 'tenant_theme_' . tenancy()->tenant->getTenantKey();
return Cache::rememberForever($cacheKey, function () {
$themeSetting = Setting::where('key', 'theme')->first()?->value;
return $themeSetting ?? 'minimalist';
});
}
}
10.2. The `ThemeServiceProvider`
This provider uses the `ThemeManager` to tell Laravel where to find the active theme's Blade views and components. Don't forget to register it in bootstrap/providers.php.
<?php
namespace App\Providers;
use App\Services\ThemeManager;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ThemeServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(ThemeManager::class, function () {
return new ThemeManager();
});
}
public function boot(): void
{
if ($this->app->runningInConsole()) {
$themeComponentPath = resource_path('views/themes/minimalist/components');
if (is_dir($themeComponentPath)) {
Blade::anonymousComponentPath($themeComponentPath, 'theme');
}
return;
}
View::composer('*', function ($view) {
$themeManager = $this->app->make(ThemeManager::class);
$currentTheme = $themeManager->getCurrentThemeSlug();
$themeViewPath = resource_path('views/themes/' . $currentTheme);
$themeComponentPath = resource_path('views/themes/' . $currentTheme . '/components');
if (is_dir($themeViewPath)) {
$this->app['view']->prependLocation($themeViewPath);
}
if (is_dir($themeComponentPath)) {
Blade::anonymousComponentPath($themeComponentPath, 'theme');
}
});
}
}
10.3. Layout Components
Finally, components like `AppLayout` and `BaseLayout` must be modified to use the `ThemeManager` to render the layout view corresponding to the active theme.
<?php
namespace App\View\Components;
use App\Services\ThemeManager;
use Illuminate\Support\Facades\Blade;
use Illuminate\View\Component;
class AppLayout extends Component
{
protected ThemeManager $themeManager;
protected string $themeSlug;
public function __construct(ThemeManager $themeManager)
{
$this->themeManager = $themeManager;
$this->themeSlug = $this->themeManager->getCurrentThemeSlug();
}
public function render()
{
$themePath = resource_path('views/themes/' . $this->themeSlug);
$themeViewsPath = $themePath . '/views';
$themeComponentsPath = $themePath . '/components';
if (is_dir($themeViewsPath)) {
view()->getFinder()->prependLocation($themeViewsPath);
}
if (is_dir($themeComponentsPath)) {
Blade::anonymousComponentPath($themeComponentsPath, 'theme');
}
return view('layouts.app');
}
}
Conclusion & Perspectives
Félicitations pour avoir terminé ce parcours sur Laravel Multi-Tenant !
Vous avez exploré en profondeur les concepts nécessaires à la création d'une application SaaS robuste et scalable. De l'initialisation du projet à la modularisation, en passant par la gestion dynamique des thèmes, vous possédez désormais les compétences pour construire des applications complexes et bien structurées. Le voyage ne s'arrête pas là. Continuez à explorer, à expérimenter et à construire. La pratique est la clé de la maîtrise !
Explorez le Code Source
Le code complet de ce projet est disponible sur GitHub. N'hésitez pas à le cloner, l'étudier ou même y contribuer !
Voir sur GitHub