In a base Laravel installation our views are placed under resources/views/*, but our goal is to have something like this themes/my-cool-theme/*

Let's start with a little excursion to how Laravel views work out of the box.

Laravel Service Providers

Service providers are the central place of all Laravel application bootstrapping. Your application, as well as all of Laravel's core services, are bootstrapped via service providers. Service providers are the central place to configure and extend your application.

By default, a set of Laravel core service providers are listed in config/app.php under providers array key.

How does Laravel views work by default?

Laravel views mechanism registered using Illuminate\View\ViewServiceProvider class, the section we are interested in is:

/**
* Register the view finder implementation.
*
* @return void
*/
public function registerViewFinder()
{
    $this->app->bind('view.finder', function ($app) {
        return new FileViewFinder($app['files'], $app['config']['view.paths']);
    });
}

FileViewFinder class has a find method that helps Laravel to find a view by view name under $this->paths directories. Other words, it does what we need.

/**
 * Get the fully qualified location of the view.
 *
 * @param  string  $name
 * @return string
 */
public function find($name)
{
	if (isset($this->views[$name])) {
		return $this->views[$name];
	}

	if ($this->hasHintInformation($name = trim($name))) {
		return $this->views[$name] = $this->findNamespacedView($name);
	}

	return $this->views[$name] = $this->findInPaths($name, $this->paths);
}

Based on this, to implement theming in your Laravel project you need:

  • Create a new view finder class, called FileThemeViewFinder
  • Create and register a new service provider

Starting with new view finder class

Ok, let's create a new FileThemeViewFinder class which extends from FileViewFinder and add some methods to support theming. Let's say we put our new class into app/Services/Theme, so its namespace will be App\Services\Theme.

<?php

namespace App\Services\Theme;

use Illuminate\View\FileViewFinder;

class FileThemeViewFinder extends FileViewFinder
{
    public function setThemePath(array|string $path): static
    {
        if (!is_array($path)) {
            $path = (array)$path;
        }

        $paths = $this->getPaths();
        $paths = array_merge($path, $paths);
        $this->setPaths($paths);
        return $this;
    }
}

I've added only one method setThemePath(array|string $path) which we will call in our new Service Provider.

Creating a new Service Provider

Let's start with a new Service Provider class called ThemeViewServiceProvider

$ php artisan make:provider ThemeViewServiceProvider

Now, open our new app/Providers/ThemeViewServiceProvider.php and bind an instance of FileThemeViewFinder class.

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

  $themePath = base_path('themes') .
  	DIRECTORY_SEPARATOR . config('defaults.theme') .
    DIRECTORY_SEPARATOR . 'views';

	$finder->setThemePath($themePath);
});

second parameter of FileViewFinder is an array of paths to find a views. By default the class is looking for views in a config('view.paths'), but we are going to change this.

Let's say we have a Laravel application under /var/www/html and our themes will be placed at /var/www/html/themes/ and our cool theme name is configured in config/app.php under theme key.

In this case

$themePath = [base_path('themes') . '/' . config('app.theme') . '/views'];

returns /var/www/html/themes/theme_name/views/

this is the path where our vews will be placed.

Registering a newly created service provider

The only thing we need to do is register our new service provider.

Just add \App\Providers\ThemeViewServiceProvider::class, to config/app.php under providers array key.

Congratulations. At this point we tell Laravel that we have a new service provider and to look for a views by newly configured path.

A nice bonus

We tell Laravel to look for a view in a list of paths: ['themes/theme_name/views/', 'resources/views/']. If the template is not found in themes/theme_name/views/, Laravel will look for it in resources/views/, so, we don't need to theming all templates of our application, missing theme templates will be found under resources/views/.

Work with namespaced templates (error pages and so on)

At this point Laravel still looks for namespaced templates, such error pages, in a standard place (vendor folder or resources/views/)

I have a post about How to create custom 404 error page in Laravel Application

But If we want our error pages to be styled inside our theme, we need to make some modification to our FileThemeViewFinder class.

Let's add theme two functions:

public function setThemeNamespace(string $namespace, array|string $paths)
{
    $this->themeHints[$namespace] = $paths;
}

protected function findNamespacedView($name): string
{
    $this->hints = array_merge_recursive($this->themeHints, $this->hints);
    return parent::findNamespacedView($name);
}

then modify app/Providers/ThemeViewServiceProvider.php class like this:

private function registerThemeViewFinder()
{
	$this->app->bind('view.finder', function ($app) {
		$finder = new FileThemeViewFinder($app['files'], $app['config']['view.paths']);

		$themePath = base_path('themes') .
			DIRECTORY_SEPARATOR . config('defaults.theme') .
			DIRECTORY_SEPARATOR . 'views';

		$finder->setThemePath($themePath)
			->setThemeNamespace('errors', [ $themePath . DIRECTORY_SEPARATOR . 'errors' ]);

		return $finder;
	});
}

now, place your error pages to theme/theme_name/views/errors/.

Now you can run your Laravel application with multiple themes. This is a simple example how you can implement it on your own way. But if you would like, you can use this package vpominchuk/laravel-theme which allows you to do more.