Categories


Archives


Recent Posts


Categories


OroCRM Frontend Asset Pipeline

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

I’m willing to concede this might be my inner frozen caveman developer talking, but right now “frontend web development” seems like it’s in a crises of chaos. Frontend developers, (the folks writing CSS, Javascript, and building our application UIs), have never had such a wealth of tools to choose from. Unfortunately, most of these tools tend to live in a universe by themselves, and getting these tools to play together and perform well in a production system is one of those thankless, yet vitally important tasks for any web application.

This chaos is on display in OroCRM. Today we’re going to cover Oro’s frontend asset pipeline. That is, we’ll cover the basics of how OroCRM pulls in its various frontend files so you, as a third party developer, can make intelligent choices about including javascript and CSS in your own bundles.

Symfony Asset Pipeline

Like most OroCRM features, the first thing we’ll want to review is Symfony 2’s frontend asset management systems. Symfony has two systems for managing frontend assets: The assets system, and the assetic system.

The assets system is the original system for managing frontend assets in Symfony. It gives you the ability to create asset links from templates, as well as include your asset files in your bundle.

In a twig template you’d write something like this

<script src="{{ asset('my/asset.js') }}" type="text/javascript"></script>

The asset twig function will automatically create the correct asset path. As a bundle developer, you distribute your frontend assets with your bundle. Specifically, they go the Resources/public folder.

For example, you would place the javascript file in the template above at

Resources/public/my/asset.js

Then, to copy the files to your publicly accessible web folder, you’d use the Symfony console application

$ php app/console assets:install

Running the above will copy all the Resource/public files to

$ /path/to/symfony/web/bundles/[bundle-name]

The asset function in the twig template handles converting the asset path to the correct http accessible path.

That’s the assets system. Its main goal is to enable linking to assets, and distributing frontend assets as part of a bundle. While useful, the needs of a modern frontend developer have evolved to the point where this simple system isn’t adequate. That’s why Symfony created the assetic system.

The Symfony documentation covers assetic well, so our description will be brief. Like the asset system, assetic gives developers a template syntax, and a way to get files from a bundle to the frontend web accessible system. The three main differences with assetic are the ability to do this dynamically at runtime, include filters for modifying the asset files (javascript minification/optimization/etc.), and automatically combine individual asset files into a single file for production deployments.

For templates, there are twig tags for each frontend file type. For example, to include a javascript file, you’d write something like this

{% javascripts '@AcmeFooBundle/Resources/public/js/file.js' %}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}

In addition to the {% javascripts %} tag, you get tags for {% stylesheets %} and an {% image %} tag.

In the example above, we’ve linked to an individual javascript file via a Symfony identifier, (@AcmeFooBundle/Resources/public/js/file.js). Unlike the assets system, the assetic system gives us the ability to link to frontend files from different bundles. Another advantage is the ability to link to multiple frontend asset files in one go. Consider the following

{% javascripts '@AcmeFooBundle/Resources/public/js/*' %}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}

The above code snippet will link to every javascript file in the AcmeFooBundle‘s Resources/public/js folder. Think of the {% javascripts %} tag as a foreach loop — one loop iteration for each file. This means adding a new file to the application is as simple as copying the file to a folder — no extra template code needed.

The key ah ha moment in understanding assetic comes from looking at its output. In the dev environment, Symfony will generate HTML something like this

<script type="text/javascript" src="/app_dev.php/js/file.js"></script>
<script type="text/javascript" src="/app_dev.php/js/other-file.js"></script>
//...

That is — requests for the frontend files are routed through the PHP app_dev.php front controller. This allows assetic to filter/change the output of these files.

So that’s the dev environment. However, when it comes time to deploy, you’ll see output that looks something like this

<script type="text/javascript" src="/web/js/7946a9a.js"></script>

In production, a single javascript file is served statically. This single file is a combination of all the javascript files specified by a single tag. Prior to deploying, a Symfony developer runs the app/console assetic:dump command.

$ php app/console assetic:dump

This command combines all the frontend files for each block, with filters applied, and spits out a single javascript file.

The end result is developers can tweak things to their heart’s content in the dev environment without regenerating a cache file, but the production deployment is a nice, static file, suitable for CDNs. A win/win as our business friends might say.

OroCRM’s Pipeline Customizations

The Symfony asset pipeline follows a common pattern in framework development. At first, Symfony tried to offer a single, straightforward way to manage javascript and CSS files (assets). When this didn’t prove adequate for every developer, they created an abstract system (assetic) that lets individual developers create their own asset pipeline.

While powerful, this means every project will end up using assetic a little bit differently. It should come as no surprise that this includes the OroCRM team.

OroCRM and Symfony

Because OroCRM is a Symfony application, they need to use asset and assetic. That’s because many bundles rely on these packages to include their frontend files. OroCRM’s installer includes the assets:install and assetic:dump commands.

There are plenty of Oro templates which use the simpler asset linking method. However, the framework makes limited use of the files generated by the assetic commands. From what I’ve seen, none of the {% javascripts %}, {% sytlesheets %}, etc. tags are used in Oro twig templates. The assetic generated javascript file(s) is/are ignored, and the assetic generated CSS file (web/css/oro.css) is linked via a twig template that sets up Oro’s use of the Less CSS pre-processor.

#File: vendor/oro/platform/src/Oro/Bundle/AsseticBundle/Resources/views/Assets/oro_css.html.twig
{% oro_css filter='cssrewrite, lessphp, cssmin' output='css/oro.css' %}
{% set isLess = ('less' in asset_url|split('.')) %}
{% if isLess %}
<script type="text/javascript">localStorage.clear();</script>
{% endif %}
<link {% if isLess %}rel="stylesheet/less"{% else %}rel="stylesheet"{% endif %} media="all" href="{{ asset_url }}" />
{% if isLess %}
<script type="text/javascript" src="{{ asset('bundles/oroui/lib/less-1.3.3.min.js') }}"></script>
{% endif %}
{% endoro_css %}

If you’re not familiar with it, Less is one of the many “I wish CSS had more features than it does so lets create a programming language on top of CSS” projects out there.

While maybe not what you hoped for, this is a valid use of Symfony’s assetic system. It does leave us with a question — how does OroCRM pull-in/combine its javascript files, and why aren’t they using assetic for this?

OroCRM Javascript and RequireJS

For the most part, when OroCRM pulls in third party libraries, they stick to the twig asset function. You can see one example of this here

#File: vendor/oro/platform/src/Oro/Bundle/UIBundle/Resources/views/Default/dialogPage.html.twig
//...
<script type="text/javascript" src="{{ asset('bundles/orowindows/js/jquery.dialog.extended.js') }}"></script>
//...

However, when it’s not a third party library — that is when OroCRM writes new custom javascript code — they do so with a framework called RequireJS.

RequireJS is a javascript Asynchronous module definition (AMD) implementation. It gives javascript a module system that’s similar to python’s, ruby’s, or NodeJS’s. A full explanation of RequireJS is beyond the scope of this article, but in short RequireJS allows you to define javascript module objects in individual files, and then include those modules in other scoped javascript code. Consider the following code

#File: web/bundles/orosync/js/sync/wamp.js
define(['jquery', 'underscore', 'backbone', 'autobahn'],
function ($, _, Backbone, ab) {
    //...
    return Wamp;
});

This defines a module named js/sync/wamp. The name comes from the javascript path of the file. This module uses (requires, imports, etc.) the modules jquery, underscore, backbone, and autobahn. The module objects are passed to the anonymous function as parameters ($, _, Backbone, ab). The returned Wamp object is the js/synx/wamp module.

The other top level function in RequireJS is require. You use the require function when you want to create an individual RequireJS program. You can see an example of this here

#File: vendor/oro/platform/src/Oro/Bundle/WorkflowBundle/Resources/views/Widget/widget/transitionForm.html.twig
require(['oro/widget-manager'],
function(widgetManager){
    widgetManager.getWidgetInstance({{ app.request.get('_wid')|json_encode|raw }}, function(widget) {
        widget.trigger(
            'formSave',
            {% if data is defined and data %}
                {{ data|json_encode|raw }}
            {% else %}
                null
            {% endif %}
        );
    });
});

In a pure RequireJS program, the require function is limited to the main entry point. OroCRM is not a pure RequireJS project, and uses require more like a document.ready/dom:loaded replacement.

Optimizing RequireJS

RequireJS presents a problem for a Symfony application. Specifically — RequireJS has its own optimization system, one that’s not compatible with the approach provided by assetic. This is why the OroCRM installer also includes the oro:requirejs:build command

$ php app/console oro:requirejs:build

This command builds the combined RequireJS files in web/js/oro.min.js. This is the file OroCRM links to when running in production mode. In development mode, OroCRM dynamically includes the RequireJS modules. The how/why of that is beyond the scope of this article but if you’re curious about OroCRM’s RequireJS setup, the scripts.html.twig template in the RequireJSBundle bundle is a good place to start.

#File: vendor/oro/platform/src/Oro/Bundle/RequireJSBundle/Resources/views/scripts.html.twig
{% if compressed and requirejs_build_exists() %}
    <script type="text/javascript">
        var require = (function(){
            var r=function(c){m(r.c,c)};r.c={};function m(a,b){
                for (var i in b)b[i].toString()==='[object Object]'?m(a[i]||(a[i]={}),b[i]):a[i]=b[i]}
            return r;
        }());
        {% placeholder requirejs_config_extend %}
        require = require.c;
    </script>
    <script type="text/javascript" src="{{ asset(get_requirejs_build_path()) }}"></script>
{% else %}
    <script type="text/javascript" src="{{ asset('bundles/ororequirejs/lib/require.js') }}"></script>
    <script type="text/javascript">
        {{ get_requirejs_config() }}
    </script>
    <script type="text/javascript">
        {% placeholder requirejs_config_extend %}
    </script>
{% endif %}

You’ll notice a RequireJS file is pulled in with the assets function.

<script type="text/javascript" src="{{ asset('bundles/ororequirejs/lib/require.js') }}"></script>

This means Oro’s RequireJS implementation is reliant on the Symfony assets system, and your having successfully run the assets:install command.

The next section takes a small detour for people interested in tracing how Symfony pulls in the RequireJS modules in development mode. It’s not required reading at this point, but we’re including it for the curious minded. If your brain’s already overloaded, skip ahead to the OroCRM Placeholders section.

OroCRM’s get_requirejs_config Twig Function

The dynamically generated modules come from the call to the get_requirejs_config twig function. This function is defined in an extension here

#File: vendor/oro/platform/src/Oro/Bundle/RequireJSBundle/Twig/OroRequireJSExtension.php
new \Twig_SimpleFunction('get_requirejs_config', function () use ($container) {
    return $container->get('oro_requirejs_config_provider')->getMainConfig();
}

In turn, this uses the getMainConfig method in the the oro_requirejs_config_provider Symfony service at

vendor/oro/platform/src/Oro/Bundle/RequireJSBundle/Provider/Config.php

to pull in the modules. This service ends up reading from a bundle’s Resources/config/requirejs.yml. These YML files contain configuration that tells OroCRM which javascript files contain RequireJS modules, as well as setting up RequireJS dependencies. You can see an example of one of these files here.

vendor/oro/platform/src/Oro/Bundle/SyncBundle/Resources/config/requirejs.yml

OroCRM Placeholders

Phew! That’s a lot of information to take in. For a frontend or full-stack developer, understanding an application’s frontend asset pipeline is hugely important. The way assets are included into a project heavily impacts the way an application works, and making sure your code lines up with expectations is vital to ensuring your code behaves itself.

However — for someone looking to make a simple CSS tweak, or include some third party javascript library, this can all seem like a bit much to process. Fortunately, OroCRM offers a much simpler way to add frontend files to your system, and that’s via the twig {% placeholder %} tag. If you take a look at the base OroBAP/OroCRM twig template file, you’ll see these tags

#File: vendor/oro/platform/src/Oro/Bundle/RequireJSBundle/Resources/views/scripts.html.twig

{% placeholder head_style %}
//...
{% placeholder scripts_before %}
//...
{% placeholder scripts_after %}

These placeholder scripts allow bundles to include any twig template wherever the {% placeholder %] tag is. This twig template can then reference any javascript or CSS you like. To use this feature, just include a placeholders.yml file in your bundle. If you’re curious about the syntax, checkout the OroBAP’s Twig Placeholder Tag post over on my Oro Quickies website.

Wrap Up

That’s Oro’s frontend asset pipeline. While seemingly chaotic, once you understand the constraints in place, and all the various moving parts, it should be simple enough to add your own frontend files to the system. While a lot has changed in software development over the past 20 years, the maxim that The Best System is the System in Use By Your Team remains truer than ever.

Every development shop will have a different approach to adding their frontend assets depending on their needs and expertise, and I hope this article helps you make your own decisions.

If you have any questions, or I’ve made a mistake above, please get in touch or let me know in the comments below. For super technical questions, open a thread over at Stack Overflow and mention it below.

Originally published April 2, 2014
Series Navigation<< OroCRM and the Symfony App ConsoleWebSockets in OroCRM >>