LESS > CSS/PHP

Eliminating legacy code and modernizing large CSS codebases.

It’s the small things.

At SmugMug our technology stack consists mainly of PHP for our backend, and YUI and React in our front-end code. Over the years we’ve seen technologies come and go, and some of that is still visible in the codebase. For example, there are small areas of the code that have some dependency on YUI 2, while others are written using modern React code bundled using ES6 syntax. When dealing with larger, older codebases like ours, you accumulate certain amounts of technical debt. This is not unique to us but simply the nature of the beast.

When technical debt like this is isolated, it can be reduced, refactored, and eventually replaced or removed. If they’re not isolated, this technical debt starts to become tricky to deal with.

Until recently we had one bit of technical debt that was the complete opposite of isolated: it was everywhere! Our CSS.

There’s something terribly wrong in that code. What are those PHP tags doing there?!

Customization and SmugMug.

Before we dig in, here’s a bit of background as to why we had CSS with PHP tags. SmugMug allows our customers to personalize their sites to their hearts’ content. We provide them with prebuilt interactive Content Blocks to showcase their beautiful photos for others to see and purchase. They also have the ability to choose their themes, which include fonts, color schemes, opacities, and more.

Themes have 5 main properties: primary color, accent color, text color, header font and body font. Since the majority of the PHP usage is around Colors, let’s focus on those.

Theme Editor Window
Our Theme Editor showing default theme settings.

Theme colors are used to generate a color palette of 32 colors for each color type. This spectrum of colors lets us to work with all kinds of color combinations, allowing us to create customized pages and interactive Content BlocksĀ that match the selected theme.

color2bpalette2bflipped

As developers-with guidance from our designers-we select one of the 32 colors to use in our CSS for the different parts of our site, like our Lightbox, Galleries, and Content Blocks. The algorithmic nature of the color palette guarantees that, regardless of the selected theme, our customers’ sites will look their best. This level of customization has been a key feature of SmugMug since early 2004.

screenshot2b-2bblog2bpost

Screenshot of old cobranding examples or interfaces (from archive.org).

How it’s done.

In order to generate CSS code with dynamic color values you need some sort of variable mechanism to substitute values at run time. So naturally, this being a PHP codebase, we added PHP to our CSS! This allowed our CSS to have variables with values that could change on the fly.

Why didn’t we use SCSS, SASS, LESS, or any of the countless other tools available to developers?

Simple. We didn’t have a choice! Back in 2004, when the first signs of this feature were appearing, the set of CSS tools was much more limited. In fact, none of the preprocessors in use today existed. SASS didn’t arrive until two years later. As you can imagine, having PHP in our CSS has caused more than a couple of headaches in the past, sometimes holding us back.

For example, in order to enable CSS minification, we had a workaround where we commented out PHP tags with multiline comments, minified the CSS and undid the comment process to bring PHP tags back. The fact that this worked is half a miracle. This was not a sustainable way forward.

We decided to investigate the tools available now that would be up to the task, but first we needed to understand how our themes were defined and built.

Finding a solution

Of the two most popular tools (SASS and LESS) that could serve our needs, we decided to go with LESS. Both tools could work equally well, but LESS had one minor advantage during the experimentation phase that sealed it as our choice: the inline import.

For those unfamiliar with LESS, the inline import directive allows you to instruct the compiler to treat a certain file as valid CSS regardless of its contents. The contents of this file are placed verbatim into the resulting final CSS produced by the LESS compiler.

This import directive would allow us to do something like

While this in-between state is less than ideal, it would allow us to transition the codebase in small chunks. Whereas modules that are converted to LESS can no longer contain PHP. As people worked on older CSS, we could encourage developers to move the code over to LESS.

With this tool at hand, a proof of concept was built and presented at our quarterly hackathon. This inline import directive allowed us to skip most of the hard tedious work and focus on the initial challenge: themes and theme colors.

PHP + LESS to the rescue.

For any non-theme CSS code, we can use the standard LESS compiler as part of our regular build process. To help manage some repeated code like media breakpoints and some internal skin code, we created a global variables.less. This file is imported by all bundles and provides a universal library of useful variables and mixins. For those that did depend on themes, we engineered a solution for compiling LESS code within PHP. A LESS-PHP bridge.

LESS-PHP bridge

Leveraging a PHP library capable of compiling LESS code, we created a mechanism for generating color variables on the fly. Using a simple template LESS file, we can build a dynamic set of variables for each color and its unique value.

This file can then be appended to the end of any theme-able bundles. Due to CSS/LESS precedence rules, the last declaration of a value takes precedence allowing a global override of any variables; in this case, skin-color values.

With the bridge complete and the ability to inline import CSS files using PHP without causing issues, we had a way forward. At the end of the hackathon, we were able to serve some pages with a mix of LESS and CSS/PHP.

Taking the plunge.

The demo of a working proof of concept sparked a bit of conversation among the engineering team, and a deeper look was required. Upon closer inspection, one of our engineering leads noticed a pattern in all our PHP in use. This pattern allowed us to easily script a full transition to LESS without the intermediate inline step. In four days we’d developed a quick Python script that could convert about 90% of all our usage of PHP in CSS. The other 10% was either dead legacy code that could be purged (some IE6 workarounds) or easy enough to change by hand.

In the end it took about six days of scripting, manual fixes and developer validation to get all our CSS/PHP in LESS files. Not only did the scripts eliminate a large amount of tedious work, it also made it possible to eliminate a large set of possible errors. By leveraging automation we could guarantee that if the transition script worked correctly in one case, it would always work the same way elsewhere. All these benefits, with nothing more than a set of simple regular expressions.

Still, in an abundance of caution, we decided to do a full regression test of the site and functionality related to themes. This process revealed some smaller issues that were missed by the manual diff inspections, but overall no major issues surfaced during this test. For a change consisting of over 25k lines of CSS, this was a nice surprise.

Lessons learned.

For the longest time this had been a part of the code that engineers thought would not be worth the huge effort needed to refactor and update. But in a short amount of time it went from being a small annoyance to a junior engineer to a successful refactoring effort that improved and modernized our codebase. While there were plenty of bumps in the road, the amount of effort required to do this transition went from months to days, all thanks to some clever thinking and scripting. Just goes to show large projects can sometimes be simplified with the tools we use every day.

Looking to the future.

We’re very happy with LESS and the benefits it has brought us, and we’re looking to take advantage of its features in the future, like plug-ins for browser pre-fixing. There’s also room for performance improvements in our themes to provide faster live previews.

Did any of that post get you thinking about a better or different way we could have done this? If any of it sounded interesting to you, please drop us a line at jobs@smugmug.com. We’re always looking for talented engineers interested in solving problems like these.

You shall not commit! (Without passing tests)

Welcome to the SmugMug engineering blog! Here at SmugMug we refer to our engineers as ‘Sorcerers’ and this blog will be all about the magic behind smugmug.com. Our engineering landscape is as vast and varied as Middle Earth so we’ll start with something everyone’s familiar with: committing code and testing.

At SmugMug our SCM tool is Bazaar (soon to be Git) and the unit test framework we fancy is PHPUnit since our website codebase is written in PHP. Running unit tests before committing is always a Good Thing for developers to do, but sometimes we forget and can commit code with broken tests. This is where a pre-commit hook comes in handy.

A pre-commit hook is a piece of code/script you tell your SCM tool to run after you type `bzr/git/svn commit` but before it actually commits your new code. A pre-commit hook can do just about anything, and with ours we run our unit tests and if they fail, we abort the commit.

The way we prevent ourselves from committing code with broken tests is by creating a short shell script that runs our unit tests via PHPUnit’s command line tool. Our script is named `precommit.sh’ and is pretty simple:

#!/bin/sh
phpunit --bootstrap include/tests/bootstrap.php include/tests/

That script runs from the root of our Bazaar repo, telling PHPUnit where our bootstrap file and tests are. The script will return 0 if all tests passed (in Unix 0 means the command succeeded). If tests fail the script will return something other than 0. Now that we have that working (and our tests pass :)), we can get Bazaar to run it before committing our code.

The way you get Bazaar to run a pre-commit hook is by writing a plugin that adds the hook to Bazaar every time you run `bzr commit`. Plugins are simply Python files placed in ~/.bazaar/plugins/[plugin-name].py . Bazaar executes them and the plugins can then do just about anything they please. Here’s our pre-commit hook plugin, which I’ll walk us through:

from bzrlib import branch

def check_tests(local_branch, master_branch, old_revision_number, old_revision_id, future_revision_number, future_revision_id, tree_delta, future_tree):
    import os,subprocess
    from bzrlib import errors

    #Only execute our script if it exists in the current directory
    if not os.path.exists("precommit.sh"):
        return

    #Run the pre-commit script
    tests_result = subprocess.call(os.path.abspath("precommit.sh"), shell=True)

    #If our script returns something other than 0, tests have failed
    if tests_result != 0:
        raise errors.BzrError("Tests failed, no soup for you!")

#Install the hook with Bazaar
branch.Branch.hooks.install_named_hook('pre_commit', check_tests, 'Run PHPUnit tests pre-commit hook')

Our plugin does the following:

  • Imports ‘branch’ from the Python module bzrlib.
  • Defines our function to run before committing new code. A pre-commit hook function accepts eight arguments, none of which we’ll need though. (For more info on hooks with Bazaar, read the hooks help page)
  • Checks for the existence of precommit.sh, which runs our tests. We do this because we could be working with multiple repos, some which may not have a pre-commit script to run.
  • If it does not exist we return and Bazaar will commit our code.
  • If it does exist we run precommit.sh via subprocess.call() and save the return code as ‘tests_result’.
  • If tests_result is is not 0, our tests have failed and we raise a Bazaar error, which will abort the commit.
  • Installs the pre-commit hook via branch.Branch.hooks.install_named_hook(). The first argument is where the hook should be run, the second is the function to run and the third is a name for the hook to be used in progress and error messages.

Here’s what it looks like for us if we commit code with broken tests:

With just a little bit of engineering we can have our own wizard preventing us from committing broken tests and code! Most other SCM tools have pre-commit hooks also (Git, SVN). Stay tuned for more posts about our architecture and engineering processes here at SmugMug.