OpenBuildings / asset-merger

Managing and merging Assets
MIT License
72 stars 31 forks source link

Some improvements - Auto load assets js/styles with priority and without doubles - Kohana 3.3.1 #26

Open Vadorequest opened 10 years ago

Vadorequest commented 10 years ago

Hi.

I ran into some issues yesterday so I figured out how deal with it. Because I'm lazy and I don't want to import manually each file, I wrote a little Kohana 3.3.1 Assets helper.

What does it do?

echo Assets::factory(App::get_theme())
    // Load files that must be required in first.
    ->js("jquery/jquery.min.js")
    ->js("jquery/jquery-ui-1.10.4.custom.min.js")
    ->js("plupload/plupload.js")
    ->js("_ie/excanvas.min.js", array('condition' => 'lte IE 8'))

    ->load_assets()// Load assets recursively
    ->load_theme_assets()// Load theme assets recursively. (my app specific)
    ->render();

How to use it?

Assuming asset-merger is already configured

  1. Include classes/Asset.php (See below, dependency)
  2. Include classes/File.php (See below, dependency)
  3. Include classes/Assets.php (See below)
  4. Replace the variables such as App::$assets_directory, App::$styles_directory, App::$js_directory
  5. Potentially remove the methods load_theme_assets and load_views_styles that are really strongly linked to my app, but are good example "how to", if you use something close in your app, anyway you can chose to not use them.
  6. Configure the allowed extensions and other options. (I don't know if .coffee should be used)
  7. Use the ->load_assets() method should import all assets that are not already loaded. Don't forget that some lb should be still required "by hand" before, such as jquery/jquery-ui.
  8. Enjoy!

    Rendered HTML

<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/jquery/jquery.min.js?1394744970"></script><style type="text/css"></style>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/jquery/jquery-ui-1.10.4.custom.min.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/plupload/plupload.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/bootstrap/bootstrap.min.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/charts/highstock.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/date.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/jquery/jquery-jtable.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/jquery/jquery-validationEngine-en.js?1396087455"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/jquery/jquery-validationEngine-fr.js?1396087455"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/jquery/jquery-validationEngine.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/plupload/fr.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/plupload/plupload-jquery-queue.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/plupload/plupload-jquery-ui.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/plupload/plupload.html5.js?1394744970"></script>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/sha256.js?1394744970"></script>
<link type="text/css" href="/my-app/application\cache\assets\themes\default/css/bootstrap/bootstrap-dataTables.css?1396087454" rel="stylesheet">
<link type="text/css" href="/my-app/application\cache\assets\themes\default/css/jquery/jquery-ui-1.10.4.custom.min.css?1396087454" rel="stylesheet">
<link type="text/css" href="/my-app/application\cache\assets\themes\default/css/jquery/validationEngine.css?1396087454" rel="stylesheet">
<link type="text/css" href="/my-app/application\cache\assets\themes\default/css/../themes/default/styles/bootstrap/bootstrap.css?1396087454" rel="stylesheet">
<!--[if lte IE 8]>
<script type="text/javascript" src="/my-app/application\cache\assets\themes\default/js/_ie/excanvas.min.js?1396087454"></script>

Asset-merger config

This is the config file I used, I use themes, (app specific) but you just have to change the config file to cache the file without use this stuff. config/asset-merger.php

<?php defined('SYSPATH') or die('No direct access allowed.');

return array(
    'merge' => App::PRODUCTION,
    'folder' => App::config('app.app_dir') . DIRECTORY_SEPARATOR . App::config('cache.cache_dir') . DIRECTORY_SEPARATOR  . App::$assets_directory . DIRECTORY_SEPARATOR . App::$themes_directory . DIRECTORY_SEPARATOR . App::get_theme(),
    "load_paths" => array(
        Assets::JAVASCRIPT => DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$js_directory . DIRECTORY_SEPARATOR,
        Assets::STYLESHEET => DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$styles_directory . DIRECTORY_SEPARATOR,
    ),
    'processor' => array(
        Assets::STYLESHEET => 'cssmin'
    )
);

Source code - Dependencies/helpers

Of course, because this script is strongly linked to my app, there are some dependencies.

App.php


class App extends Kohana {

    /**
     * Theme in use. 
     * Updated by Controller_Template->_initialize_template() on each HTTP request.
     * @var string
     */
    public static $current_theme = 'default';

    /**
     * Assets directory name.
     * @var string
     */
    public static $assets_directory = 'assets';

    /**
     * Upload directory name. Directory used for upload all images. 
     * MUST be in assets directory.
     * @var string
     */
    public static $uploads_directory = 'uploads';

    /**
     * Theme directory name.
     * @var string
     */
    public static $themes_directory = 'themes';

    /**
     * Image directory name.
     * @var string
     */
    public static $img_directory = 'images';

    /**
     * Javascript directory name.
     * @var string
     */
    public static $js_directory = 'js';

    /**
     * Styles directory name.
     * @var string
     */
    public static $styles_directory = 'styles';

    /**
     * Less directory name.
     * @var string
     */
    public static $less_directory = 'less';

    // More but not useful here!
}

I had to make visible the protected $_file from the Kohana_Asset class, so I overloaded it.

classes/Asset.php

<?php defined('SYSPATH') OR die('No direct script access.');

    class Asset extends Kohana_Asset {
        /**
         * Return the filename.
         *
         * @return string
         */
        public function file(){
            return $this->_file;
        }
    }
?>

I also use a custom function to read all files recursively, to do so I overloaded the Kohana_File class.

classes/File.php

<?php defined('SYSPATH') OR die('No direct script access.');
class File extends Kohana_File {

    /**
     * Read all files in all subdirectories for a directory.
     *
     * @param $dir
     *
     * @return array
     * @see http://www.php.net/manual/fr/function.scandir.php
     */
    public static function scandir_recursive($dir)
    {
        $root = scandir($dir);
        foreach($root as $value)
        {
            if($value === '.' || $value === '..') {continue;}
            if(is_file("$dir/$value")) {$result[]="$dir/$value";continue;}
            foreach(File::scandir_recursive("$dir/$value") as $value)
            {
                $result[]=$value;
            }
        }
        return $result;
    }
}

Finally, the big script:

classes/Assets.php

<?php defined('SYSPATH') OR die('No direct script access.');

    class Assets extends Kohana_Assets {

        /**
         * Array of default options to use when load css files.
         * @var array
         */
        public static $default_css_options = array('processor' => 'cssmin');

        /**
         * Array of default options to use when load javascript files.
         * @var array
         */
        public static $default_js_options = array('processor' => 'jsmin');

        /**
         * All file or dir that start by this pattern won't be auto loaded.
         * One character only.
         * @var string
         */
        public static $pattern_autoload_disable = '_';

        /**
         * Allowed file extensions.
         * @var array
         */
        public static $allowed_ext = array(
            Assets::JAVASCRIPT => array('js', 'coffee'),
            Assets::STYLESHEET => array('css', 'less', 'scss', 'sass'),
        );

        /**
         * Directory that contains the style by views.
         * @var string
         */
        public static $views_dir = '_views';

        /**
         * Path to use to go from styles directory to the theme directory.
         * @var string
         */
        public static $path_to_themes_from_styles = '../';

        /**
         * *************************************************************
         * ********************* Override ******************************
         * *************************************************************
         */

        /**
         * Add stylesheet
         *
         * @param   string  $file
         * @param   array   $options
         * @return  Assets
         */
        public function css($file, array $options = array())
        {
            return parent::css($file, array_merge(Assets::$default_css_options, $options));
        }

        /**
         * Add javascript
         *
         * @param   string  $file
         * @param   array   $options
         * @return  Assets
         */
        public function js($file, array $options = array())
        {
            return parent::js($file, array_merge(Assets::$default_js_options, $options));
        }

        /**
         * *************************************************************
         * ********************* App specific helpers ******************
         * *************************************************************
         */

        /**
         * Load all the theme assets for the current theme used.
         *
         * @return Assets
         */
        public function load_theme_assets(){
            $style_paths = DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$themes_directory . '/' .App::get_theme() . '/' . App::$styles_directory;
            $js_paths = DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$themes_directory . '/' .App::get_theme() . '/' . App::$js_directory;

            if(is_dir($style_paths)){
                Assets::load_styles($this, Assets::_clean_file_names(File::scandir_recursive($style_paths), DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR, Assets::STYLESHEET), Assets::$path_to_themes_from_styles);
            }
            if(is_dir($js_paths)){
                Assets::load_javascripts($this, Assets::_clean_file_names(File::scandir_recursive($js_paths), DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR, Assets::JAVASCRIPT), Assets::$path_to_themes_from_styles);
            }

            return $this;
        }

        /**
         * Load the view style with the same name than the current controller used.
         *
         * @param string $controller - Controller name. First letter should be lower case.
         *
         * @return Assets
         */
        public function load_views_styles($controller){
            $style_paths = DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$themes_directory . '/' .App::get_theme() . '/' . App::$styles_directory . '/' . Assets::$views_dir;

            if(is_dir($style_paths)){
                $files = Assets::_clean_file_names(File::scandir_recursive($style_paths), DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR, Assets::STYLESHEET, false);

                // Could be null.
                if($files){
                    foreach($files as $file){
                        // Try to load the file that has the controller name.
                        if(is_file(DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . $file)){
                            // Remove the path until the view dir to keep only what's left.
                            $filename = explode(Assets::$views_dir . '/', $file)[1];

                            if(File::remove_all_ext($filename) == $controller){
                                // A file that
                                Assets::load_style($this, Assets::$path_to_themes_from_styles . $file);
                            }
                        }
                    }
                }
            }

            return $this;
        }

        /**
         * *************************************************************
         * ********************* Helpers *******************************
         * *************************************************************
         */

        /**
         * Load all the js/style assets.
         * They are basically inside the assets folder, don't load assets in sub folders. (Such as themes)
         *
         * @return Assets
         */
        public function load_assets(){
            $style_paths = DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$styles_directory;
            $js_paths = DOCROOT . App::$assets_directory . DIRECTORY_SEPARATOR . App::$js_directory;

            if(is_dir($style_paths)){
                Assets::load_styles($this, Assets::_clean_file_names(File::scandir_recursive($style_paths), $style_paths . '/', Assets::STYLESHEET));
            }
            if(is_dir($js_paths)){
                Assets::load_javascripts($this, Assets::_clean_file_names(File::scandir_recursive($js_paths), $js_paths . '/', Assets::JAVASCRIPT));
            }

            return $this;
        }

        /**
         * Load an array of style files.
         *
         * @param Assets $assets
         * @param array $styles
         * @param string $before
         *
         * @return Assets
         */
        public static function load_styles(Assets $assets, array $styles, $before = ''){
            foreach($styles as $style){
                Assets::load_style($assets, $before . $style);
            }

            return $assets;
        }

        /**
         * Load a style file.
         *
         * @param Assets $assets
         * @param $file
         *
         * @return Assets
         */
        public static function load_style(Assets $assets, $file){
            if(Assets::is_unique($assets, $file, Assets::STYLESHEET)){
                return $assets->css($file, Assets::$default_css_options);
            }
            return $assets;
        }

        /**
         * Load an array of javascript files.
         *
         * @param Assets $assets
         * @param array $javascripts
         * @param string $before
         *
         * @return Assets
         */
        public static function load_javascripts(Assets $assets, array $javascripts, $before = ''){
            foreach($javascripts as $js){
                Assets::load_js($assets, $before . $js);
            }

            return $assets;
        }

        /**
         * Load a js file.
         *
         * @param Assets $assets
         * @param $file
         *
         * @return Assets
         */
        public static function load_js(Assets $assets, $file){
            if(Assets::is_unique($assets, $file, Assets::JAVASCRIPT)){
                return $assets->js($file, Assets::$default_css_options);
            }
            return $assets;
        }

        /**
         * Check that the file isn't already loaded.
         *
         * @param Assets $assets
         * @param $file
         * @param $group
         *
         * @return bool
         */
        private static function is_unique(Assets $assets, $file, $group){
            $collection = $assets->_groups[$group];
            $loaded_assets = $collection->assets();

            foreach($loaded_assets as $loaded_asset){
                if($loaded_asset->file() === $file){
                    return false;
                }
            }
            return true;
        }

        /**
         * Clean file names to be usable by the asset-merger module.
         * Check that the file should be loaded or not. (extension, etc.)
         * Clean by creating a new array and add entries only if they pass the validation.
         *
         * @param $files
         * @param $path_to_clean
         * @param $group
         * @param $check_autoload
         * @param $check_extension
         *
         * @return array
         */
        private static function _clean_file_names(array $files, $path_to_clean, $group, $check_autoload = true, $check_extension = true){
            $files_cleaned = array();

            foreach($files as $file){
                if($file_cleaned = Assets::_clean_file_name($file, $path_to_clean, $group, $check_autoload, $check_extension)){
                    $files_cleaned[] = $file_cleaned;
                }
            }
            return $files_cleaned;
        }

        /**
         * Clean a file name and returns the cleaned name.
         * Returns null if the file doesn't pass the validation.
         *
         * @param $file
         * @param $path_to_clean
         * @param $group
         * @param bool $check_autoload
         * @param bool $check_extension
         *
         * @return mixed
         */
        private static function _clean_file_name($file, $path_to_clean, $group, $check_autoload = true, $check_extension = true){
            // Filter results by deleting all entries that do not match our allowed extensions.
            if(Assets::_check_file_name($file, $group, $check_autoload, $check_extension)){
                return str_replace($path_to_clean, '', $file);
            }
        }

        /**
         * Check that an asset file should be loaded.
         * Check pattern to don't auto load.
         * Check file extension.
         *
         * @param $file
         * @param $group
         * @param $check_autoload
         * @param $check_extension
         *
         * @return bool
         */
        private static function _check_file_name($file, $group, $check_autoload = true, $check_extension = true){
            $dir_and_files = explode("/", $file);

            foreach($dir_and_files as $name){
                // Should be auto loadable.
                if($check_autoload && $name[0] === Assets::$pattern_autoload_disable){
                    return false;
                }

                // Should contain an allowed extension.
                if($check_extension && !in_array(pathinfo($file, PATHINFO_EXTENSION), Assets::$allowed_ext[$group])){
                    return false;
                }
            }

            return true;
        }
    }
?>

What next?

I don't know, I never built any Kohana module, I think that if what I did is cool, then it should be included in the asset-merger module, but in accordance with the kohana way. (I.e: Write configuration inside the config/asset-merger.php, etc.)

Let me know!