This site looks a lot better with CSS turned on. Note: IE<10 is not supported.

an article is displayed.

Symfony 4: using ParcelJs with Twig templates

Using Parcel in a Symfony 4 project

Introduction

I've used Webpack for a couple of projects so far, and it's undoubtedly been a useful tool. If you have a need for a Javascript based static module bundler then you'll more than likely appreciate its popularity and granular configuration.

Webpack's flexibility and power comes at the price of a fairly steep learning curve, however, and whilst the time required to learn its intricacies turned out to be a sound investment for my more complicated bundling requirements, it all started to feel a bit unnecessary when I just wanted to spin up something straight forward.

ParcelJs brings something slightly different to the table, and whilst it won't replace Webpack for complicated bundling, it more than makes up for it with speed and increased simplicity.

As I've been getting into PHP quite recently, I decided to find out whether Parcel plays nicely with the Symfony 4 framework and its default Twig templating system.

There may well be other, better ways to achieve the same thing with Parcel and Twig, but the following article describes a process that has fulfilled my needs so far.

It's worthwhile to note that Symfony 4 provides Encore, which is a simpler way to integrate Webpack into your application. If you're still trying to decide which bundler to use for your application, then you might want want to look into that too.

I notice there have been efforts to create a plugin that will allow Parcel to deal with Twig templates internally. I couldn't find anything that was in a state of completion at the time of writing this article, but be sure to do your own research in case something has been released in the meantime.

I've also created a Github repo for anyone that wants to dive straight into the code.

Goals

By the end of this article, you should be able to:

  • set up a skeleton Symfony 4 project
  • create a basic pipeline with ParcelJs that will bundle Javascript and CSS assets
  • display the bundled assets using Symfony's Twig templates
  • display certain assets inline

Prerequisites

  • OSX Mojave
  • A local installation of the Composer dependency manager
  • Basic knowledge of PHP and the Symfony framework
  • A local installation of the latest stable NodeJs release (12.9.1 at the time of writing)
  • Basic knowledge of asset bundling

OSX Mojave isn't an essential requirement, but that's what I've used to test everything.

You can probably get away with an earlier version of NodeJS, but again this is what I used to test the process.

Create a Symfony 4 skeleton project

If you're unsure about this process then please read my article that explains how to create a new Symfony project from scratch.

For our purposes here, I'm using hackerbox_symfony_4_twig_parcel for the new project name, so if you wish to follow along exactly then be sure to use this instead of the name that I use in the other article.

Create a new Controller

We'll need a controller to test out the results of our asset bundling.

Open up the new project in your favourite code editor

Create the file src/Controller/HomeController.php, and paste in the following code:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;


class HomeController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig');
    }
}

The controller we've made is about as basic as it gets really. I've used the route annotations so that we don't have to be bothered with a separate routing file, but that's about it.

Create a Twig template

At the moment, our controller method will attempt to render a template that doesn't exist, so let's create a simple twig template to fix that.

Create the file templates/home/index.html.twig, and paste in the following code:

{% extends 'base.html.twig' %}
{% block body %}
    <div class="example-wrapper">
        <h1>Symfony 4: using ParcelJs with Twig templates</h1>
        <p>Hello from Hackerbox.io</p>
    </div>
{% endblock %}

Refresh your browser, and you should see the contents of the template that we created. Black text, white background, not exactly thrilling.

The results of a very basic twig template

Before we move on though, let's try to understand a bit more about what's going on here.

If you view the source of our page in your web browser, you should see something similar to the following:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Welcome!</title>
            </head>
    <body>
            <div class="example-wrapper">
        <h1>Symfony 4: using ParcelJs with Twig templates</h1>
        <p>Hello from Hackerbox.io</p>
    </div>
</body>
</html>

You'll probably find that your browser's page source shows a lot of stuff between lines 10 and 11 that I didn't include in the example above. That's just the code that Symfony injects to create the handy "sticky bar" that appears at the bottom of the screen during development. It isn't relevant for our purposes, so just ignore it for now.

Lines 8 - 10 contain the markup that we put in our template, but everything else comes from the base template.

If you take another look at our template code, you'll see that we actually extended base.html.twig, and added a block named body.

{% extends 'base.html.twig' %}
{% block body %}
    <div class="example-wrapper">
        <h1>Symfony 4: using ParcelJs with Twig templates</h1>
        <p>Hello from Hackerbox.io</p>
    </div>
{% endblock %}

This tells the Twig templating system to first load the base.html.twig template, and then inject the markup that it finds between block and endblock tags (in index.html.twig in this case) into the corresponding references in base.html.twig.

Here we've named our block body, and if we open up the templates/base.html.twig file, we can see that it contains, amongst other things, a body block too (line 9):

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

We'll need to deal with some of those other blocks shortly, so let's get on with creating our assets.

Adding styles and scripts

In the project root, create the file styles/main.css, and paste in the following code:

body {
    background-color: #ff0077;
    color: #00ff00;
}

h1 {
    font-weight: bold;
    color: #fff;
}

In the project root, create the file styles/homePage/index.css, and paste in the following code:

@import '../main.css';

In the project root, create the file scripts/main.js, and paste in the following code:

export default class Messages {
    static SayHello () {
        return 'Hello from Messages!';
    }
}

In the project root, create the file scripts/homePage/index.js, and paste in the following code:

import Messages from '../main';

console.log(Messages.SayHello());

Installing ParcelJs

Our assets aren't much good to us in their current location, but as we'll be using Parcel for our pipeline, it's time to get that installed.

Stop your development server (or open a new terminal window), navigate to the project root, and run the following command to create a package.json file:

npm init

For our purposes here, it's fine to provide the default answers to the questions when prompted.

Run the following command to install Parcel globally:

npm install parcel-bundler -g

Type the following command to ensure that Parcel has installed properly.

parcel --help

You should see a list of options. For example:

Usage: parcel [options] [command]
.......

Creating the asset bundles

At this point, we can see Parcel in action.

Navigate to the project root, and run the following command to bundle our scripts:

parcel build scripts/homePage/index.js --out-dir public/assets --out-file homePage.bundle.js --no-source-maps

This will actually create a production bundle of our scripts, which will "minify" the code and avoid creating source maps, but we'll come to how we can create something more suitable for development later on.

The above command tells Parcel to take the code in the file scripts/homePage/index.js, and bundle it along with the code contained in any of its associated import statements into a single file in Symfony's default public directory (public/assets/homePage.bundle.js). The code in the resulting file will be minified, as mentioned previously, and no source maps will be created thanks to the --no-source-maps flag.

Check that a file containing minified javascript has been created at public/assets/homePage.bundle.js.

We can now do the same for our css files:

Navigate to the project root, and run the following command to bundle our styles:

parcel build styles/homePage/index.css --out-dir public/assets --out-file homePage.bundle.css --no-source-maps

Check that a file containing minified CSS has been created at public/assets/homePage.bundle.css.

Using the bundles

In order to make use of the scripts and styles that we bundled, we'll need to reference them in our twig template using the stylesheets and javascripts blocks that I mentioned earlier.

Open the home/index.html.twig template, and add the stylesheets and javascripts block as follows:

{% extends 'base.html.twig' %}

{% block stylesheets %}
    <link rel="stylesheet" href="/assets/homePage.bundle.css" />
{% endblock %}

{% block body %}
    <div class="example-wrapper">
        <h1>Symfony 4: using ParcelJs with Twig templates</h1>
        <p>Hello from Hackerbox.io</p>
    </div>
{% endblock %}

{% block javascripts %}
    <script src="/assets/homePage.bundle.js"></script>
{% endblock %}

Make sure your project is running, and refresh your browser. You should see the garish screen shown below:

The effects of the bundled assets are rendered in a web browser

Isn't that beautiful. An inspiration for designers everywhere.

Well, maybe not, but it illustrates very clearly that our bundled styles have been successfully rendered.

If you check the Javascript console, you should see that the bundled scripts have run too, and that a message has been printed: "Hello from Messages!"

Bundling assets during development

So far, we've seen how to create minified bundles using scripts that were run directly on the command line. This is all well and good, but we can make the process a little bit neater and set things up so that the bundles will be created automatically when changes are made to the associated source files.

Make sure your project is running, open another terminal window in the project root, and run the following command:

parcel watch scripts/homePage/index.js --no-hmr --public-url /assets/ --out-dir public/assets --out-file homePage.bundle.js & parcel watch styles/homePage/index.css --no-hmr --public-url /assets/ --out-dir public/assets --out-file homePage.bundle.css

Open the styles/main.css, and make a change to the styles. Something obvious, like the background colour, for example.

Refresh your browser, and you should see that your change has taken effect.

Open the scripts/main.js, and make a change to the message that is output to the console.

Refresh your browser, open the javascript console, and you should see that your change has taken effect.

What we've actually done here is run two commands that will start two simultaneous processes: one that watches for changes in .js files, and another that watches for changes in .css files.

The watch flag tells Parcel to watch for changes in the source files, and then the relevant bundles are recreated whenever a change is detected.

This is really handy during the development cycle, and it's far easier having the process triggered automatically rather than having to mess around in the terminal every time you make a change.

It's possible to take the process further by using Parcel's Hot Module Replacement (HMR) feature, but that's beyond the scope of this article. I've turned HMR off for our purposes here, using the --no-hmr flag. Read the documentation to find out more about HMR :)

The only other thing to note is the --public-url flag. This accepts a value that specifies the location that the assets are being served from, and is used to provide a reference in the bundle to its associated source map file.

If you open up homePage.bundle.css, for example, you should see the following reference.

/*# sourceMappingURL=/assets/homePage.bundle.css.map */

When you're ready to do so, you can quit the watch processes with ctrl + c

Tidying up

It's all very messy running big long commands in the terminal, so it would be nice if we could tidy things up a bit.

Stop any watch processes that you might have running, open the package.json file, and alter the scripts section so that it looks like the example below:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build_styles_prod": "parcel build styles/homePage/index.css --out-dir public/assets --out-file homePage.bundle.css --no-source-maps",
    "build_scripts_prod": "parcel build scripts/homePage/index.js --out-dir public/assets --out-file homePage.bundle.js --no-source-maps",
    "build_styles_dev": "parcel watch styles/homePage/index.css --no-hmr --public-url /assets/ --out-dir public/assets --out-file homePage.bundle.css",
    "build_scripts_dev": "parcel watch scripts/homePage/index.js --no-hmr --public-url /assets/ --out-dir public/assets --out-file homePage.bundle.js",
    "build_assets_prod": "find public/assets | grep -E '.*\\.bundle\\..*$' | xargs rm; npm run build_styles_prod & npm run build_scripts_prod",
    "build_assets_dev": "find public/assets | grep -E '.*\\.bundle\\..*$' | xargs rm; npm run build_styles_dev & npm run build_scripts_dev"
  },

The development build / watch process can now be started by running the following command in the terminal:

npm run build_assets_dev

The production build can now be started by running the following command in the terminal:

npm run build_assets_prod

You'll need to have your project running in one terminal window, and then run the NPM commands in a separate window if you're running build_assets_dev. We'll tidy that up shortly though.

You may have noticed that there's a bit of additional jiggery-pokery at the beginning of the build_assets_dev and build_assets_prod scripts:

find public/assets | grep -E '.*\\.bundle\\..*$' | xargs rm;

All this does is remove any pre-existing bundle files (the ones that have .bundle. in their name) so that the production build isn't affected by any artifacts that might remain from a previously run development build, or vice versa.

First, we find all the files in the public/assets directory:

find public/assets

We then pipe the resulting files names through to the grep command, which returns only the files that have .bundle. in their name:

| grep -E '.*\\.bundle\\..*$'

Finally, we pipe the filtered results into xargs, which passes the identified file names on to the rm command.

| xargs rm;

At this point, our development process still involves having two terminal windows open. This might be fine, or even desirable for some people, but nevertheless we can simplify our process further using composer.json.

Open the composer.json file, and add run-local-dev and run-local-prod to the scripts section, similar to the example below:

"scripts": {
    "auto-scripts": {
        "cache:clear": "symfony-cmd",
        "assets:install %PUBLIC_DIR%": "symfony-cmd"
    },
    "post-install-cmd": [
        "@auto-scripts"
    ],
    "post-update-cmd": [
        "@auto-scripts"
    ],
    "run-local-dev": "npm run build_assets_dev & php bin/console server:run",
    "run-local-prod": "npm run build_assets_prod & php bin/console server:run"
},

Now you should be able to start the Symfony project and start watching both .css and .js files from composer. Try stopping the development server, along with any watch processes, and run:

composer run-local-dev

If you want to do the same thing with a production build (without watchers), you can run the following command:

composer run-local-prod

In practice, the production build might never really be run locally, but it's here just to illustrate a concept.

Inlining styles

The main reason to bundle assets is to reduce the number of HTTP requests required to render a web page. If you have two files referenced in the initial HTML response from a web server, for example, then a browser would need to make two additional requests to retrieve this files. Assuming that the files are small enough, combining them into a single file would then only require one additional HTTP request to retrieve it.

This is a bit of an over simplification, so do your own research around bundling and optimisation.

A potential downside to requiring additional files, however, is that those files will be downloaded after the initial HTML has been rendered in the browser, which can often cause the page to flash or jiggle while the browser updates everything.

Nowadays, it's popular practice to embed, or "inline", certain assets within the initial response from the web server in order to give the user the perception of a "visually ready" page as soon as possible, despite the resulting increase in the size of the initial response.

Typically these inlined assets (styles, scripts, images, fonts, etc) are essential to the page's fundamental structure, and provide the user with everything required for a pleasant experience, whilst less important functionality and styling is loaded in later on by the bundles.

The example I'm using in this tutorial is far too simple to be realistic, but we can still use it to take a look at how we might go about inlining assets that have been processed by Parcel.

Let's create some "essential" assets that we can inline.

Create the file styles/essential.css, and paste in the following code:

body {
    font-family: "Comic Sans MS", cursive, sans-serif;
}

Create the file scripts/essential.js, and paste in the following code:

(function inlineJsExample() {
    console.log('HELLO, FROM AN INLINE JAVASCRIPT!');
})();

We'll also need a bit of additional code to help us out, so create the file src/Controller/Helpers/ClientBundleHelpers.php, and paste in the following code:

<?php

namespace App\Controller\Helpers;


class ClientBundleHelpers {
    /**
     * @return bool|string
     */
    public static function getEssentialCssInline() {
        return file_get_contents(__DIR__ . '/../../../public/assets/essential.inline.css');
    }

    /**
     * @return array|false|string
     */
    public static function getEssentialJsInline() {
        return file_get_contents(__DIR__ . '/../../../public/assets/essential.inline.js');
    }
}

I've used a couple of hard-coded paths to asset files there. You might want to replace the paths with environment variables, and maybe pass the names of the assets in as parameters to the functions, but I'll leave that choice to you.

The above code is pretty simple. All we're really doing is using PHP's file_get_contents function to return the contents of the specified files rather than the entire files themselves.

We also need to tweak our controller method a little bit if we're going to make use of those helpers. Open up the controller (scr/Controller/HomeController.php), and update the code so that it looks like the following:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use App\Controller\Helpers\ClientBundleHelpers;


class HomeController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        return $this->render(
            'home/index.html.twig',
            [
                'essential_styles' => ClientBundleHelpers::getEssentialCssInline(),
                'essential_scripts' => ClientBundleHelpers::getEssentialJsInline()
            ]
        );
    }
}

Here, we're taking the asset file contents that were returned from our helper methods, and making them available to our Twig templates in the essential_styles and essential_scripts variables.

Those variables aren't yet referenced in our Twig template, so let's fix that.

Open up the templates/base.html.twig file, and alter the contents so that it appears as follows:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
        {% if essential_styles is defined and essential_styles is not empty %}
            <style>
                {{ essential_styles|raw }}
            </style>
        {% endif %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
        {% if essential_scripts is defined and essential_scripts is not empty %}
            <script>
                {{ essential_scripts|raw }}
            </script>
        {% endif %}
    </body>
</html>

There are a couple of things to pay attention to here:

{% if essential_styles is defined and essential_styles is not empty %}
    <style>
        {{ essential_styles|raw }}
    </style>
{% endif %}
{% if essential_scripts is defined and essential_scripts is not empty %}
    <script>
        {{ essential_scripts|raw }}
    </script>
{% endif %}

In both of those code snippets, we're referencing a variable and using the raw filter to ensure that the variable's content isn't HTML encoded before it's rendered. By default, Symfony provides automatic escaping, but we want the content of our asset files to appear in the source exactly as it appears in the files themselves. As you can see, the asset contents are injected between style and script tags respectively.

Automatic escaping can help to prevent code injection vulnerabilities, so be careful where you're using the raw filter.

The checks on lines 7 and 16 are just defensive programming. We make sure that the essential_styles and essential_scripts variables exist before we try to use them, and also that they contain content. There's really no point in rendering an empty pair of tags.

We'll also need to update our NPM scripts, so open the package.json file update the scripts section with the following code:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build_styles_prod": "parcel build styles/homePage/index.css --out-dir public/assets --out-file homePage.bundle.css --no-source-maps",
    "build_scripts_prod": "parcel build scripts/homePage/index.js --out-dir public/assets --out-file homePage.bundle.js --no-source-maps",
    "build_essential_styles_prod": "parcel build styles/essential.css --out-dir public/assets --out-file essential.inline.css --no-source-maps",
    "build_essential_scripts_prod": "parcel build scripts/essential.js --out-dir public/assets --out-file essential.inline.js --no-source-maps",
    "build_styles_dev": "parcel watch styles/homePage/index.css --no-hmr --public-url /assets/ --out-dir public/assets --out-file homePage.bundle.css",
    "build_essential_styles_dev": "parcel watch styles/essential.css --no-hmr --public-url /assets/ --out-dir public/assets --out-file essential.inline.css --no-source-maps",
    "build_essential_scripts_dev": "parcel watch scripts/essential.js --no-hmr --public-url /assets/ --out-dir public/assets --out-file essential.inline.js --no-source-maps",
    "build_scripts_dev": "parcel watch scripts/homePage/index.js --no-hmr --public-url /assets/ --out-dir public/assets --out-file homePage.bundle.js",
    "build_assets_prod": "find public/assets | grep -E '.*\\.(bundle|inline)\\..*$' | xargs rm; npm run build_essential_styles_prod & npm run build_essential_scripts_prod & npm run build_styles_prod & npm run build_scripts_prod",
    "build_assets_dev": "find public/assets | grep -E '.*\\.(bundle|inline)\\..*$' | xargs rm; npm run build_essential_styles_dev & npm run build_essential_scripts_dev & npm run build_styles_dev & npm run build_scripts_dev"
},

Restart your project, and then run one of the scripts we created earlier (composer run-local-dev or composer run-local-prod). Refresh your browser.

You should see that the new Comic Sans MS font has been applied:

Inline assets are render in a web browser

If you check the Javascript console, you should also see that the "HELLO, FROM AN INLINE JAVASCRIPT!" message has been output.

The page source should show that our essential scripts and styles have been added directly into the HTML response. If you started the project with composer run-local-dev, then the inline assets will not be minified, and will be automatically updated when you make changes to the source, whereas the composer run-local-prod should have minified the inline assets.

Styles (unminified):

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Welcome!</title>
            <link rel="stylesheet" href="/assets/homePage.bundle.css" />
                    <style>
                body {
    font-family: "Comic Sans MS", cursive, sans-serif;
}
            </style>
            </head>

Scripts (unminified):

})({"essential.js":[function(require,module,exports) {
(function inlineJsExample() {
  console.log('HELLO, FROM AN INLINE JAVASCRIPT!');
})();
},{}]},{},["essential.js"], null)
            </script>

Summary

By this stage, you should hopefully have a functional Symfony 4 project containing examples of assets that were bundled and inlined using Parcel.js.

Be sure to do your own research around all the topics dicscussed here, as I'm fairly new to PHP myself and this is quite an early iteration of my Symfony / Parcel / Twig solution.

I hope that my article has been helpful to you, and please share a link around if you liked it.

If you liked that article, then why not try:

Using Redis as a cache inside Symfony 4

I've been writing a lot of PHP over the last few months, and have been getting a decent amount of experience with the Symfony framework. In my Symfony 4 project, I wanted to swap out the default memory caching for a Redis database, and thought that it might be helpful to others if I shared my solution. This is only a very simple example, but it should illustrate the basic steps that I took to get Redis caching up and running.

  • php
  • symfony
  • twig
  • javascript
  • parcel