Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Webpacking our First Assets

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

So, Webpack only needs to know three things. The first - setOutputPath() - tells it where to put the final, built files and the second - setPublicPath() - tells it the public path to this directory:

... lines 1 - 2
// directory where compiled assets will be stored
// public path used by the web server to access the output path
... lines 8 - 65
... lines 67 - 68

The Entry File

The third important piece, and where everything truly starts, is addEntry():


The Encore recipe now puts app.js in assets/app.js. But the purpose of the file is exactly the same!

... lines 1 - 2
... lines 4 - 10
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if you JavaScript imports CSS.
.addEntry('app', './assets/js/app.js')
//.addEntry('page1', './assets/js/page1.js')
//.addEntry('page2', './assets/js/page2.js')
... lines 23 - 65
... lines 67 - 68

Here's the idea: we point Webpack at just one JavaScript file - assets/js/app.js. Then, it parses through all the import statements it finds, puts all the code together, and outputs one file in public/build called app.js. The first argument - app - is the entry's name, which can be anything, but it controls the final filename: app becomes public/build/app.js.

And the recipe gave us a few files to start. Open up assets/js/app.js:


The recipe now puts CSS files into an assets/styles/ directory. So, assets/styles/app.css - but the purpose of all these files is the same.

15 lines assets/js/app.js
* Welcome to your app's main JavaScript file!
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
// any CSS you require will output into a single css file (app.css in this case)
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
// const $ = require('jquery');
console.log('Hello Webpack Encore! Edit me in assets/js/app.js');

This is the file that Webpack will start reading. There's not much here yet - a console.log() and... woh! There is one cool thing: a require() call to a CSS file! We'll talk more about this later, but in the same way that you can import other JavaScript files, you can import CSS too! And, by the way, this require() function and the import statement we saw earlier on Webpack's docs, do basically the same thing. More on that soon.

To make the CSS a bit more obvious, open app.css and change the background to lightblue and add an !important so it will override my normal background:

body {
background-color: lightblue !important;


Before we execute Encore, back in webpack.config.js, we need to make one other small tweak. Find the enableSingleRuntimeChunk() line, comment it out, and put disableSingleRuntimeChunk() instead:

... lines 1 - 2
... lines 4 - 26
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
... lines 31 - 66
... lines 68 - 69

Don't worry about this yet - we'll see exactly what it does later.

Running Encore

Ok! We've told Webpack where to put the built files and which one file to start parsing. Let's do this! Find your terminal and run the Encore executable with:

./node_modules/.bin/encore dev


For Windows, your command may need to be node_modules\bin\encore.cmd dev

Because we want a development build. And... hey! A nice little notification that it worked!

And... interesting - it built two files: app.js and app.css. You can see them inside the public/build directory. The app.js file... well... basically just contains the code from the assets/js/app.js file because... that file didn't import any other JavaScript files. We'll change that soon. But our app entry file did require a CSS file:

15 lines assets/js/app.js
... lines 1 - 7
// any CSS you require will output into a single css file (app.css in this case)
... lines 10 - 15

And yea, Webpack understands this!

Here's the full flow. First, Webpack looks at assets/js/app.js. It then looks for all the import and require() statements. Each time we import a JavaScript file, it puts those contents into the final, built app.js file. And each time we import a CSS file, it puts those contents into the final, built app.css file.

Oh, and the final filename - app.css? It's app.css because our entry is called app. If we changed this to appfoo.css, renamed the file, then ran Encore again, it would still build app.js and app.css files thanks to the first argument to addEntry():

... lines 1 - 2
... lines 4 - 19
.addEntry('app', './assets/js/app.js')
... lines 21 - 66
... lines 68 - 69

What this means is... we now have one JavaScript file that contains all the code we need and one CSS file that contains all the CSS! All we need to do is add them to our page!

Open up templates/base.html.twig. Let's keep the existing stylesheets for now and add a new one: <link rel="stylesheet" href=""> the asset() function and the public path: build/app.css:

<!doctype html>
<html lang="en">
... lines 5 - 8
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
... lines 11 - 14
{% endblock %}
... lines 17 - 107

At the bottom, add the script tag with src="{{ asset('build/app.js') }}".


In new Symfony projects, the javascripts block is at the top of this file - inside the <head> tag. We'll learn more about why in a few minutes.

<!doctype html>
<html lang="en">
... lines 3 - 17
... lines 19 - 90
{% block javascripts %}
<script src="{{ asset('build/app.js') }}"></script>
... lines 93 - 105
{% endblock %}

If you're not familiar with the asset() function, it's not doing anything important for us. Because the build/ directory is our document root, we're literally pointing to the public path.

Let's try it! Move over, refresh and... hello, weird blue background. And in the console... yes! There's the log!


If you're coding along with a fresh Symfony project, you likely will not see the console.log() being printed. That's ok! In the next chapter, you'll learn about a Twig function that will render some <script> tags that you're currently missing.

We've only started to scratch the surface of the possibilities of Webpack. So if you're still wondering: "why is going through this build process so useful?". Stay tuned. Because next, we're going to talk about the require() and import statements and start organizing our code.

Leave a comment!

This tutorial works great with Symfony5!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.91.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.3.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.5
        "symfony/console": "^4.0", // v4.2.5
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.5
        "symfony/framework-bundle": "^4.0", // v4.2.5
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.5
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.5
        "symfony/validator": "^4.0", // v4.2.5
        "symfony/web-server-bundle": "^4.0", // v4.2.5
        "symfony/webpack-encore-bundle": "^1.4", // v1.5.0
        "symfony/yaml": "^4.0", // v4.2.5
        "twig/extensions": "^1.5" // v1.5.4
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.5
        "symfony/dotenv": "^4.0", // v4.2.5
        "symfony/maker-bundle": "^1.0", // v1.11.5
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.5
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.5

What JavaScript libraries does this tutorial use?

// package.json
    "devDependencies": {
        "@symfony/webpack-encore": "^0.27.0", // 0.27.0
        "autocomplete.js": "^0.36.0",
        "autoprefixer": "^9.5.1", // 9.5.1
        "bootstrap": "^4.3.1", // 4.3.1
        "core-js": "^3.0.0", // 3.0.1
        "dropzone": "^5.5.1", // 5.5.1
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.4.0", // 3.4.0
        "popper.js": "^1.15.0",
        "postcss-loader": "^3.0.0", // 3.0.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.1", // 7.3.1
        "sortablejs": "^1.8.4", // 1.8.4
        "webpack-notifier": "^1.6.0" // 1.7.0