This tutorial has a new version, check it out!

Assets: CSS & JavaScript

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.

Even astronauts - who generally spend their time staring into the black absyss - demand a site that is less ugly than this! Let's fix that!

If you download the course code from the page that you're watching this video on right now, inside the zip file, you'll find a start/ directory. And inside that, you'll see the same tutorial/ directory that I have here. And inside that... I've created a new base.html.twig. Copy that and overwrite our version in templates/:

<!doctype html>
<html lang="en">
<head>
<title>{% block title %}Welcome to the SpaceBar{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block stylesheets %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
<a style="margin-left: 75px;" class="navbar-brand space-brand" href="#">The Space Bar</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a style="color: #fff;" class="nav-link" href="#">Local Asteroids</a>
</li>
<li class="nav-item">
<a style="color: #fff;" class="nav-link" href="#">Weather</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-info my-2 my-sm-0" type="submit">Search</button>
</form>
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown" style="margin-right: 75px;">
<a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="nav-profile-img rounded-circle" src="images/astronaut-profile.png">
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="#">Profile</a>
<a class="dropdown-item" href="#">Create Post</a>
<a class="dropdown-item" href="#">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
{% block body %}{% endblock %}
<footer class="footer">
<div class="container text-center">
<span class="text-muted">Made with <i class="fa fa-heart" style="color: red;"></i> by the guys and gals at <a href="https://knpuniversity.com">KnpUniversity</a></span>
</div>
</footer>
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script>
$('.dropdown-toggle').dropdown();
</script>
{% endblock %}
</body>
</html>

On a technical level, this is basically the same as before: it has the same blocks: title stylesheets, body and javascripts at the bottom. But now, we have a nice HTML layout that's styled with Bootstrap.

If you refresh, it should look better. Woh! No change! Weird! Actually... this is more weird than you might think. Find your terminal and remove the var/cache/dev directory:

rm -rf var/cache/dev/*

What the heck is this? Internally, Symfony caches things in this directory. And... you normally don't need to think about this at all: Symfony is smart enough during development to automatically rebuild this cache whenever necessary. So... why am I manually clearing it? Well... because we copied my file... and because its "last modified" date is older than our original base.html.twig, Twig gets confused and thinks that the template was not updated. Seriously, this is not something to worry about in any other situation.

Referencing CSS Files

And when we refresh... there it is! Ok, it's still pretty ugly. That's because we're missing some CSS files!

In the tutorial/ directory, I've also prepped some css/, fonts/ and images/. All of these files need to be accessed by the user's browser, and that means they must live inside public/. Open that directory and paste them there.

By the way, Symfony has an awesome tool called Webpack Encore that helps process, combine, minify and generally do amazing things with your CSS and JS files. We are going to talk about Webpack Encore... but in a different tutorial. For now, let's get things setup with normal, static files.

The two CSS files we want to include are font-awesome.css and styles.css. And we don't need to do anything complex or special! In base.html.twig, find the stylesheets block and add a link tag.

But wait, why exactly are we adding the link tag inside the stylesheets block? Is that important? Well, technically... it doesn't matter: a link tag can live anywhere in head. But later, we might want to add additional CSS files on specific pages. By putting the link tags inside this block, we'll have more flexibility to do that. Don't worry: we're going to see an example of this with a JavaScript file soon.

So... what path should we use? Since public/ is the document root, it should just be /css/font-awesome.css:

<!doctype html>
<html lang="en">
<head>
... lines 5 - 8
{% block stylesheets %}
... line 10
<link rel="stylesheet" href="/css/font-awesome.css">
... line 12
{% endblock %}
</head>
... lines 15 - 67
</html>

Do the same thing for the other file: /css/styles.css:

<!doctype html>
<html lang="en">
<head>
... lines 5 - 8
{% block stylesheets %}
... line 10
<link rel="stylesheet" href="/css/font-awesome.css">
<link rel="stylesheet" href="/css/styles.css">
{% endblock %}
</head>
... lines 15 - 67
</html>

It's that simple! Refresh! Still not perfect, but much better!

The Not-So-Mystical asset Function

And now I'm going to slightly complicate things. Go back into PhpStorm's Preferences, search for "Symfony" and find the "Symfony" plugin. Change the "web" directory to public - it was called web in Symfony 3.

This is not required, but it will give us more auto-completion when working with assets. Delete the "font-awesome" path, re-type it, and hit tab to auto-complete:

<!doctype html>
<html lang="en">
<head>
... lines 5 - 8
{% block stylesheets %}
... line 10
<link rel="stylesheet" href="{{ asset('css/font-awesome.css') }}">
... line 12
{% endblock %}
</head>
... lines 15 - 67
</html>

Woh! It wrapped the path in a Twig asset() function! Do the same thing below for styles.css:

<!doctype html>
<html lang="en">
<head>
... lines 5 - 8
{% block stylesheets %}
... line 10
<link rel="stylesheet" href="{{ asset('css/font-awesome.css') }}">
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
{% endblock %}
</head>
... lines 15 - 67
</html>

Here's the deal: whenever you link to a static asset - CSS, JS or images - you should wrap the path in this asset() function. But... it's not really that important. In fact, right now, it doesn't do anything: it will print the same path as before. But! In the future, the asset() function will give us more flexibility to version our assets or store them on a CDN.

In other words: don't worry about it too much, but do remember to use it!

Installing the asset Component

Actually, the asset() function does do something immediately - it breaks our site! Refresh! Ah!

The asset() function comes from a part of Symfony that we don't have installed yet. Fix that by running:

composer require asset

This installs the symfony/asset component. And as soon as Composer is done... we can refresh, and it works! To prove that the asset() function isn't doing anything magic, you can look at the link tag in the HTML source: it's the same boring /css/styles.css.

There is one other spot where we need to use asset(). In the layout, search for img. Ah, an img tag! Remove the src and re-type astronaut-profile:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown" style="margin-right: 75px;">
<a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="nav-profile-img rounded-circle" src="{{ asset('images/astronaut-profile.png') }}">
</a>
... lines 40 - 44
</li>
</ul>
</div>
</nav>
... lines 49 - 66
</body>
</html>

Perfect! Refresh and enjoy our new avatar on the user menu. There's a lot of hardcoded data, but we'll make this dynamic over time.

Styling the Article Page

The layout is looking great! But the inside of the page... yea... that's still pretty terrible. Back in the tutorial/ directory, there is also an article.html.twig file. Don't copy this entire file - just copy its contents. Close it and open show.html.twig. Paste the new code at the top of the body block:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
<img class="show-article-img" src="{{ asset('images/asteroid.jpeg') }}">
<div class="show-article-title-container d-inline-block pl-3 align-middle">
<span class="show-article-title ">Why do Asteroids Taste Like Bacon?</span>
<br>
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/alien-profile.png') }}"> Mike Ferengi </span>
<span class="pl-2 article-details"> 3 hours ago</span>
<span class="pl-2 article-details"> 5 <a href="#" class="fa fa-heart-o like-article"></a> </span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="article-text">
<p>Spicy jalapeno bacon ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow,
lorem proident beef ribs aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow
turkey shank eu pork belly meatball non cupim.</p>
<p>Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder,
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt
occaecat lorem meatball prosciutto quis strip steak.</p>
<p>Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck
fugiat.</p>
<p>Sausage tenderloin officia jerky nostrud. Laborum elit pastrami non, pig kevin buffalo minim ex quis. Pork belly
pork chop officia anim. Irure tempor leberkas kevin adipisicing cupidatat qui buffalo ham aliqua pork belly
exercitation eiusmod. Exercitation incididunt rump laborum, t-bone short ribs buffalo ut shankle pork chop
bresaola shoulder burgdoggen fugiat. Adipisicing nostrud chicken consequat beef ribs, quis filet mignon do.
Prosciutto capicola mollit shankle aliquip do dolore hamburger brisket turducken eu.</p>
<p>Do mollit deserunt prosciutto laborum. Duis sint tongue quis nisi. Capicola qui beef ribs dolore pariatur.
Minim strip steak fugiat nisi est, meatloaf pig aute. Swine rump turducken nulla sausage. Reprehenderit pork
belly tongue alcatra, shoulder excepteur in beef bresaola duis ham bacon eiusmod. Doner drumstick short loin,
adipisicing cow cillum tenderloin.</p>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<p class="share-icons mb-5"><span class="pr-1">Share:</span> <i class="pr-1 fa fa-facebook-square"></i><i class="pr-1 fa fa-twitter-square"></i><i class="pr-1 fa fa-reddit-square"></i><i class="pr-1 fa fa-share-alt-square"></i></p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<h3><i class="pr-3 fa fa-comment"></i>10 Comments</h3>
<hr>
<div class="row mb-5">
<div class="col-sm-12">
<img class="comment-img rounded-circle" src="{{ asset('images/astronaut-profile.png') }}">
<div class="comment-container d-inline-block pl-3 align-top">
<span class="commenter-name">Amy Oort</span>
<div class="form-group">
<textarea class="form-control comment-form" id="articleText" rows="1"></textarea>
</div>
<button type="submit" class="btn btn-info">Comment</button>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<img class="comment-img rounded-circle" src="{{ asset('images/alien-profile.png') }}">
<div class="comment-container d-inline-block pl-3 align-top">
<span class="commenter-name">Mike Ferengi</span>
<br>
<span class="comment"> Now would this be apple wood smoked bacon? Or traditional bacon - IMHO it makes a difference.</span>
<p><a href="#">Reply</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h1>{{ title }}</h1>
... lines 99 - 122

Check it out in your browser. Yep! It looks cool... but all of this info is hardcoded. I mean, that article name is just static text.

Let's take the dynamic code that we have at the bottom and work it into the new HTML. For the title, use {{ title }}:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
... line 13
<div class="show-article-title-container d-inline-block pl-3 align-middle">
<span class="show-article-title ">{{ title }}</span>
... lines 16 - 19
</div>
</div>
</div>
... lines 23 - 94
</div>
</div>
</div>
</div>
{% endblock %}

Below, it prints the number of comments. Replace that with {{ comments|length }}:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
... lines 11 - 60
<div class="row">
<div class="col-sm-12">
<h3><i class="pr-3 fa fa-comment"></i>{{ comments|length }} Comments</h3>
... lines 64 - 92
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Oh, and at the bottom, there is a comment box and one actual comment. Let's find this and... add a loop! For comment in comments on top, and endfor at the bottom. For the actual comment, use {{ comment }}:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
... lines 11 - 60
<div class="row">
<div class="col-sm-12">
... lines 63 - 78
{% for comment in comments %}
<div class="row">
<div class="col-sm-12">
... line 82
<div class="comment-container d-inline-block pl-3 align-top">
... lines 84 - 85
<span class="comment"> {{ comment }}</span>
... line 87
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Delete the old code from the bottom... oh, but don't delete the endblock:

... lines 1 - 4
{% block body %}
<div class="container">
... lines 8 - 97
</div>
{% endblock %}

Let's try it - refresh! It looks awesome! A bunch of things are still hardcoded, but this is much better.

It's time to make our homepage less ugly and learn about the second job of routing: route generation for linking.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "sensio/framework-extra-bundle": "^5.1", // v5.1.3
        "symfony/asset": "^4.0", // v4.0.3
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.9.10
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.3
        "symfony/web-server-bundle": "^4.0", // v4.0.3
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "sensiolabs/security-checker": "^5.0", // v5.0.3
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.3
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.3
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.3
    }
}