1. 2016
    Nov
    14

    Adding LESS to a MVC Framework

    Posted By


    LESS is a CSS pre-processor which allows you to extend the CSS language, adding features that allow for variables, mixins and functions to make your CSS more maintainable and themeable.

    Less usually runs inside Node, in the browser and inside Rhino, meaning you have to run both Apache/Nignx and Node Servers to use it, but there is an alternative called PHPLESS that uses PHP to compile LESS making it easiler to comfortably use one server.

    How do I get PHPLESS

    You can download PHPLESS from 2 popular sources the leafo version and the oyejorge version. Both do a pretty good job of compiling LESS to CSS, but for this tutorial I have chosen to use the leafo version.

    Best practices when using LESS

    I recommend that you use your LESS compiler only in a Development Environment and not in a Production Environment. Your site can take a big performance hit while compiling at runtime and there is a good chance that that small LESS file will turn into a large CSS file, the CSS files can get very large especially for large sites.

    Adding LESS to an MVC Framework

    After downloading and unzipping PHPLESS you will notice it has it’s own library so all you really need to do it incorporate its functionalities into your framework. For this tutorial I am under the assumption that you already understand the basics concepts of MVC framework and how the framework interacts with external libraries, in this scenario I will be using CAKEPHP.

    There are 2 ways of doing about incorporating LESS into your application

    For its tutorial I will show you how to use both, each one has its advantages and disadvantages, so choose the one that best suits you.

    1) The first way is adding it to the controller so it compiles the LESS file before rendering the page.

    LessComponent.php

    
    
    /**
     * Less component
     *
     * Compiler Wrapper for LESS.
     * 
     * PHP versions 5
     *
     * 
     * @copyright	Copyright 2016
     * @link		http://leafo.net/lessphp/docs/			
     * @package		app
     * @subpackage	app.Controller.Component
     * @authors:
     *			 Emeka U Echeruo (emeka.echeruo@gmail.com)
     *
     * Allows to Compile Less through your controller
     * Usage:
     * 
     * public $components = array('less');
     
    App::import('Vendor', 'LessPhp/LessPhp');
    App::uses('Component', 'Controller');
    
    class LessComponent extends Component {
    
    /**
     * Controller for the request.
     *
     * @var object
     * @access protected
     */
    	protected $_controller = null;
    
    /**
     * A reference to less complier
     *
     * @var object
     * @access protected
     */
    	protected $_less = null;
    
    /**
     * Called before the Controller::beforeFilter().
     *
     * @param Controller $controller Controller with components to initialize
     * @return void
     * @access public
     */
    	public function initialize(Controller $controller)
    	{
    		$this->_controller = $controller;
    		$this->_less = new lessc;
    	}
    
    /**
     * Set a variable from PHP
     *
     * @param array $variables The variables
     * @return void
     * @access public
     */
    	public function setVariables($variables)
    	{
    		$this->_less->setVariables($variables);
    	}
    
    /**
     * Remove a PHP variable
     *
     * @param string $variable The variable name
     * @return void
     * @access public
     */
    	public function unsetVariable($name)
    	{
    		$this->_less->unsetVariable($name);
    	}
    	
    /**
     * Change how CSS output looks
     *
     * @param string $name Formatter name
     * @return void
     * @access public
     */
    	public function setFormatter($name)
    	{
    		$this->_less->setFormatter($name);
    	}	
    
    /**
     * Add a custom function
     *
     * @param string $name The name
     * @param mixed $func Callable function 
     * @return void
     * @access public
     */
    	public function registerFunction($name, $func)
    	{
    		$this->_less->registerFunction($name, $func);
    	}	
    
    /**
     * Remove a registered function
     *
     * @param string $name The name
     * @return void
     * @access public
     */
    	public function unregisterFunction($name) 
    	{
    		$this->_less->unregisterFunction($name);
    	}	
    
    /**
     * Set the search path for imports
     *
     * @param mixed $dirs The path
     * @return void
     * @access public
     */
    	public function setImportDir($dirs)
    	{
    		$this->_less->setImportDir($dirs);
    	}
    
    /**
     * Append directory to search path for imports
     *
     * @param array $dir The path
     * @return void
     * @access public
     */
    	public function addImportDir($dir)
    	{
    		$this->_less->addImportDir($dir);
    	}
    
    /**
     * Compile a string.
     *
     * @param string $string
     * @param string $name 
     * @return string $out Buffered string
     * @access public
     */
    	public function compile($string, $name = null) 
    	{
    		return $this->_less->compile($string, $name);		
    	}
    
    /**
     * Compile a file to another or return it.
     *
     * @param string $fname Path to the file where to read the data
     * @param string $outFname Path to the file where to write the data
     * @return string $out This function returns the number of bytes that were written to the file, or FALSE on failure.
     * @access public
     */
    	public function compileFile($fname, $outFname = null)
    	{
    		return $this->_less->compileFile($fname, $outFname);		
    	}
    
    /**
     * Compile a file only if it’s newer
     *
     * @param string $in Path to the file where to read the data
     * @param string $out Path to the file where to write the data
     * @return boolean returns TRUE if file is complied
     * @access public
     */
    	public function checkedCompile($in, $out)
    	{
    		return $this->_less->checkedCompile($in, $out);		
    	}
    
    /**
     * Conditionally compile while tracking imports
     *
     * @param string $in Input string
     * @param boolean $force Switch to force rebuild
     * @return array Lessphp cache structure
     * @access public
     */
    	public function cachedCompile($in, $force = false)
    	{
    		return $this->_less->cachedCompile($in, $force);
    	}
    		
    }
    
    

    PagesController.php

    
    
    class PagesController extends AppController {
    
    /**
     * This controller uses these components
     *
     * @var array
     * @access public
     */
    	public $components = array('Less');
    
    /**
     * This controller uses these helpers
     *
     * @var array
     * @access public
     */
    	public $helpers = array('Less');
    
    /**
     * This controller does not use a model
     *
     * @var array
     */
    	public $uses = array();
    
    /**
     * Called before the controller action.
     *
     * @access public
     */
    	public function beforeFilter()
    	{
    		
    		$dir = new Folder();
    		$lessFile = new File(CSS . 'global.less');
    		$cssFile = new File(CSS . 'global.css');
    		if($lessFile->exists() && (!$cssFile->exists() || ($lessFile->lastChange() > $cssFile->lastChange()))) {
    			if(!$lessFile->writable()) {
    				$dir->chmod(CSS, 0777, true);					
    			}
    			$this->Less->compileFile($lessFile->path, $cssFile->path);
    		}
    		parent::beforeRender();
    	}
      
    /**
     * Displays a view
     *
     * @return void
     * @throws NotFoundException When the view file could not be found
     *	or MissingViewException in debug mode.
     */
    	public function display() {
    		$path = func_get_args();
    
    		$count = count($path);
    		if (!$count) {
    			return $this->redirect('/');
    		}
    		$page = $subpage = $title_for_layout = null;
    
    		if (!empty($path[0])) {
    			$page = $path[0];
    		}
    		if (!empty($path[1])) {
    			$subpage = $path[1];
    		}
    		if (!empty($path[$count - 1])) {
    			$title_for_layout = Inflector::humanize($path[$count - 1]);
    		}
    		$this->set(compact('page', 'subpage', 'title_for_layout'));
    
    		try {
    			$this->render(implode('/', $path));
    		} catch (MissingViewException $e) {
    			if (Configure::read('debug')) {
    				throw $e;
    			}
    			throw new NotFoundException();
    		}
    	}
    }
    
    

    2) The second way is adding it to a Helper so it compiles and renders its output as the page is rendering.

    LessHelper.php

    
    
    /**
     * Less Helper class file
     *
     * 
     * PHP versions 5
     *
     * @copyright     Copyright 2016
     * @link
     * @package       app
     * @subpackage    app.View.Helper
     * @authors:
     *			 Emeka U Echeruo (emeka.echeruo@gmail.com)
     */
     
    App::import('Vendor', 'LessPhp/LessPhp'); 
    App::uses('Folder', 'Utility');
    App::uses('File', 'Utility');
    App::uses('HtmlHelper', 'View/Helper');
    App::uses('CakeResponse', 'Network'); 
    
    class LessHelper extends HtmlHelper {
    
    /**
     * Reference to the Response object
     *
     * @var CakeResponse
     */
    	public $response;
    	
    /**
     * A reference to less complier
     *
     * @var object
     * @access protected
     */
    	protected $_less = null;
    	
    /**
     * Constructor
     *
     * @param View $View The View this helper is being attached to.
     * @param array $settings Configuration settings for the helper.
     */
    	public function __construct(View $View, $settings = array()) {
    		parent::__construct($View, $settings);
    		$this->_less = new lessc;
    		
    		if (is_object($this->_View->response)) {
    			$this->response = $this->_View->response;
    		} else {
    			$this->response = new CakeResponse();
    		}
    		if (!empty($settings['configFile'])) {
    			$this->loadConfig($settings['configFile']);
    		}
    		
    	}
    
    /**
     * Creates a link element for CSS stylesheets.
     *
     * @param string|array $path The name of a CSS style sheet or an array containing names of
     *   CSS stylesheets. If `$path` is prefixed with '/', the path will be relative to the webroot
     *   of your application. Otherwise, the path will be relative to your CSS path, usually webroot/css.
     * @param array $options Array of options and HTML arguments.
     * @return string CSS `<link />` or `<style />` tag, depending on the type of link.
     */
    	public function css($path, $options = array()) {
    		if (!is_array($options)) {
    			$rel = $options;
    			$options = array();
    			if ($rel) {
    				$options['rel'] = $rel;
    			}
    			if (func_num_args() > 2) {
    				$options = func_get_arg(2) + $options;
    			}
    			unset($rel);
    		}
    
    		$options += array(
    			'block' => null,
    			'inline' => true,
    			'once' => false,
    			'rel' => 'stylesheet'
    		);
    		if (!$options['inline'] && empty($options['block'])) {
    			$options['block'] = __FUNCTION__;
    		}
    		unset($options['inline']);
    
    		if (is_array($path)) {
    			$out = '';
    			foreach ($path as $i) {
    				$out .= "\n\t" . $this->css($i, $options);
    			}
    			if (empty($options['block'])) {
    				return $out . "\n";
    			}
    			return '';
    		}
    
    		if ($options['once'] && isset($this->_includedAssets[__METHOD__][$path])) {
    			return '';
    		}
    		unset($options['once']);
    		$this->_includedAssets[__METHOD__][$path] = true;
    
    		if (strpos($path, '//') !== false) {
    			
    			$path = basename($path, '.less');
    			$this->compileFile(CSS . $path . '.less', CSS . $path . '.css');
    			$url = $this->assetUrl($path, $options + array('pathPrefix' => Configure::read('App.cssBaseUrl'), 'ext' => '.css'));
    			
    		} else {
    			
    			$this->compileFile(CSS . $path . '.less', CSS . $path . '.css');
    			$url = $this->assetUrl($path, $options + array('pathPrefix' => Configure::read('App.cssBaseUrl'), 'ext' => '.css'));
    			$options = array_diff_key($options, array('fullBase' => null, 'pathPrefix' => null));
    			if (Configure::read('Asset.filter.css')) {
    				$pos = strpos($url, Configure::read('App.cssBaseUrl'));
    				if ($pos !== false) {
    					$url = substr($url, 0, $pos) . 'ccss/' . substr($url, $pos + strlen(Configure::read('App.cssBaseUrl')));
    				}
    			}
    			
    		}
    
    		if ($options['rel'] === 'import') {
    			$out = sprintf(
    				$this->_tags['style'],
    				$this->_parseAttributes($options, array('rel', 'block')),
    				'@import url(' . $url . ');'
    			);
    		} else {
    			$out = sprintf(
    				$this->_tags['css'],
    				$options['rel'],
    				$url,
    				$this->_parseAttributes($options, array('rel', 'block'))
    			);
    		}
    
    		if (empty($options['block'])) {
    			return $out;
    		}
    		$this->_View->append($options['block'], $out);
    	}	
    	
    /**
     * Compile a file to another or return it.
     *
     * @param string $fname Path to the file where to read the data
     * @param string $outFname Path to the file where to write the data
     * @return string $out This function returns the number of bytes that were written to the file, or FALSE on failure.
     * @access public
     */
    	public function compileFile($fname, $outFname)
    	{
    
    		$dir = new Folder();
    		$lessFile = new File($fname);
    		$cssFile = new File($outFname);
    		if($lessFile->exists() && (!$cssFile->exists() || ($lessFile->lastChange() > $cssFile->lastChange()))) {
    			if(!$lessFile->writable()) {
    				$dir->chmod(CSS, 0777, true);					
    			}
    			$this->_less->compileFile($lessFile->path, $cssFile->path);
    		}
    		
    	}
    	
    }	
     	
    

    home.ctp

    
    
    /**
     * Home Page
     *
     * CakePHP 2.x with Bootstrap 3
     *
     * @copyright     Copyright 2016
     * @link          
     * @package       app.View.Pages
     * @since         
     * @license       
     * @authors:
     *			 Emeka U Echeruo (emeka.echeruo@gmail.com)
     */
    
    	//meta headers
    	echo $this->Html->meta(array('http-equiv' => 'X-UA-Compatible', 'content' => 'IE=edge'), null, array('inline' => false));
    	echo $this->Html->meta(array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1'), null, array('inline' => false));
    
     	// external stylesheets
    	echo $this->Html->css(array('bootstrap.min'), null, array('inline' => false, 'media' => 'screen,projection'));
    	
    	// external Less
    	echo $this->Less->css(array('global'), null, array('inline' => false, 'media' => 'screen,projection'));
    
    	// external javascript
    	echo $this->Html->script(array('jquery' . DS .'jquery-3.1.1.min', 'bootstrap.min'), array('inline' => false));
     
    
    

    Since it compiles the file before the page is rendered of advantage of using a LessComponent to Controller method is you can incorporate more functionality as opposed to using LessHelper to a view Method.


  2. About Emeka Echeruo

    Emeka Echeruo

    I love sports, football which I refuse to call soccer, and the outdoor especially walks in park. Software development is my passion, there is a beauty in creating something out of nothing but algebra that ends up becomes a part of a persons daily life. I love kids, dogs, nightlife and art because it finds you and moves you emotionally!

  3. Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.