Konrad Podgórski - Web Developer

Personal blog about developing web applications with Symfony, Node JS and Angular JS

A better way to work with assets in Symfony 2

I will explain how to work with assets in Symfony framework without having to use Assetic Bundle at all.

We will build a stack that will

  • download and prepare dependencies (jQuery, Bootstrap and Font Awesome icons)
  • merge and minify css and javascript files
  • copy necessary fonts (font-awesome) in the right place so the path in css is correct
  • automate deploy to S3 bucket which you can optionally convert to CDN

The process will be really fast and easy to understand even if you never used software listed here. However if you experience any problems do not hesitate to ask for help in comments. Post is quite long because it contain a lot of different configs but don't run away just yet. They are ready to copy & paste.

Also ... I'm working with Symfony here but you can use it for literally any other web framework.

Table of contents

  1. Introduction
  2. Prerequisites
    1. Installing NodeJS
    2. Installing Bower
    3. Installing GruntJS
  3. Use cases
    1. Scenario - download dependencies and copy them to /web/assets/* dir
    2. Scenario - download dependencies, copy and minify them
    3. Scenario - download, copy, concat (merge) and minify
    4. Deploying to S3/CDN - all above plus automated deploy to the CDN

Introduction

Why you would want to do it?

  • assets are generated on your computer once, so the server doesn't have to do anything (less software on server = the better)
  • if you are (or will be) using load balancer you definitely should (actually must) keep assets on CDN
  • one less bundle to load in your Symfony application (Assetic)

What we will use? NodeJS, Bower, Grunt JS and some grunt tasks

Before we begin please add these to your .gitignore file, you don't want to keep vendor libraries in your repository.

bower_components/
node_modules/

Install NodeJS

NodeJS is a runtime platform for applications written in javascript, it's required by Bower and GruntJs we will install in a moment.

Those who already have node js installed can go to the next step

How to install Node JS

Installing Bower

Bower is like a composer for frontend libraries. If you like composer you will definitely like Bower.

npm install -g bower

Create file bower.json, it will hold information about all required dependencies.

{
    "name": "symfony-application",
    "dependencies": {
        "jquery": "1.11.*",
        "bootstrap": "3.1.*",
        "font-awesome": "4.1.*"
    }
}

run bower install

bower install

Folder structure will look like this

.
├── bower_components
│   ├── bootstrap
│   ├── font-awesome
│   └── jquery
├── bower.json
└── .gitignore

Installing Grunt JS

GruntJS is our task runner, by adding a set of tasks created by Grunt community we can automate many tasks we used to do manually with a very little effort.

To install it we will once again use node package manager, npm:

npm install -g grunt-cli

Create file package.json, it will hold all dependencies for grunt tasks

{
  "name": "symfony-application",
  "version": "0.1.0"
}

Run the following commands (--save-dev option will add it to package.json)

npm install grunt --save-dev
npm install grunt-bowercopy --save-dev
npm install grunt-contrib-clean --save-dev
npm install grunt-contrib-concat --save-dev
npm install grunt-contrib-copy --save-dev
npm install grunt-contrib-cssmin --save-dev
npm install grunt-contrib-uglify --save-de
npm install grunt-contrib-watch --save-dev

Now package.json should look like this.

{
  "name": "symfony-application",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-bowercopy": "^1.0.1",
    "grunt-contrib-clean": "^0.5.0",
    "grunt-contrib-concat": "^0.4.0",
    "grunt-contrib-copy": "^0.5.0",
    "grunt-contrib-cssmin": "^0.10.0",
    "grunt-contrib-uglify": "^0.5.0",
    "grunt-contrib-watch": "^0.6.1"
  }
}

Once package.json is updated in the future you can use simple npm install to install grunt dependencies

Now when all dependencies are ready we can configure how we want assets to be processed.

Scenario 1

Download latest jQuery, Bootstrap, Font Awesome with Bower and copy the only necessary files to web/assets/*

Create file Gruntfile.js, it will contain all configurations

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        bowercopy: {
            options: {
                srcPrefix: 'bower_components',
                destPrefix: 'web/assets'
            },
            scripts: {
                files: {
                    'js/jquery.js': 'jquery/dist/jquery.js',
                    'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
                }
            },
            stylesheets: {
                files: {
                    'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
                    'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
                }
            },
            fonts: {
                files: {
                    'fonts': 'font-awesome/fonts'
                }
            }
        }
    });

    grunt.loadNpmTasks('grunt-bowercopy');
    grunt.registerTask('default', ['bowercopy']);
};

Run grunt with simple

grunt

It will fetch latest dependencies with Bower and copy them to desired locations

web
└── assets
    ├── css
    │   ├── bootstrap.css
    │   └── font-awesome.css
    ├── fonts
    │   ├── FontAwesome.otf
    │   ├── fontawesome-webfont.eot
    │   ├── fontawesome-webfont.svg
    │   ├── fontawesome-webfont.ttf
    │   └── fontawesome-webfont.woff
    └── js
        ├── bootstrap.js
        └── jquery.js

Scenario 2

Download dependencies with Bower, copy necessary files to web/assets/*. Then minify javascript and stylesheet files.

Although most frontend libraries comes with both normal and minified versions we will do it for the sake of learning.

Update Gruntfile.js you created in previous scenario with configuration for cssmin and uglify

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        bowercopy: {
            options: {
                srcPrefix: 'bower_components',
                destPrefix: 'web/assets'
            },
            scripts: {
                files: {
                    'js/jquery.js': 'jquery/dist/jquery.js',
                    'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
                }
            },
            stylesheets: {
                files: {
                    'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
                    'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
                }
            },
            fonts: {
                files: {
                    'fonts': 'font-awesome/fonts'
                }
            }
        },
        cssmin : {
            bootstrap:{
                src: 'web/assets/css/bootstrap.css',
                dest: 'web/assets/css/bootstrap.min.css'
            },
            "font-awesome":{
                src: 'web/assets/css/font-awesome.css',
                dest: 'web/assets/css/font-awesome.min.css'
            }
        },
        uglify : {
            js: {
                files: {
                    'web/assets/js/jquery.min.js': ['web/assets/js/jquery.js'],
                    'web/assets/js/bootstrap.min.js': ['web/assets/js/bootstrap.js']
                }
            }
        }
    });

    grunt.loadNpmTasks('grunt-bowercopy');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['bowercopy', 'cssmin', 'uglify']);
};

Run grunt by typing grunt and the the structure should look like this

web
└── assets
    ├── css
    │   ├── bootstrap.css
    │   ├── bootstrap.min.css
    │   ├── font-awesome.css
    │   └── font-awesome.min.css
    ├── fonts
    │   ├── FontAwesome.otf
    │   ├── fontawesome-webfont.eot
    │   ├── fontawesome-webfont.svg
    │   ├── fontawesome-webfont.ttf
    │   └── fontawesome-webfont.woff
    └── js
        ├── bootstrap.js
        ├── bootstrap.min.js
        ├── jquery.js
        └── jquery.min.js

Scenario 3

Download dependencies with Bower, merge them with your custom css and js files, then minify.

Assume we have the following structure

src
└── KP
    └── LearningBundle
        └── Resources
            └── public
                ├── css
                │   └── main.css
                ├── images
                │   └── no-photo.gif
                └── js
                    ├── editor.js
                    └── notification.js

First task we configure will be copy, it will copy image(s) to web/assets/images/* directory. Second task we will add is concat, it will merge all scripts into single file which we will later uglify to make it smaller Same thing for stylesheet file

Updated Gruntfile.js

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        bowercopy: {
            options: {
                srcPrefix: 'bower_components',
                destPrefix: 'web/assets'
            },
            scripts: {
                files: {
                    'js/jquery.js': 'jquery/dist/jquery.js',
                    'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
                }
            },
            stylesheets: {
                files: {
                    'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
                    'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
                }
            },
            fonts: {
                files: {
                    'fonts': 'font-awesome/fonts'
                }
            }
        },
        cssmin : {
            bundled:{
                src: 'web/assets/css/bundled.css',
                dest: 'web/assets/css/bundled.min.css'
            }
        },
        uglify : {
            js: {
                files: {
                    'web/assets/js/bundled.min.js': ['web/assets/js/bundled.js']
                }
            }
        },
        concat: {
            options: {
                stripBanners: true
            },
            css: {
                src: [
                    'web/assets/css/bootstrap.css',
                    'web/assets/css/font-awesome.css',
                    'src/KP/LearningBundle/Resources/public/css/*.css'
                ],
                dest: 'web/assets/css/bundled.css'
            },
            js : {
                src : [
                    'web/assets/js/jquery.js',
                    'web/assets/js/bootstrap.js',
                    'src/KP/LearningBundle/Resources/public/js/*.js'
                ],
                dest: 'web/assets/js/bundled.js'
            }
        },
        copy: {
            images: {
                expand: true,
                cwd: 'src/KP/LearningBundle/Resources/public/images',
                src: '*',
                dest: 'web/assets/images/'
            }
        }
    });

    grunt.loadNpmTasks('grunt-bowercopy');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['bowercopy','copy', 'concat', 'cssmin', 'uglify']);
};

Deploy to S3/CDN

Cloudfront is one of the most popular and in my opinion one of the easiest to begin with cdn servers Uploading assets to S3 storage and setting it as a source for CDN is enough to get started.

S3 Integration is handled by another grunt task we will add to our stack

npm install grunt-s3 --save-dev

You can find the full documentation on the official repository https://github.com/pifantastic/grunt-s3

We will only use upload feature.

Create a new file where you will store credentials to the S3 bucket. Lets call it aws-credentials.json

Don't forget to add it to .gitignore, last thing you want to do is to push your key/secret to the repository.

# .gitignore
bower_components/
node_modules/
aws-credentials.json

and the content of aws-credentials.json should be like this

{
    "key": "your_aws_key",
    "secret": "your_aws_secret",
    "bucket": "name_of_your_bucket"
}

Updated Gruntfile.js with grunt-s3 task

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        bowercopy: {
            options: {
                srcPrefix: 'bower_components',
                destPrefix: 'web/assets'
            },
            scripts: {
                files: {
                    'js/jquery.js': 'jquery/dist/jquery.js',
                    'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
                }
            },
            stylesheets: {
                files: {
                    'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
                    'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
                }
            },
            fonts: {
                files: {
                    'fonts': 'font-awesome/fonts'
                }
            }
        },
        cssmin : {
            bundled:{
                src: 'web/assets/css/bundled.css',
                dest: 'web/assets/css/bundled.min.css'
            }
        },
        uglify : {
            js: {
                files: {
                    'web/assets/js/bundled.min.js': ['web/assets/js/bundled.js']
                }
            }
        },
        concat: {
            options: {
                stripBanners: true
            },
            css: {
                src: [
                    'web/assets/css/bootstrap.css',
                    'web/assets/css/font-awesome.css',
                    'src/KP/LearningBundle/Resources/public/css/*.css'
                ],
                dest: 'web/assets/css/bundled.css'
            },
            js : {
                src : [
                    'web/assets/js/jquery.js',
                    'web/assets/js/bootstrap.js',
                    'src/KP/LearningBundle/Resources/public/js/*.js'
                ],
                dest: 'web/assets/js/bundled.js'
            }
        },
        copy: {
            images: {
                expand: true,
                cwd: 'src/KP/LearningBundle/Resources/public/images',
                src: '*',
                dest: 'web/assets/images/'
            }
        },
        aws: grunt.file.readJSON('aws-credentials.json'),
        s3: {
            options: {
                key: '<%= aws.key %>',
                secret: '<%= aws.secret %>',
                bucket: '<%= aws.bucket %>'
            },
            cdn: {
                upload: [
                    {
                        src: 'web/assets/css/*',
                        dest: 'css/'
                    },

                    {
                        src: 'web/assets/fonts/*',
                        dest: 'fonts/'
                    },
                    {
                        src: 'web/assets/images/*',
                        dest: 'images/'
                    },
                    {
                        src: 'web/assets/js/*',
                        dest: 'js/'
                    }
                ]
            }
        }
    });

    grunt.loadNpmTasks('grunt-bowercopy');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-s3');

    grunt.registerTask('default', ['bowercopy','copy', 'concat', 'cssmin', 'uglify']);
    grunt.registerTask('deploy', ['s3']);
};

Now you can generate assets like you did until now with command grunt, but also deploy them directly to the CDN with command grunt:deploy.

grunt
grunt:deploy

What next?

Article is already pretty long and I still didn't cover everything I wanted. There will be another part of this article that will cover

  • Working with less/sass
  • Automated cleaning legacy assets
  • Watching for changes in assets
  • Better explain how to correctly configure S3 bucket to act as a CDN
  • How to use Require JS (the most interesting topic)
  • Disabling Assetic Bundle from Symfony application


Published on , last updated on

Find this post helpful? Spread the word, thanks.

Comments