Photo de F. Rahmouni Oussama

By F. Rahmouni Oussama

Trainer in Software Development & Data Science, ISMO

Laravel Saas - Building a Laravel 12 Multi-Tenant, Dynamic Theming and Modular Application from A to Z

Last update: November 2025

Part 1: Project Initialization & Prerequisites

Chapter 1: Installing Laravel 12 and the Base Layout

We start with the basics: creating a fresh Laravel 12 project, configuring the database, and setting up a simple layout structure with a Blade component to get ready for building our interface.

1.1. Create the Laravel Project

composer create-project laravel/laravel multitenants

1.2. Configure the Environment (.env)

Modify your .env file to connect the project to your central MySQL database.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_multitenant_central
DB_USERNAME=root
DB_PASSWORD=

1.3. Run the First Migration

Run the initial migration to create Laravel's base tables (users, etc.) in the central database.

php artisan migrate

1.4. Create the Layout Component

We create an AppLayout component that will serve as the main structure for our application's pages.

php artisan make:component AppLayout

This generates App/View/Components/AppLayout.php and resources/views/components/app-layout.blade.php. You can then structure your layout by including partials like a header and a sidebar (main-sidebar.blade.php).

Chapter 2: Integrating Redis for Cache & Sessions

To improve performance, we will configure Redis as the driver for cache, sessions, and queues.

2.1. Install the Predis Client via Composer

composer require predis/predis

2.2. Update the .env File

Modify these variables to make Laravel use Redis. Note: if the line CACHE_STORE=database is present, it must be removed to avoid conflicts.

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. Check Configuration Files

Ensure that the config/database.php and config/cache.php files are correctly set up to use these Redis environment variables.


// In 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),
    ],
],

// In config/cache.php
'stores' => [
    // ...
    'redis' => [
        'driver' => 'redis',
        'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
        'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
    ],
    // ...
],
                

2.4. Test the Redis Connection

Use the Redis command-line interface to ensure the server is running and responsive.

redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> exit

Part 2: Multi-Tenant Architecture

Chapter 3: Setting up Stancl/Tenancy

We install the stancl/tenancy package, which will provide us with all the tools to manage separate databases for each client (tenant).

3.1. Install the Package

composer require stancl/tenancy
php artisan tenancy:install

3.2. Create the Tenant Model

Create the file app/Models/Tenant.php. This Eloquent model will represent our clients in the central database.

<?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 and Central Migration

Open config/tenancy.php and ensure the tenant_model key points to your new model. Then, run the migration to create the tenants table.

// In config/tenancy.php
'tenant_model' => \App\Models\Tenant::class,
php artisan migrate

Chapter 4: Tenant Migrations, Seeders, and Routing

We will now structure our application to handle migrations and routes specific to each tenant.

4.1. Tenant Migrations

Migrations for tenant tables (e.g., products, invoices...) must be placed in database/migrations/tenant. To create one:

php artisan make:migration create_eleves_table --path=database/migrations/tenant

To run these migrations on all existing tenants or on a specific tenant:

# For all tenants
php artisan tenants:migrate

# For a single tenant
php artisan tenants:migrate --tenant=school1

# To reset and re-run migrations for all tenants
php artisan tenants:migrate-fresh

4.2. Tenant Seeders

To ensure seeding commands target the correct class, configure config/tenancy.php:

// In config/tenancy.php
'seeder_parameters' => [
    '--class' => 'TenantDatabaseSeeder', // root seeder class
],

Then, run the seeder:

php artisan tenants:seed

4.3. Create and configure TenancyServiceProvider

This Service Provider, included with the package, is crucial. Ensure it is created (app/Providers/TenancyServiceProvider.php) and registered in bootstrap/providers.php.

<?php

// In bootstrap/providers.php
return [
    App\Providers\AppServiceProvider::class,
    App\Providers\TenancyServiceProvider::class, // Add this line
];

4.4. Route Separation

The routes/tenant.php file will contain routes accessible via a tenant's domain. The routes/web.php file is for the central application.

Example for 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');
    });
});

Example for 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');
    
    // Add routes for the tenant CRUD
    Route::resource('tenants', \App\Http\Controllers\TenantController::class)->except(['show']);
    Route::get('tenants/export', [\App\Http\Controllers\TenantController::class, 'export'])->name('tenants.export');
});

4.5. Create a First Tenant for Testing

Let's use Tinker to manually create our first tenant.

php artisan tinker

# In Tinker
use App\Models\Tenant;
$tenant = Tenant::create(['id' => 'school1']);
$tenant->domains()->create(['domain' => 'school1.myapp.com']);
exit;

Don't forget to add 127.0.0.1 school1.myapp.com to your hosts file to access it locally via `http://school1.myapp.com:8000`.

Part 3: Tenant Management (Central Application)

Chapter 5: Creating the CRUD for Tenants

We will now build the interface in our central application to list, create, edit, and delete tenants. This includes the controller and the Blade views.

5.1. The Controller: TenantController.php

This controller handles the business logic for the tenant CRUD, including search, sorting, pagination, and Excel export.

<?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([
            'id' => $validated['id'],
            'data' => [
                'name' => $validated['data']['name'],
                'plan' => $validated['data']['plan'] ?? 'Standard',
            ],
        ]);
        
        $tenant->createDomain(['domain' => $validated['domain']]);

        return redirect()->route('tenants.index')->with('success', 'Tenant created successfully.');
    }

    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 updated successfully.');
    }
    
    public function destroy(Tenant $tenant)
    {
        $tenant->delete();
        return redirect()->route('tenants.index')->with('success', 'Tenant deleted successfully.');
    }

    public function export()
    {
        return Excel::download(new TenantsExport, 'tenants.xlsx');
    }
}

5.2. The Blade Views

We need several views for the CRUD: one for the list (index.blade.php), one for creation (create.blade.php), one for editing (edit.blade.php), and a partial form (_form.blade.php).

resources/views/tenants/index.blade.php:

<x-app-layout title="Tenant Management">
    <div class="py-12 px-4 sm:px-6 lg:px-8">
        <div class="max-w-7xl mx-auto" x-data="columnsManager">

            <div class="sm:flex sm:items-center sm:justify-between mb-6">
                <div>
                    <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-200">Tenant Management</h2>
                    <p class="mt-1 text-sm text-gray-500">Manage client accounts (tenants) and their access domains.</p>
                </div>
                <div class="mt-4 sm:mt-0 sm:ml-4 flex space-x-3 items-center">
                     <a href="{{ route('tenants.create') }}" class="btn bg-primary text-white">New Tenant</a>
                     <a href="{{ route('tenants.export') }}" class="btn bg-success text-white">Export Excel</a>
                 </div>
            </div>

            <!-- Table, filters, pagination... -->
            
        </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">
    @unless(isset($tenant))
    <div class="md:col-span-2">
        <label for="id" class="block text-sm font-medium text-gray-700">Tenant ID</label>
        <input type="text" name="id" id="id" value="{{ old('id') }}" class="mt-1 block w-full" placeholder="e.g., acme, companyx" required>
        <p class="mt-2 text-xs text-gray-500">Must be unique, no spaces or special characters (except dashes).</p>
    </div>
    @endunless

    <div>
        <label for="data_name" class="block text-sm font-medium text-gray-700">Company Name</label>
        <input type="text" name="data[name]" id="data_name" value="{{ old('data.name', isset($tenant) ? $tenant->data['name'] ?? '' : '') }}" class="mt-1 block w-full" required>
    </div>

    <div>
        <label for="domain" class="block text-sm font-medium text-gray-700">Main Domain</label>
        <input type="text" name="domain" id="domain" value="{{ old('domain', isset($tenant) ? $tenant->domains->first()?->domain ?? '' : '') }}" class="mt-1 block w-full" placeholder="e.g., client.mydomain.com" required>
    </div>
</div>

Chapter 6: Automating Database Creation

When we create a tenant through our interface, its database, tables, and initial data should be created automatically in the background. We'll use Laravel's events and jobs for this.

6.1. The `SetupTenantDatabaseListener`

This listener will listen for the TenantCreated event and delegate the heavy lifting to a queued job.

<?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; // already processed
        }
        SetupTenantDatabase::dispatch($tenantModel);
    }
}

6.2. The `SetupTenantDatabase` Job

This job is responsible for running migrations and seeders for the new tenant.

<?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 Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Database\Seeders\tenant\TenantDatabaseSeeder;

class SetupTenantDatabase implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tenant;

    public function __construct($tenant)
    {
        $this->tenant = $tenant;
    }

    public function handle()
    {
        $tenantId = $this->tenant->id;

        // 1. Migration
        MigrateDatabase::dispatchSync($this->tenant);

        // 2. Initialize tenant context
        tenancy()->initialize($this->tenant);

        // 3. Run Seeder
        try {
            Log::info("Job SeedTenantDatabase started for: {$tenantId}");
            (new TenantDatabaseSeeder())->run();
            Log::info("Seeding completed for: {$tenantId}");
        } catch (\Exception $e) {
            Log::error("Seeding job failed for {$tenantId}: " . $e->getMessage());
            throw $e;
        }

        // 4. End tenant context
        tenancy()->end();
    }
}

6.3. Event Registration and Configuration

We register the listener in `AppServiceProvider` and configure the central domains for production.

<?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,
        );
    }
}

Don't forget to add your production domain to the central domains list in config/tenancy.php to prevent it from being treated as a tenant.

// config/tenancy.php
'central_domains' => [ 
    '127.0.0.1',
    'localhost',
    'central.manar.com', // Very important for production
],

Part 4: Application Modularization

Chapter 7: Integrating Laravel-Modules (Nwidart)

For large applications, it's better to split the code into modules (e.g., Billing, Schooling). We use the nwidart/laravel-modules package to achieve this.

7.1. Installation and Publishing

composer require nwidart/laravel-modules
php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider"

7.2. Configure composer.json

Add the `merge-plugin` so that Composer automatically discovers each module's dependencies.

"extra": {
    "laravel": {
        "dont-discover": []
    },
    "merge-plugin": {
        "include": [
            "Modules/*/composer.json"
        ]
    }
},

7.3. Create a First Module

This command will create a Modules/School directory with a full file structure (Controllers, Models, Views, etc.).

php artisan module:make School

7.4. Reload Composer's Autoloader

After creating a module, it's crucial to update the class autoloader.

composer dump-autoload
php artisan optimize:clear

Chapter 8: Migrations and Routes in Modules

Working with modules slightly changes how we manage migrations and routes, especially in a multi-tenant context.

8.1. Tenant Migrations in a Module

For tenancy to automatically discover our modules' tenant migrations, we need to update config/tenancy.php.

// in config/tenancy.php
'migration_parameters' => [
    '--force' => true,
    '--path' => [
        'database/migrations/tenant',
        'Modules/*/database/migrations/tenant' // Add this line
    ],
    '--realpath' => true,
],

Then, to create a tenant migration for a module, specify the path:

php artisan make:migration create_test_table --path="Modules/School/database/migrations/tenant"

8.2. Tenant Seeders in a Module

Your main tenant seeder (e.g., `TenantDatabaseSeeder.php`) can call seeders from each module.

<?php
namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class TenantDatabaseSeeder extends Seeder
{
    public function run(): void
    {
        User::factory()->create([
            'name' => 'Manar',
            'email' => 'manar@admin.com',
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
        ]);

        // Call module seeders
         $this->call(\Modules\School\Database\Seeders\SchoolDatabaseSeeder::class);
    }
}

8.3. Module Routes and Tenant Context

To make a module's routes aware of the current tenant, create an alias for the `tenancy` middleware in bootstrap/app.php.

<?php // in bootstrap/app.php
// ...
->withMiddleware(function (Middleware $middleware) {
     $middleware->alias([
        'tenant' => \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
    ]);
    // ...
})
// ...

Then, in your module's routes file (e.g., Modules/School/routes/web.php), you can protect your routes like this:

<?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 & Future Steps

Congratulations on completing this journey on Laravel Multi-Tenancy!

You have deeply explored the concepts required to build a robust and scalable SaaS application. From project initialization to modularization and dynamic theme management, you now have the skills to build complex, well-structured applications. The journey doesn't end here. Keep exploring, experimenting, and building. Practice is the key to mastery!

Explore the Source Code

The complete code for this project is available on GitHub. Feel free to clone it, study it, or even contribute!

View on GitHub