frontend-bundle

A modern frontend development workflow for Symfony apps

Build Status Coverage Status Scrutinizer Code Quality SensioLabsInsight Packagist Version Total Downloads

Symfony comes packaged with Assetic for managing frontend assets like CSS, JavaScript or images. Assetic is great to quickly start a project but, as applications grow, its limitations start to show.

It has thus become more and more common to integrate tools native to frontend development into Symfony projects (bower, gulp, webpack, livereload, etc). However, setting up a seamless frontend development workflow is not easy and developers must repeat themselves every time they start a new project.

There are several tools out there that make it easier to do this but they come with their own limitations and many are wrappers for the native frontend development tools. Developers should be able to use the native tools directly and have them just work within their Symfony projects.

This bundle attempts to be the go-to solution for quickly, easily and cleanly setting up a tailored frontend development workflow in Symfony projects.

Supports PHP 5.3+, Symfony 2.3+

Features

  • Asset pipeline
    • Automatically generate the build file for your preferred asset pipeline
    • Supports Gulp, (Webpack, Broccoli and others on the way)
    • Sensible defaults that work with most Symfony projects
    • You can easily adapt it for your use case
  • Use Symfony’s native calls to reference assets
    • <script src="{{ asset('js/foo.js') }}"></script>
    • No need to clutter your Twig templates with boundaries for the asset pipeline
    • Assets are automatically cache-busted in production
  • Fast development
    • Fast rebuilds make for an efficient workflow
    • Only changed files are processed
    • No more slow refreshes due to Assetic
  • Livereload
    • Browser updates when you save a file
    • Change the CSS, the browser instantaneously updates, without a page reload
  • Bower
    • Frontend dependencies are a bower install away
    • No more vendor code in your repository
    • Automatically generates vendor.js and vendor.css files from your bower.json
  • Cache busting
    • Automatically add a version to assets when in production
    • No more need to set a version on every deploy
    • An asset’s version only changes if its content changed

Table of Contents

Setup

Installation

Install with composer:

composer require regularjack/frontend-bundle

Add to your AppKernel.php:

// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        // ...
        new Rj\FrontendBundle\RjFrontendBundle(),
    );
}

Node.js must be installed on your system. You can find installation instructions on Node’s website.

Once Node is installed, run:

npm install -g bower
npm install -g gulp-cli

Note

Only gulp is supported at the moment, other asset pipelines are on the way. From now on, this document will assume you’re using gulp.

Configuration

Tip

If you’re starting a new project, no configuration is needed at this point and you can safely skip this step.

Symfony has the notion of an asset package which allows you to group related assets together. For example, you could have an app package and an admin package which you reference as follows:

<script src="{{ asset('js/foo.js', 'app') }}"></script>
<script src="{{ asset('js/bar.js', 'admin') }}"></script>

When the second argument to the asset() Twig helper is omitted, you’re in fact using the default package:

<script src="{{ asset('js/foo.js') }}"></script>

This bundle overrides the default package in order for you to not to have to pass the second argument. If you’re integrating this bundle into an existing application, which expects the asset() helper to behave as it usually does, you must disable this magic and explicitely specify a package to use:

# app/config/config.yml

rj_frontend:
    override_default_package: false
    packages:
        mypackage:
            prefix: assets

This will ensure that existing asset() calls will keep functioning as expected and you can then progressively migrate to this bundle by using the mypackage package:

<script src="{{ asset('js/foo.js', 'mypackage') }}"></script>

Setting up the asset pipeline

A console command is provided that allows you to generate a gulpfile.js tailored for your project. The command will ask you a set of questions (Where are your source assets? Where should the compiled assets be placed? Which CSS pre-processor you wish to use? Etc.) and use your answers to generate the gulpfile.js.

After running the command you’ll have a functioning gulpfile.js and the directory tree for your source assets under app/Resources/ (or wherever you decided to place them).

You can run the command with:

app/console rj_frontend:setup

Or one of the following:

# Output which commands would have been run instead of running them
app/console rj_frontend:setup --dry-run

# Use default values for all the options
app/console rj_frontend:setup --no-interaction

# Use Less and CoffeeScript, ask for the other options
app/console rj_frontend:setup --csspre=less --coffee=true

# Use Less and CoffeeScript, use defaults for other options
app/console rj_frontend:setup --csspre=less --coffee=true --no-interaction

You can read about all available options with:

app/console rj_frontend:setup --help

Tip

Feel free to take a look at the generated gulpfile.js. Even though the file is somewhat long, it should be straightforward to understand so you’ll be able to adapt it to your use case, if need be.

Finally, install npm dependencies required by gulpfile.js:

npm install

Livereload

Note

Livereload is enabled by default in development and disabled in production.

With Livereload enabled, all the requests that return a response with a closing body tag will have the following injected into the HTML, right before </body>:

<script src="//localhost:35729/livereload.js"></script>

If, for some reason, you need to change the URL, you can do so with the following configuration:

# app/config/config_dev.yml
rj_frontend:
    livereload:
        url: //example.com:1234/livereload.js

Note

The configuration should be added to app/config/config_dev.yml since it does not apply in other environments.

If you wish to not have the livereload script injected, you can do so with the following configuration:

# app/config/config_dev.yml
rj_frontend:
    livereload: false

Next steps

You’re done with setup! In development, simply run the following command, and leave it running. Assets will be recompiled when changed and livereload will be triggered:

gulp

If you just want to build the assets but not watch for changes:

gulp build

To build the assets for the production environment run:

gulp build --env production

Directory Structure

This section describes the default directory structure for both source and compiled assets. The default directory structure follows Symfony’s best practices and conventions as much as possible, as long as they make sense for the use case.

You’re free to change this directory structure as you see fit but we recommend you use the default one. If you do change it, remember to update your gulpfile.js accordingly.

Here’s an example of the directory structure of the source assets and the corresponding compiled assets:

# Sources                    # Compiled

app/Resources                web/assets
├── images                   ├── images
│   ├── foo.png              │   ├── foo.png
├── scripts                  ├── js
│   ├── app.coffee           │   ├── app.js
└── stylesheets              └── css
    └── app.scss                 └── app.css

Source Assets

Symfony’s best practices recommend you store your source assets under web/, which means they will be publicly available. However, in our case, this doesn’t make sense because those assets are meant to be compiled: you don’t want your .scss or .coffee sources to be publicly available.

Having assets under app/Resources/ solves that problem and has the added advantage that they’re right next to the templates, under app/Resources/views/, which is the best-practice location for storing templates.

Compiled Assets

Compiled assets are publicly visible so they must be stored in a directory under web/. By default, they’re stored under web/assets.

To use a directory other than web/assets just modify your gulpfile.js accordingly:

// gulpfile.js

var config = {
  buildDir: path.join(__dirname, 'web/foo'),
  // ..
};

You also need to make sure that your bundle configuration references the correct directory:

# app/config/config.yml
rj_frontend:
    prefix: foo

Referencing Assets

Note

In this section it’s assumed your compiled assets are located under web/assets/.

In templates

To reference an asset from a template, you do as you normally would, with Symfony’s asset helper:

<img src="{{ asset('images/foo.png') }}" />

Note

You’re referencing the compiled asset, from the web/assets directory, not the source asset.

This will automatically prefix, and when in production cache-bust, the URL so the previous call would ouput:

<img src="/assets/images/foo.png" />

Or, in production:

<img src="/assets/images/foo-123abc.png" />

In styleshets

It’s common that you need to reference images from your stylesheets. To do that, use the url() notation and the full path to the image, relative to web/assets/:

background-image: url(images/foo.png);

Note

Remember that you’re referencing the compiled asset, from the web/assets directory, not the source asset.

Tip

Never reference images in stylesheets with a relative path like ../images/foo.png. Relative paths make the code harder to reason about, are unnecessary and will be converted to the absolute path (i.e. ../ is stripped).

The compiled CSS would be:

background-image: url(/assets/images/foo.png);

Or, in production:

background-image: url(/assets/images/foo-123abc.png);

Using Bower

Note

In this section it’s assumed your compiled assets are under web/assets/.

Bower allows you to require frontend assets in a similar way to what Composer does for PHP packages. Instead of commiting third-party code to your repository, you simply add your dependencies to a bower.json file.

For example, if you wanted to use Bootstrap you would do:

bower install --save bootstrap

Note

The --save flag adds the dependency to bower.json

Since Bootstrap requires jquery, Bower installed both under bower_components/:

bower_components/
├── bootstrap/
└── jquery/

Requiring JavaScript and CSS assets

JavaScript and CSS assets installed with Bower are automatically compiled into web/assets/js/vendor.js and web/assets/css/vendor.css, respectively, along with their dependencies and in the correct order.

Tip

For information on how this works see Main Files.

To reference the vendor.js file from your template:

<script src="{{ asset('js/vendor.js') }}"></script>

And to reference the vendor.css file:

<link href="{{ asset('css/vendor.css') }}" rel="stylesheet">

Requiring SASS or Less stylesheets

SASS or Less stylesheets installed with bower, as opposed to CSS assets, are not automatically compiled into web/assets/css/vendor.css. This is because they are meant to be used differently.

Depending on your use case, when requiring SASS or Less stylesheets you typically want one of the following:

  1. Compile the vendor stylesheet into vendor.css, while overriding variables
  2. Include the vendor stylesheet in your own stylesheets so that you can use mixins, @extend rules, etc
Compiling into vendor.css

The app/Resources/stylesheets/vendor.scss (or .less) file will be compiled and appended to web/assets/css/vendor.css (after any potential CSS that is automatically included from your Bower dependencies). This is useful when you simply want to compile some vendor stylesheet but don’t need to use its mixins in your own stylesheets.

As an example, consider you wanted to use Font Awesome:

bower install --save font-awesome

Then you need to specify that you’re interested in its font assets:

// bower.json

{
  "dependencies": {
    "font-awesome": "~4.4.0"
  },
  "overrides": {
    "font-awesome": {
        "main": "fonts/*"
    }
  }
}

Tip

For more information on why this is needed see Overriding a package’s settings.

When using Font Awesome, you have to set the path to the fonts directory so that url() calls use the correct path. Since font assets required with Bower are automatically “compiled” into web/assets/fonts/, you would simply do:

// app/Resources/stylesheets/vendor.scss

// Use the absolute path to the fonts directory, relative to `web/assets`.
// This ensures paths will be rewritten correctly when in production.
$fa-font-path: "fonts";

@import "../../../bower_components/font-awesome/scss/font-awesome";

If you now build your assets and look into web/assets/css/vendor.css you’ll see Font Awesome’s code, where the url() calls are something like:

url(/assets/fonts/fontawesome-webfont.eot)

Or, in production:

url(/assets/fonts/fontawesome-webfont.123abc.eot)
Including in your own stylesheets

Instead of compiling a vendor stylesheet into vendor.css, it’s sometimes better to @import it in your own stylesheet instead. This is the case when you want to use mixins or variables defined by the vendor stylesheet.

Following Font Awesome’s example above, suppose you wanted to use its mixins in your stylesheet:

// app/Resources/stylesheets/app.scss

$fa-font-path: "fonts";
@import "../../../bower_components/font-awesome/scss/variables";
@import "../../../bower_components/font-awesome/scss/mixins";

.foo {
  @include fa-icon();
}

Note

Note that this is just an example and not the correct usage of Font Awesome. In a real application you would never use the fa-icon mixin directly.

Overriding a package’s settings

Main Files

We’re able to automatically generate the vendor.css and vendor.js files from bower.json because Bower packages, in their own bower.json, define their main files.

For example, if you look into Bootstrap’s bower.json, you will see something like:

// bower_components/bootstrap/bower.json

"main": [
  "less/bootstrap.less",
  "dist/js/bootstrap.js"
],

By parsing this file, we’re able to automatically add the bootstrap.js file to our vendor.js.

However, as you can see above, Bootstrap does not define bootstrap.css as a main file. If you wanted to automatically include bootstrap.css into your vendor.css, you would override the main files defined in Bootstrap’s bower.json by adding the following to your own bower.json:

// bower.json

{
  "dependencies": {
    "bootstrap": "~3.3.5"
  },
  "overrides": {
    "bootstrap": {
        "main": "dist/css/bootstrap.css"
    }
  }
}

If you then build your assets you’ll see that bootstrap.css is present in your vendor.css.

Dependencies in the same package

If you need to specify dependencies between Bower assets in a given package, you can do so in the overrides section of you bower.json.

For example, instead of including all of Bootstrap’s JavaScript, suppose you only needed popover.js. Since popover.js requires tooltip.js, your bower.json would be:

// bower.json

{
  "dependencies": {
    "bootstrap": "~3.3.5"
  },
  "overrides": {
    "bootstrap": {
        "main": [
            "js/tooltip.js",
            "js/popover.js"
        ]
    }
  }
}

You’ll then find both files in your vendor.js, in the order you specified.

Dependencies between packages

Sometimes you run into Bower packages that do not correctly specify dependencies in their bower.json.

As an example, suppose you wanted to use a foo package that requires jquery but does not specify that in their bower.json. You would install both packages using Bower:

bower install --save foo jquery

And then setup the dependency in your bower.json:

// bower.json

{
  "dependencies": {
    "foo": "0.0.1",
    "jquery": "~2.1.4"
  },
  "overrides": {
    "foo": {
        "dependencies": {
            "jquery": "*"
        }
    }
  }
}

This ensures that, in your vendor.js, jquery.js will appear before foo.js.

Deployment

Note

In this section it’s assumed your compiled assets are under web/assets/.

To build your assets for the production environment:

gulp build --env production

In production, CSS and JavaScript assets are minified and all assets are revisioned so that browsers don’t use a stale version from cache. As an example, if you have an images/foo.png file it will become something like images/foo-123abc.png where 123abc is the hash of the file’s content.

Manifest

So, how does this magic work? How can you do:

<script src="{{ asset('js/foo.js') }}"></script>

And have it output:

<script src="/assets/js/foo-123abc.js"></script>

The answer is: by using a manifest file that maps the original filename to the filename with the hash appended. When you run gulp build --env production you will also get a manifest.json file that looks something like:

{
  "js/foo.js": "js/foo-123abc.js"
}
Using the manifest

Using the manifest file to generate the URLs is disabled by default. You need to enable it for the production environment:

# app/config/config_prod.yml

rj_frontend:
    manifest: true
Changing the manifest path

By default, the manifest is expected to be found under web/assets/manifest.json. If you need to change this, you would add the following to your app/config/config_prod.yml file:

# app/config/config_prod.yml

rj_frontend:
    manifest: "%kernel.root_dir%/../web/foo/manifest.json"

Configuring assets to never expire

Since an asset’s filename will change if its content changes, you can safely tell browsers to cache all assets indefinitely. You can do that by having your webserver set the Expires header to a value in the far future, say one year.

If you’re using nginx, you can do this by adding the following location block to the server configuration:

location /assets/ {
    expires 1y;
}

If using Apache, make sure you have mod_expires active and add the following to your configuration:

<ifmodule mod_expires.c>
    <Directory /path/to/web/assets>
        ExpiresActive on
        ExpiresDefault "access plus 1 year"
    </Directory>
</ifmodule>

Using a CDN

When serving assets from a Content Delivery Network, you want to use an absolute URL, for example:

<script src="//cdn.example.com/js/foo-123abc.js"></script>

You can do this with the following configuration:

# app/config/config_prod.yml

rj_frontend:
    prefix: //cdn.example.com/
    manifest: true

Note

The manifest file must still be present locally in your server

You also want references between assets to use the absolute URL, like when referencing images from your stylesheets. In your gulpfile.js you can set an URL prefix to use in production as follows:

// gulpfile.js

var config = {
  ...
  // Prepend references between assets with a prefix.
  // Will only be used in production builds.
  urlPrefix: '//cdn.example.com',
  ...
};

License

MIT