September 11, 2017 • 7 minute read
Generating custom branded stylesheets in Laravel with scssphp
Want to provide your customers with a quick and simple way of styling your Laravel application to match their corporate brand? This post is for you. If you've ever customised Bootstrap with Sass (or LESS) before, you already know how simple it is to drastically change the visual appearance of your Web apps and sites by merely changing a few variables. Simply set $brand-primary
, run Laravel Mix and you're done - all of your buttons, panels and other Bootstrap components will now display in your colour scheme. But what if you wanted to extend this power to your customers, simply by submitting a form on your app, so that their colour is saved in the database and the entire appearance of your app changes to reflect their selection?
A naive implementation, and one that I've used in the past, would be to create a Blade template named something like styles.blade.php
and add a metric ton of CSS overrides to change various properties of buttons, panels and other components based on a value in your database:
.btn-primary {
background-color: {{ $customer->brand_primary }};
}
You'll quickly discover that this approach is not a lot of fun. When you mouse over the button, for example, you'll noticed that you also want to change the background color on the hover state to a darker shade of the $customer->brand_primary
variable. But because you're working in PHP and CSS rather than Sass, you won't have the trusty darken()
Sass function to hand. Sure, you can implement a crude variant of it in PHP and use that instead - but before long you'll find yourself looking for other features like colour conversion, mixins and more. All it offers is a shitload of misery.
Wouldn't it be great if you could just use Sass in your PHP code? Well, actually, you can - thanks to a fantastic library called scssphp
.
Sass to the rescue
Installing the library is as simple as requiring a Composer package in your Laravel project:
$ composer require leafo/scssphp
Using it is pretty straightforward too:
$scss = new Leafo\ScssPhp\Compiler();
$css = $scss->compile('
$brand-primary: '.$customer->brand_primary.';
.btn-primary {
background-color: $brand-primary;
}
');
echo $css;
Assuming the value of $customer->brand_primary
is #E91E63
, the above snippet would output the following CSS:
.btn-primary {
background-color: #E91E63;
}
Nice. Even nicer, however, is that you can use the @import
directive to load in Sass files from the filesystem. Want to bring in your existing project's Sass files, or even Sass files from a library like Bootstrap installed with npm
? No problem - just tell scssphp
where to look:
$scss = new Leafo\ScssPhp\Compiler();
$scss->addImportPath(realpath(app()->path() . '/../resources/assets/sass'));
$css = $scss->compile('
@import "variables";
$brand-primary: '.$customer->brand_primary.'
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
');
This will load the _variables.scss
file from your Laravel app's resources/assets/sass
directory, override the $brand-primary
variable with the value from your customer model, and finally import Bootstrap itself. Essentially it's like running Laravel Mix, but using colour values stored in the database. One thing to note is that the tilde syntax for referencing the node_modules
path will not work - you'll need to use the horribly ugly ../../../../node_modules
style instead I'm afraid!
Storing the output
Now that you're generating this CSS code, the next concern is what to do with it. Storing it in a file that can be publicly accessed is a smart move, either on your server's filesystem, or better yet in a public readable Amazon S3 object. Whatever you decide to do, it's easy to achieve using the Storage
facade. For the sake of example, I'm going to store it in the public
disk, which is simply the public
folder in storage/app
symlinked into the public
directory using the php artisan storage:link
command.
I like to put this code directly in the model itself. Flame me to your hearts' content, it works for me - if you prefer to use repositories or service classes or any other approach, do what you need to do! The following is an example of my usage in a Laravel Eloquent model:
<?php
namespace App;
use Leafo\ScssPhp\Compiler;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Customer extends Model
{
// ...
public function getStylesheetUrlAttribute()
{
return Storage::disk('public')->url("css/$this->id/app.css");
}
// ...
public function compileTheme()
{
$scss = new Compiler();
$scss->addImportPath(realpath(app()->path() . '/../resources/assets/sass'));
$scss->setFormatter('Leafo\ScssPhp\Formatter\Crunched');
$output = $scss->compile('
@import "variables";
$brand-primary: '.$this->brand_primary.'
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
');
Storage::disk('public')->put("css/$this->id/app.css", $output);
}
}
The getStylesheetUrlAttribute
method allows me to easily get a URL to load the stylesheet using $customer->stylesheet_url
. The compileTheme
method will take the customer's primary colour, feed it into Bootstrap along with my application's other variables and store the resulting CSS file. I'm also telling scssphp
that I want the output to be crunched so that comments are removed and the output is minified.
Deciding when to compile
So now you have a nice method for compiling a theme for a customer, but when should you call this method? I like to recompile any time the customer model is saved - whether a new customer is created or an existing one is updated. Something like the following in your customer model should do the trick:
protected static function boot()
{
parent::boot();
static::saved(function($customer) {
$customer->compileTheme();
});
}
This is nice, as any time the brand_primary
attribute on the model is changed, the theme file will be regenerated. But what if you make changes to the resources/assets/_variables.scss
or any other file you are loading in? These changes will not be reflected in your customer stylesheets until the next time the customer model is saved. That won't do - wouldn't it be nice if there was a simple way to recompile all customer styles in one swift go? Sounds like a good use case for an Artisan command to me. Run the following:
$ php artisan make:command CompileThemes
Change the $signature
attribute to compile:themes
, and modify the handle
method to the following:
use App\Customer;
// ...
public function handle()
{
$this->info("Compiling customer themes.");
$customers = Customer::all();
$bar = $this->output->createProgressBar(count($customers));
foreach($customers as $customer) {
$customer->compileTheme();
$bar->advance();
}
$bar->finish();
$bar->clear();
$this->info("Customer themes compiled successfully.");
}
If you're using Laravel 5.5, you don't even need to register the command anywhere, it will automatically be picked up for you. So now to recompile all customer themes, all you have to do is run:
$ php artisan compile:themes
Beautiful, eh?
Watch it!
So all of this is great, but the next pain in the ass you'll encounter is that when you're working on the styling of your application you'll be running your shiny new Artisan command over and over again. If only there was a way to automatically run it every time your Sass code is recompiled when you run the Laravel Mix watch command npm run watch
. I think you know where this is going...
To do this, we're going to need to merge in some custom Webpack configuration with Laravel Mix. We'll also need to bring in a Webpack plugin for calling shell commands. If that sounds complicated, it really isn't! First, grab webpack-shell-plugin
from npm and add it as a dev dependency:
# If you're using npm
$ npm install --save-dev webpack-shell-plugin
# If you're using yarn
$ yarn add -D webpack-shell-plugin
Next, open up the webpack.mix.js
file in the root of your project and change it so it looks like the following:
const WebpackShellPlugin = require('webpack-shell-plugin');
let mix = require('laravel-mix');
mix.webpackConfig({
plugins: [
new WebpackShellPlugin({
onBuildExit:['php artisan compile:themes']
})
],
})
.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');
This essentially tells Mix to run the command php artisan compile:themes
every time it finished compiling a build.
Now run npm run watch
and you'll see the notifications and progress bar about customer themes being compiled, and you'll see it again any time you modify a Sass file in your project.
Are we there yet?
One more thing... While we now have a great workflow for while we're working on our project's Sass in development, we still have a problem when it comes to deploying those changes to production. When we compile our customer themes locally, it's only doing so for whatever customers exist in our local development database. How do we ensure that the customers in our production database have their stylesheets updated every time we deploy a new stylesheet to the server? The simplest way is to run php artisan compile:themes
as part of your deployment script, much like you would run migrations or other commands.
If you're using bootstrap-sass
or any other npm module as part of your Sass code, you'll need to ensure that this gets installed on your server when your application is deployed. By default bootstrap-sass
is listed in the devDependencies
block in the package.json
file in the root of your Laravel project. Move this to the dependencies
block (create it if it doesn't exist). Now in your deployment script, make sure you run the command npm install --production
prior to running the php artisan compile:theme
command, so the npm dependencies get installed first.
Using Laravel Forge? The following is a slightly modified version of the default deployment script:
cd /home/forge/yourapp.com
git pull origin master
composer install --no-interaction --prefer-dist --optimize-autoloader
npm install --production
echo "" | sudo -S service php7.1-fpm reload
if [ -f artisan ]
then
php artisan migrate --force
php artisan compile:themes
fi
Depending on the size of your stylesheets and how many customers you have, the scssphp
compilation process could end up being resource intensive - so you should also strongly consider compiling in a queued job rather than synchronously.
Summary
So there you have it. A very straightforward (well, fairly straightforward) approach to providing your customers with a way to customise the appearance of your Laravel app simply by selecting a colour from a colour picker.