Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Mapping 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.

AssetMapper isn't that big of a deal. Sure it dresses cool and has good dance moves, but it's really quite simple. It has two main features.

Feature number one: we configure "paths" - like the assets/ directory - and it makes the files inside available publicly.

Let's see this in action. If you downloaded the course code, you should have a tutorial/ directory with an important penguin.png file inside. Copy that. Inside assets/, we can organize things however we want. So let's create an images/ directory and transport our penguin there.

Now, remember, without the magic of AssetMapper, the only files that our browser should be able to access are those inside the public/ directory. So it should be impossible to add an img tag that loads our penguin. But... it is possible.

Using the "Logical Path"

Head into, how about, templates/base.html.twig. Anywhere - I'll go above the body block - add an img with src="{{ asset() }}" passing this the path to our file relative to the assets/ directory. So images/penguin.png.

... lines 1 - 20
<body class="bg-gray-800 text-white">
... lines 22 - 49
<img src="{{ asset('images/penguin.png') }}">
... lines 51 - 69
... lines 71 - 72

That's it. This is known as the "logical path" to the asset. Because we've pointed AssetMapper at the assets/ directory, we can refer to things inside of that via their path relative to that root.

And there's a great way to see all assets that are in the AssetMapper paths by going to the terminal and running:

php bin/console debug:asset

Awesome! First, on top, it shows the AssetMapper paths, including the assets/ directory. This project also has Pagerfanta installed. And we're already seeing how bundles can add their own AssetMapper paths to make their own files available publicly. This won't be important for us, but we could point the browser at any files inside that directory of the bundle.

Below, we see our image file, our CSS file, and our JavaScript file. These are their filesystem paths and these are their logical paths.

Versioned Filenames

The point is, by using the asset() function and the logical path to an asset, when we refresh... it works! Woh! And if we Inspect Element, check out the URL! It contains a version hash in the middle! I'm actually going to view the page source... it's a little easier to see.

So not only is penguin.png available publicly, but the path is not just penguin.png: it contains a version hash. If we modified the source penguin.png file - like gave it a cool bowtie - the version hash would automatically change forcing anyone using our site to download the fresh file. Booya!

This is also how app.css is loaded! Up near the top, the link tag uses asset('styles/app.css'), which is the logical path in AssetMapper to that file. And so it also output with a nice, versioned filename.

How Are the Files Made Public?

Okay, but how does this work? If you're like me, you want to know how the sausages are made. Well, in the dev environment, it works thanks to a core event listener... basically a fancy, internal Symfony controller.

For example, when the browser loads this image, that request goes through Symfony. It sees that we're trying to load /assets/images/penguin-versionhash.png, it finds the source file and serves it.

We can prove it! On the main tab, click any icon on the web debug toolbar to go into the profiler, then click "Last 10" to see the 10 most recent requests through Symfony. And there it is: the request that served the penguin image. Adorable.

In production, loading our files through Symfony would not be fast enough. So, instead, during deploy, you'll run a new console command:

php bin/console asset-map:compile

We're going to talk more about deployment later. But this is really cool! It copies each file of every AssetMapper path into the public/assets/ directory using their versioned filename. That's it.

Suddenly, this file is no longer being served by Symfony: we're seeing a real, physical file! Over in public/assets/, yep! We can see the final files in all their glory.

But... while we're developing, remove that directory so that everything continues to load dynamically.

Moving favicons into AssetMapper

And actually, while we're here, see those favicons inside the public/ directory? We're linking to them at the top of base.html.twig. That totally works: the asset() function can still refer to things inside the public/ directory.

... lines 1 - 2
... lines 4 - 7
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16x16.png') }}">
... lines 11 - 19
... lines 21 - 72

But... with almost no work, we can add free asset versioning to these! Step 1: move them into the assets/images/ directory. Step 2: prefix each path with images/ to get their logical path.

And... just like that, we still see the favicon up here... but more importantly, if we view the page source, those are now versioned!

Let's go a bit deeper into CSS files next. Like, how can we refer to background images from inside of CSS... if the final filename is versioned?

Leave a comment!

Login or Register to join the conversation

Hi, nice to find that symfony goes back, abandoning once and for all the prebuilt js in favor of assets, among other things that of assets is a very old (not bad) concept used by an excellent framework (I won't name names) considered minor compared to symfony. Does this method promise to become the best way to deal with css and js or is it just an alternative? Thanks


Hey @pasquale_pellicani!

nice to find that symfony goes back, abandoning once and for all the prebuilt js in favor of assets

I'm pretty happy too - it's really nice to work with :)

Does this method promise to become the best way to deal with css and js or is it just an alternative

I can't say for sure. My best guess right now is that we, for the next few years, will have 2 paths. The "build" systems are now so widely used, that I think it will take time for people to realize that they don't need it. And, build systems WILL still exist forever, at the very least, to handle things like JSX, Vue, etc. The challenge over the next 12 months will be to (A) continue improving the DX for asset mapper (we have some work to do on 6.4 for this, especially related to making CSS nicer to use I think) and (B) reminding people this exists and it's excellent! It also helps that we're leveraging "web standards" (e.g. importmaps, ECMAScript code).

Anyway, we'll see. But there is nothing preventing this method from becoming the main way of handling things for non-SPA sites.


1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^4.0", // v4.2.0
        "doctrine/doctrine-bundle": "^2.7", // 2.10.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
        "doctrine/orm": "^2.12", // 2.15.2
        "knplabs/knp-time-bundle": "^1.18", // v1.20.0
        "pagerfanta/doctrine-orm-adapter": "^4.0", // v4.1.0
        "pagerfanta/twig": "^4.0", // v4.1.0
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.1
        "symfony/asset": "6.3.*", // v6.3.0
        "symfony/asset-mapper": "6.3.*", // v6.3.0
        "symfony/console": "6.3.*", // v6.3.0
        "symfony/dotenv": "6.3.*", // v6.3.0
        "symfony/flex": "^2", // v2.3.1
        "symfony/framework-bundle": "6.3.*", // v6.3.0
        "symfony/http-client": "6.3.*", // v6.3.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.3.*", // v6.3.0
        "symfony/runtime": "6.3.*", // v6.3.0
        "symfony/stimulus-bundle": "^2.9", // v2.9.1
        "symfony/twig-bundle": "6.3.*", // v6.3.0
        "symfony/ux-turbo": "^2.9", // v2.9.1
        "symfony/web-link": "6.3.*", // v6.3.0
        "symfony/yaml": "6.3.*", // v6.3.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.6.1
        "twig/twig": "^2.12|^3.0" // v3.6.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/debug-bundle": "6.3.*", // v6.3.0
        "symfony/maker-bundle": "^1.41", // v1.49.0
        "symfony/stopwatch": "6.3.*", // v6.3.0
        "symfony/web-profiler-bundle": "6.3.*", // v6.3.0
        "zenstruck/foundry": "^1.21" // v1.33.0