Themes in a multi tenant Laravel application

This is how we've configured themes in an app where one company operates under different brands in a single Laravel application.

Our solution provides different views for each tenant and as such isn't great for SaaS applications. Although we're using Spatie's package for multi-tenancy, this should work for other packages as well.

We have an application with multiple tenants. Each tenant represents a brand that has its own visual identity. We require a lot of flexibility when changing the look & feel for each brand. This means we have to change how Laravel loads its Blade-files.

How do views work in Laravel?

Laravel's view()-helper seems simple when you're using it. You pass the name of a blade file, the data it requires and your controller renders the HTML for you. Under the hood it's actually pretty sophisticated, like everything in Laravel it's an incredible elegant solution.

Views in Laravel are managed by the View-factory. The view()-helper retrieves the factory from the container and instantiates a new View-object. In that process, the FileViewFinder-class is used to find your blade file. By default, the finder-class will look for files in the paths specific in your view.paths config.

As with many things in Laravel, both the View-factory and the FileViewFinder are available in the Service Container. And this is actually what we use to implement themes in our multi-tenant application.

Adding theme support

By default views are stored in resources/views. But we want to store the blade files of our tenants in resources/themes/{brand}. Laravel will not look in that directory by default.

Unfortunately, it's not as easy as adding new locations to the view.paths configuration. This wouldn't understand the concept of tenants, so a different approach is needed.

We already know the FileViewFinder is used to find blade-files, so we can probably change its logic to look in our custom location as well. As it turns out, the findInPaths-method is used to return the path of a view, so we can probably change how that works.

We extended the FileViewFinder as below:

class ViewTenantFinder extends FileViewFinder
{
protected function findInPaths($name, $paths): string
{
$tenant = Tenant::current();
 
array_unshift($paths, $this->resolvePath(
resource_path('themes/'.($tenant?->theme ?? 'default').'/views')
));
 
return parent::findInPaths($name, $paths);
}
}
class ViewTenantFinder extends FileViewFinder
{
protected function findInPaths($name, $paths): string
{
$tenant = Tenant::current();
 
array_unshift($paths, $this->resolvePath(
resource_path('themes/'.($tenant?->theme ?? 'default').'/views')
));
 
return parent::findInPaths($name, $paths);
}
}

With Spatie's Multitenancy package we can retrieve the current tenant. We have adjusted our database scheme to include a theme column which we will use as a folder name. With array_unshift we prepend our path to the array of paths specified in the view.paths configuration.

To make sure Laravel uses our class instead of the standard FileViewFinder, we need to change the binding in our AppServiceProvider:

$this->app->bind('view.finder', function ($app) {
return new ViewTenantFinder(
$app['files'],
$app['config']['view.paths'],
);
});
$this->app->bind('view.finder', function ($app) {
return new ViewTenantFinder(
$app['files'],
$app['config']['view.paths'],
);
});

With this, the View factory will use our custom ViewTenantFinder-class to find a suitable blade file. If a file doesn't exist in your theme, it will automatically use the file in your resources/views folder.