Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Assets, CSS, Images, etc

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.

If you download the course code from the page where you're watching this video, after unzipping, you'll find a start/ directory that contains the same brand new Symfony 6 app that we created earlier. You don't actually need that code, but it does contain one extra directory called tutorial/, like I have here. This holds some files that we're about to use.

So let's talk about our next goal: to make this site look like a real site... instead of looking like something I designed myself. And that means we need a true HTML layout that brings in some CSS.

Adding a Layout & CSS Files

We know that our layout file is base.html.twig... and there's also a base.html.twig file in the new tutorial/ directory. Copy that... paste it into templates, and override the original.

Before we look at that, also copy the three .png files and put those into the public/ directory... so that our users can access them.

Beautiful. Open up the new base.html.twig file. There's nothing special here. We bring in some external CSS files from some CDNs for Bootstrap and FontAwesome. By the end of this tutorial, we'll refactor this into a fancier way of handling CSS... but for right now, this will work great.

But otherwise, everything is still hardcoded. We have some hardcoded navigation, the same block body... and a hardcoded footer. Let's go see what it looks like. Refresh and woo! Well, not perfect, but better!

Adding a Custom CSS File

The tutorial/ directory also holds an app.css file with custom CSS. To make this publicly available so that our user's browser can download it, it needs to live somewhere in the public/ directory. But it doesn't matter where or how you organize things inside.

Let's create a styles/ directory... and then copy app.css... and paste it there.

Back in base.html.twig, head to the top. After all the external CSS files, let's add a link tag for our app.css. So <link rel="stylesheet" and href="". Because the public/ directory is our document root, to refer to a CSS or image file there, the path should be with respect to that directory. So this will be /styles/app.css.

<!DOCTYPE html>
<html>
<head>
... lines 4 - 15
<link rel="stylesheet" href="/styles/app.css">
... lines 17 - 25
</head>
... lines 27 - 85
</html>

Let's check it. Refresh now and... even better!

The asset() Function

I want you to notice something. So far, Symfony is not involved at all in how we organize or use images or CSS files. Nope. Our setup is dead simple: we put things in the public/ directory... then refer to them with their paths.

But does Symfony have any cool features to help work with CSS and JavaScript? Absolutely. It's called Webpack Encore and Stimulus. And we'll talk about both of those towards the end of the tutorial.

But even in this simple setup - where we just put files in public/ and point to them - Symfony does have one minor feature: the asset() function.

It works like this: instead of using /styles/app.css, say {{ asset() }} and then, inside quotes, move our path there... but without the opening "/".

<!DOCTYPE html>
<html>
<head>
... lines 4 - 15
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
... lines 17 - 25
</head>
... lines 27 - 85
</html>

So the path is still relative to the public/ directory... you just don't need to include the first "/".

Before we talk about what this does... let's go see if it works. Refresh and... it doesn't! Error:

Unknown function: did you forget to run composer require symfony/asset.

I keep saying that Symfony starts small... and then you install things as you need them. Apparently, this asset() function comes from a part of Symfony that we don't have yet! But getting it is easy. Copy this composer require command, spin over to your terminal and run it:

composer require symfony/asset

This is a pretty simple install: it downloads just this one package... and there are no recipes.

But when we try the page now... it works! Check out the HTML source. Interesting: the link tag's href is still, literally, /styles/app.css. That's exactly what we had before! So what the heck is this asset() function doing?

The answer is... not much. But it's still a good idea to use. The asset() function gives you two features. First, imagine you deploy to a sub-directory of a domain. Like, the homepage lives at https://example.com/mixed-vinyl.

If that were the case, then in order for our CSS to work, the href would need to be /mixed-vinyl/styles/app.css. In this situation, the asset() function would detect the sub-directory automatically and add that prefix for you.

The second - and more important thing that the asset() function does - is allow you to easily switch to a CDN later. Because this path is now going through the asset() function, we could, via a configuration file, say:

Hey Symfony! When you output this path, please prefix it with the URL to my CDN.

This means that, when we load the the page, instead of href="/styles/app.css, it would be something like https://mycdn.com/styles/app.css.

So the asset() function might not be doing anything you need today, but anytime you reference a static file - whether it's a CSS file, JavaScript file, image, whatever, use this function.

In fact, up here, I'm referencing three images. Let's use asset: {{ asset()... and then it auto-completes the path! Thanks Symfony plugin! Repeat this for the second image... and the third.

<!DOCTYPE html>
<html>
<head>
... lines 4 - 6
<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 10 - 15
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
... lines 17 - 25
</head>
... lines 27 - 85
</html>

We know this won't make any difference today... we can refresh the HTML source to see the same paths... but we're ready for a CDN in the future.

Homepage And Browse Page HTML

So the layout now looks great! But the content for our homepage is... just kind of hanging out... looking weird... like me in middle school. Back in the tutorial/ directory, copy the homepage template... and overwrite our original file.

Open that up. This still extends base.html.twig... and it still overrides the body block. And then, it has a bunch of completely hard coded HTML. Let's go see what it looks like. Refresh and... it looks awesome!

Except that... it's 100% hard coded. Let's fix that. All the way on top, here's the name of our record, print the title variable.

And then, below for the songs.. we have a long list of hardcoded HTML. Let's turn this into a loop. Add {% for track in tracks %} like we had before. And... at the bottom, endfor.

For the song details, use track.song... and track.artist. And now we can remove all the hardcoded songs.

{% extends 'base.html.twig' %}
... lines 2 - 4
{% block body %}
<div class="container">
<h1 class="d-inline me-3">{{ title }}</h1> <i class="fas fa-edit"></i>
<div class="row mt-5">
... lines 9 - 34
<div class="col-12 col-md-8 ps-5">
<h2 class="mb-4">10 songs (30 minutes of 60 still available)</h2>
{% for track in tracks %}
<div class="song-list">
<div class="d-flex mb-3">
<a href="#">
<i class="fas fa-play me-3"></i>
</a>
<span class="song-details">{{ track.song }} - {{ track.artist }}</span>
<a href="#">
<i class="fas fa-bars mx-3"></i>
</a>
<a href="#">
<i class="fas fa-times"></i>
</a>
</div>
</div>
{% endfor %}
<button type="button" class="btn btn-success"><i class="fas fa-plus"></i> Add a song</button>
</div>
</div>
</div>
{% endblock %}

Sweet! Let's try that. Hey! It's coming to life people!

One more page to go! The /browse page. You know the drill: copy browse.html.twig, and paste into our directory. This looks a lot like the homepage: it extends base.html.twig and overrides block body.

Over in VinylController, we weren't rendering a template before... so let's do that now: return $this->render('vinyl/browse.html.twig') and let's pass in the genre. Add a variable for that: $genre = and if we have a slug... use our fancy title-case code, else set this to null. Then delete the $title stuff... and pass genre into Twig.

<?php
... lines 3 - 9
class VinylController extends AbstractController
{
... lines 12 - 29
#[Route('/browse/{slug}')]
public function browse(string $slug = null): Response
{
$genre = $slug ? u(str_replace('-', ' ', $slug))->title(true) : null;
return $this->render('vinyl/browse.html.twig', [
'genre' => $genre
]);
}
}

Back in the template, use this in the h1. In Twig, we can also use fancy syntax. So if we have a genre, print genre, else print All Genres.

{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<h1>Browse {{ genre ? genre : 'All Genres' }}</h1>
... lines 6 - 45
</div>
{% endblock %}

Testing time. Head over to /browse: "Browse all genres"! And then /browse/death-metal: Browse Death Metal. Friends, this is starting to feel like a real site!

Except that these links up in the nav... go nowhere! Let's fix that next by learning how to generate URLs. We're also going to meet the mega-powerful bin/console command line tool.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    },
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3
    }
}