MVC CSS Stylesheets

While working on redoing my website using a custom model-view-controller framework I decided that it was a good time to do some extra cleanup. After all, cleaning up the backend only changes the content portion of the site. There's much more to websites - styles, behaviors, cache control - then the content. In the interest of cleaning up my website I decided to take my MVC framework to the next level and use it to create my stylesheets and Javascript in real-time.

The primary benefit of MVC frameworks is to simplify code logic and remove duplicate code. With all user requests being transferred through the same routes and controllers, it's easy to whip out extra html pages or make site-wide changes by editing a few lines. Common SQL functions, AJAX calls, and classes can be used across different pages and sites easily without duplication.

There's a lot of duplicated code in my assets. I use Erik Meyer's reset stylesheet, a global set of style rules, jQuery, similar lightboxes, etc across my different subdomains. I could include each asset as a separate file, but that setup would create an increasingly long list of http requests, which is bad for website optimization. My adventure map, for example, would need at least 5 Javascript assets: Google maps settings, jQuery, lightbox, xml parser, and a primary script for special map-only functions. I needed to combine separate scripts with cross-site functionality which could be sent to the client as a single file in real-time.

The first step was decided what pieces of code can be broken up. To keep this post simple, I'll discuss my stylesheet approach only, which can be extended to handle the Javascript. For a single web site, I'll need a reset, global, and the site-specific stylesheet. The following bit of code is located in my controller - if you aren't using a model-view-controller setup, then this bit of code needs to happen before you send the head of the html document.

  1. Loader::model('assets');

  2. $css = Assets::css('reset','global','about');

Loader is a class that includes a file called asset, which is a model, and then sends some data to a static function in the Asset class. If you aren't using classes, then you would include_once the asset file, then send the data to a normal function called CSS. Next is a portion of my Asset class that handles the CSS.

  1. class Assets

  2. {

  3. static $meta = array();

  4. static function css()

  5. {

  6. foreach(func_get_args() as $file) {

  7. $files[$file] = filemtime(Config::get('public_root').'styles/pieces/'.$file.'.css');

  8. }

  9. $primary = self::getPrimary('styles') ? filemtime(Config::get('public_root').'styles/'.self::getPrimary('styles')) : -1;

  10. if(max($files)>$primary)

  11. {

  12. foreach($files as $file => $value)

  13. {

  14. $string .= file_get_contents(Config::get('public_root').'styles/pieces/'.$file.'.css');

  15. }

  16. $primary = self::getPrimary('styles');

  17. $version = preg_match('@-v([0-9]+).css@',$primary,$matches) ? $matches[1]+1 : 1;

  18. if($version!==1)

  19. unlink(Config::get('public_root').'styles/'.self::getPrimary('styles'));

  20. $fh = fopen(Config::get('public_root').'styles/'.Config::get('subdomain').'-v'.$version.'.css','w');

  21. fwrite($fh,$string);

  22. fclose($fh);

  23. return Config::get('subdomain').'-v'.$version.'.css';

  24. }

  25. else

  26. return self::getPrimary('styles');

  27. }

  28. }

There's a lot of static properties in there that may seem confusing for non-OOP programmers. All Config::get commands are pulling variables that I set before this class was initiated. Config::get('public_root') is just a string that contains the file system that leads to my public_html contents, for example. This class reads all the variables passed to it (through func_get_args), pulls the file modified times (through filemtime), and compares it to something called self::getPrimary('styles').

So, the individual arguments sent to this class are actually pieces of a stylesheet. The combined stylesheet's name is pulled from self::getPrimary('styles'), which I'll show next. If one of the individual pieces has been modified recently, then I loop through all of the pieces, pull and combine the content, and save it as a new main stylesheet. I'm changing the filename a bit for caching - each change is a brand new stylesheet, forcing the user to download the new set of styles instead of pulling an old cached one.

  1. private static function getPrimary($type)

  2. {

  3. if($handle = opendir(Config::get('public_root').$type.'/'))

  4. {

  5. while(false!==($file = readdir($handle)))

  6. {

  7. if($file!==('.'||'..'))

  8. $files[] = $file;

  9. }

  10. }

  11. foreach($files as $file)

  12. {

  13. if(preg_match('@'.Config::get('subdomain').'@',$file))

  14. return $file;

  15. }

  16. }

Note: the above class is included within the Assets class.

The getPrimary class only pulls the name of the main site's stylesheet. I named them after my subdomains (example: might be home-v3.css) to keep things simple.

So, why would you want to do this? After all, reading the contents of a file into a string with PHP isn't a quick process, and you usually want to serve up a site's assets without any holdup on a client's visit. However, there's something else I've been doing with my website lately: caching. By setting caching headers on your files, then it's harder to make sure the end user is seeing the most up-to-date styles. If a user visits your page ninety-nine times, it's best that they have your assets cached either on their machine or on a proxy server. When you change the styles and don't change the filename, the client's computer might not be able to sense the change and will ignore your newer file on their hundredth visit.

Creating a class to handle your assets will clean up your folder system, allow easier handling of cross-page and cross-site assets, and auto-create new files with new filenames only when it needs to be done. It's an elegant way of bringing object logic to your styles, and will make backend changes much easier to replicate through an entire site, or in my case, several sites.