10.

Copying Files

Share this awesome video!

|

Do a force refresh on the homepage. Ok, we've got some broken images. Inspect that. Of course: this points to /images/meteor-shower.jpg.

Open this template: article/homepage.html.twig. There it is:

65 lines | templates/article/homepage.html.twig
// ... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
<!-- H1 Article -->
<a class="main-article-link" href="#">
<div class="main-article mb-5 pb-3">
<img src="{{ asset('images/meteor-shower.jpg') }}" alt="meteor shower">
// ... line 15
</div>
</a>
// ... lines 18 - 40
</div>
// ... lines 42 - 61
</div>
</div>
{% endblock %}

A normal asset() function pointing to images/meteor-shower.jpg. That's broken because we moved our entire images/ directory out of public/ and into assets/.

There's a nice side-effect of using a build system like Webpack: you don't need to keep your CSS, JavaScript or assets in a public directory anymore! You put them in assets/, organize them however you want, and the end-user will only ever see the final, built version.

But unless you're building a single page application, you'll probably still have some cases where you want to render a good, old-fashioned img tag. And because this image is not being processed through Webpack, it's not being copied into the final build/ directory.

Hello copyFiles()

To make life more joyful, Encore has a feature for exactly this situation. Open up webpack.config.js. And, anywhere in here, say .copyFiles() and pass this a configuration object:

73 lines | webpack.config.js
// ... lines 1 - 2
Encore
// ... lines 4 - 53
.copyFiles({
// ... line 55
})
// ... lines 57 - 70
;
// ... lines 72 - 73

Tip

If you're using Encore 1.0 or later, you'll also need to install file-loader. As soon as you use copyFiles(), check your Encore terminal tab: it will have the exact command you need to run.

Obviously... this function helps you copy files from one place to another. Neato! But... how exactly do we use it? One of the nicest things about Encore is that its code is extremely well-documented. Hold Command or Ctrl and click copyFiles(). It jumps us straight to the index.js file of Encore... which is almost entirely small methods with HUGE docs above them! This is a great resource for finding out, not only how you can use a function, but what functions and features are even available!

For copyFiles(), it can be as simple as:

I want to copy everything from assets/images into my build directory.

Yea, that sounds about right. If we did that, we could then reference those images from our img tags. Copy that config, go back to webpack.config.js and paste. Oh, I have an extra set of curly braces:

73 lines | webpack.config.js
// ... lines 1 - 2
Encore
// ... lines 4 - 53
.copyFiles({
from: './assets/images'
})
// ... lines 57 - 70
;
// ... lines 72 - 73

And because we just made a change to webpack.config.js, find your terminal, press Ctrl+C, and re-run Encore. When that finishes... go check it out. In the public/build/ directory, there they are: meteor-shower.jpg, space-ice.png and so on.

Controlling the copy Destination

Um, but it is kind of lame that it just dropped them directly into build/, I'd rather, for my own sanity, copy these into build/images/.

Let's see... go back to the docs. Here it is: you can give it a destination... and this has a few wildcards in it, like [path], [name] and [ext]. Oh, but use this second one instead: it gives us built-in file versioning by including a hash of the contents in the filename.

Back in our config, paste that:

74 lines | webpack.config.js
// ... lines 1 - 2
Encore
// ... lines 4 - 53
.copyFiles({
from: './assets/images',
to: 'images/[path][name].[hash:8].[ext]'
})
// ... lines 58 - 71
;
// ... lines 73 - 74

Before we restart Encore, shouldn't we delete some of these old files... at least to get them out of the way and clean things up? Nope! Well, yes, but it's already happening. One other optional feature that we're using is called cleanOutputBeforeBuild():

74 lines | webpack.config.js
// ... lines 1 - 2
Encore
// ... lines 4 - 38
.cleanupOutputBeforeBuild()
// ... lines 40 - 71
;
// ... lines 73 - 74

This is responsible for emptying the build/ directory each time we build.

Ok, go restart Encore: Ctrl+C, then:

yarn watch

Let's go check it out! Beautiful! Everything now copies to images/ and includes a hash.

Public Path to Versioned Copied Files: manifest.json

Oh, but... that's a problem. What path are we supposed to use for the img tag? Should we put build/images/meteor-shower.5c77...jpg? No, because if we ever updated that image, the hash would change and all our img tags would break. And because they aren't being processed by Webpack, that failure would be the worst kind: it would fail silently!

In the build/ directory, there are two special JSON files generated by Encore. The first - entrypoints.json - is awesome because the Twig helpers can use it to generate all of the script and link tags for an entry. But there's another file: manifest.json.

This is a big, simple, beautiful map that contains every file that Encore outputs. It maps from the original filename to the final filename. For most files, because we haven't activated versioning globally yet, the paths are the same. But check out the images! It maps from build/images/meteor-shower.jpg to the real, versioned path! If we could read this file, we could automagically get the correct hash!

When we installed WebpackEncoreBundle, the recipe added a config/packages/assets.yaml file. Inside, oh! It has json_manifest_path set to the path to manifest.json:

framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

The significance of this line is that anytime we use the asset() function in Twig, it will take that path and look for it inside of manifest.json. If it finds it, it will use the final, versioned path.

This means that if we want to point to meteor-shower.jpg, all we need to do is use the build/images/meteor-shower.jpg path. Copy that, go to the homepage template, and paste it here:

65 lines | templates/article/homepage.html.twig
// ... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
<!-- H1 Article -->
<a class="main-article-link" href="#">
<div class="main-article mb-5 pb-3">
<img src="{{ asset('build/images/meteor-shower.jpg') }}" alt="meteor shower">
// ... line 15
</div>
</a>
// ... lines 18 - 40
</div>
// ... lines 42 - 61
</div>
</div>
{% endblock %}

There are a few other images tags in this file. Search for <img. This is pointing to an uploaded file, not a static file - so, that's good. Ah, but this one needs to change: build/images/alien-profile.png:

65 lines | templates/article/homepage.html.twig
// ... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
// ... lines 10 - 20
{% for article in articles %}
<div class="article-container my-1">
<a href="{{ path('article_show', {slug: article.slug}) }}">
// ... line 24
<div class="article-title d-inline-block pl-3 align-middle">
// ... lines 26 - 34
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('build/images/alien-profile.png') }}"> {{ article.author }} </span>
// ... line 36
</div>
</a>
</div>
{% endfor %}
</div>
// ... lines 42 - 61
</div>
</div>
{% endblock %}

And one more, add build/ before space-ice.png:

65 lines | templates/article/homepage.html.twig
// ... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
// ... lines 6 - 45
<div class="col-sm-12 col-md-4 text-center">
<div class="ad-space mx-auto mt-1 pb-2 pt-2">
<img class="advertisement-img" src="{{ asset('build/images/space-ice.png') }}">
// ... lines 49 - 50
</div>
// ... lines 52 - 60
</div>
</div>
</div>
{% endblock %}

Let's try it! Move over, refresh and... we got it! Inspect element: it's the final, versioned filename. Let's update the last img tags - they're in show.html.twig. Search for img tags again, then... build/, build/ and build/:

86 lines | templates/article/show.html.twig
// ... lines 1 - 4
{% block content_body %}
<div class="row">
<div class="col-sm-12">
// ... line 8
<div class="show-article-title-container d-inline-block pl-3 align-middle">
// ... lines 10 - 11
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('build/images/alien-profile.png') }}"> {{ article.author }} </span>
// ... lines 13 - 24
</div>
</div>
</div>
// ... lines 28 - 39
<div class="row">
<div class="col-sm-12">
// ... lines 42 - 44
<div class="row mb-5">
<div class="col-sm-12">
<img class="comment-img rounded-circle" src="{{ asset('build/images/astronaut-profile.png') }}">
// ... lines 48 - 54
</div>
</div>
{% for comment in article.nonDeletedComments %}
<div class="row">
<div class="col-sm-12">
<img class="comment-img rounded-circle" src="{{ asset('build/images/alien-profile.png') }}">
// ... lines 62 - 71
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
// ... lines 80 - 86

Click to go view one of the articles. These comment avatars are now using the system.

copyFiles() is nice because it lets you keep all your frontend files in the same directory... even if some need to be copied to the build directory. But to sweeten the deal, you're rewarded with free asset versioning.

By the way, this function was added by @Lyrkan, one of the core devs for Encore and... even though it's pretty simple, it's an absolutely brilliant implementation that I haven't seen used anywhere else. So, if you like it, give him a thanks on Symfony Slack or Twitter.

Next, let's create multiple entry points to support page-specific CSS and JavaScript.