Stellar Development with Symfony 4
Stellar Development with Symfony 4
Hey guys! Yes! It's Symfony 4 time! I am so excited. Why? Because nothing makes me happier than sitting
down to work inside a framework where coding is actually fun, and where I can build features fast, but without
sacrificing quality. Well, maybe I'd be even happier doing all of that on a beach... with, maybe a cold drink?
Anyways, Symfony 4 completely re-imagined the developer experience: you're going to create better features,
faster than ever. And, Symfony has a new, unique super-power: it starts as a microframework, then
automatically scales in size as your project grows. How? Stay tuned...
Oh, and did I mention that Symfony 4 is the fastest version ever? And the fastest PHP framework? Honestly, all
frameworks are fast enough anyways, but the point is this: you're building on a seriously awesome foundation.
Tip
$ composer self-update
Install Symfony!
To download your new Symfony project, run composer create-project symfony/skeleton and put this into a new
directory called the_spacebar .
That's the name of our project! "The Spacebar" will be the place for people from across the galaxy to
communicate, share news and argue about celebrities and BitCoin. It's going to be amazing!
This command clones the symfony/skeleton project and then runs composer install to download its dependencies.
Further down, there's something special: something about "recipes". OooOOO. Recipes are a new and very
important concept. We'll talk about them in a few minutes.
$ cd the_spacebar
This starts the built-in PHP web server, which is great for development. public/ is the document root of the
project - but more on that soon!
Tip
If you want to use Nginx or Apache for local development, you can! See http://bit.ly/symfony-web-servers.
Time to blast off! Move to your browser and go to http://localhost:8000 . Say hello to your new Symfony app!
Tip
Symfony no longer creates a Git repository automatically for you. But, no problem! Just type git init once
to initialize your repository.
Back in the terminal, I'll create a new terminal tab. Symfony already inititalized a new git repository for us and
gave us a perfect .gitignore file. Thanks Symfony!
Tip
If you're using PhpStorm, you'll want to ignore the .idea directory from git. I already have it ignored in my
global .gitignore file: https://help.github.com/articles/ignoring-files/
$ git init
$ git add .
$ git commit
Woh! Check this out: the entire project - including Composer and .gitignore stuff - is only 16 files! Our app is
teenie-tiny!
Let's learn more about our project next and setup our editor to make Symfony development amazing!
Chapter 2: Our Micro-App & PhpStorm Setup
Our mission: to boldly go where no one has gone before... by checking out our app! I already opened the new
directory in PhpStorm, so fire up your tricorder and let's explore!
But, really , you'll almost never need to worry about it. In fact, now that we've talked about this directory, stop
thinking about it!
If you're familiar with Composer... that package name should look funny! Really, wrong! Normally, every
package name is "something" slash "something", like symfony/console . So... server just should not work! But it
does! This is part of a cool new system called Flex. More about that soon!
When this finishes, you can now run:
$ ./bin/console server:run
This does basically the same thing as before... but the command is shorter. And when we refresh, it still works!
By the way, this bin/console command is going to be our new robot side-kick. But it's not magic: our project has
a bin/ directory with a console file inside. Windows users should say php bin/console ... because it's just a PHP
file.
So, what amazing things can this bin/console robot do? Find your open terminal tab and just run:
$ ./bin/console
Yes! This is a list of all of the bin/console commands. Some of these are debugging gold . We'll talk about them
along the way!
PhpStorm Setup
Ok, we are almost ready to start coding! But we need talk about our spaceship, I mean, editor! Look, you can
use whatever your want... but... I highly recommend PhpStorm! Seriously, it makes developing in Symfony a
dream! And no, those nice guys & gals at PhpStorm aren't paying me to say this... but they can if they want to!
Ahem, If you do use it... which would be awesome for you... there are 2 secrets you need to know to trick out
your spaceship, ah, editor! Clearly I was in hyper-sleep too long.
Go to Preferences, Plugins, then click "Browse Repositories". There are 3 must-have plugins. Search for
"Symfony". First: the "Symfony Plugin". It has over 2 million downloads for a reason: it will give you tons of
ridiculous auto-completion. You should also download "PHP Annotations" and "PHP Toolbox". I already have
them installed. If you don't, you'll see an "Install" button right at the top of the description. Install those and
restart PHPStorm.
Then, come back to Preferences, search for "symfony" and find the new "Symfony" section. Click the "Enable
Plugin" checkbox: you need to enable the Symfony plugin for each project. It says you need to restart... but I
think that's lie. It's space! What could go wrong?
So that's PhpStorm trick #1. For the second, search "Composer" and click on the "Composer" section. Click to
browse for the "Path to composer.json" and select the one in our project. I'm not sure why this isn't
automatic... but whatever! Thanks to this, PhpStorm will make it easier to create classes in src/ . You'll see this
really soon.
Okay! Our project is set up and it's already working. Let's start building some pages and discovering more cool
things about new app.
Chapter 3: Routes, Controllers, Pages, oh my!
Let's create our first page! Actually, this is the main job of a framework: to give you a route and controller
system. A route is configuration that defines the URL for a page and a controller is a function that we write that
actually builds the content for that page.
And right now... our app is really small! Instead of weighing down your project with every possible feature you
could ever need - after all, we're not in zero-gravity yet - a Symfony app is basically just a small route-
controller system. Later, we'll install more features when we need them, like a warp drive! Those always come
in handy. Adding more features is actually going to be pretty awesome. More on that later.
4 lines config/routes.yaml
1 #index:
2 # path: /
3 # controller: App\Controller\DefaultController::index
Hey! We already have an example! Uncomment that. Ignore the index key for now: that's the internal name of
the route, but it's not important yet.
This says that when someone goes to the homepage - / - Symfony should execute an index() method in a
DefaultController class. Change this to ArticleController and the method to homepage :
4 lines config/routes.yaml
1 index:
2 path: /
3 controller: App\Controller\ArticleController::homepage
And... yea! That's a route! Hi route! It defines the URL and tells Symfony what controller function to execute.
The controller class doesn't exist yet, so let's create it! Right-click on the Controller directory and go to "New"
or press Cmd + N on a Mac. Choose "PHP Class". And, yes! Remember that Composer setup we did in
Preferences? Thanks to that, PhpStorm correctly guesses the namespace! The force is strong with this one...
The namespace for every class in src/ should be App plus whatever sub-directory it's in.
Name this ArticleController :
14 lines src/Controller/ArticleController.php
1 <?php
2
3 namespace App\Controller;
... lines 4 - 6
7 class ArticleController
8 {
... lines 9 - 12
13 }
14 lines src/Controller/ArticleController.php
... lines 1 - 2
3 namespace App\Controller;
... lines 4 - 6
7 class ArticleController
8 {
9 public function homepage()
10 {
... line 11
12 }
13 }
This function is the controller... and it's our place to build the page. To be more confusing, it's also called an
"action", or "ghob" to its Klingon friends.
Anyways, we can do whatever we want here: make database queries, API calls, take soil samples looking for
organic materials or render a template. There's just one rule: a controller must return a Symfony Response
object.
So let's say: return new Response() : we want the one from HttpFoundation . Give it a calm message:
OMG! My first page already! WOOO! :
14 lines src/Controller/ArticleController.php
... lines 1 - 2
3 namespace App\Controller;
4
5 use Symfony\Component\HttpFoundation\Response;
6
7 class ArticleController
8 {
9 public function homepage()
10 {
11 return new Response('OMG! My first page already! WOOO!');
12 }
13 }
Ahem. Oh, and check this out: when I let PhpStorm auto-complete the Response class it added this use
statement to the top of the file automatically:
14 lines src/Controller/ArticleController.php
... lines 1 - 4
5 use Symfony\Component\HttpFoundation\Response;
... lines 6 - 14
Annotation Routes
That was pretty easy, but it can be easier! Instead of creating our routes in YAML, let's use a cool feature
called annotations. This is an extra feature, so we need to install it. Find your open terminal and run:
Interesting... this annotations package actually installed sensio/framework-extra-bundle . We're going to talk about
how that works very soon.
4 lines config/routes.yaml
1 #index:
2 # path: /
3 # controller: App\Controller\ArticleController::homepage
Then, in ArticleController , above the controller method, add /** , hit enter, clear this out, and say @Route() . You
can use either class - but make sure PhpStorm adds the use statement on top. Then add "/" :
18 lines src/Controller/ArticleController.php
... lines 1 - 4
5 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
... lines 6 - 7
8 class ArticleController
9 {
10 /**
11 * @Route("/")
12 */
13 public function homepage()
14 {
... line 15
16 }
17 }
That's it! The route is defined right above the controller, which is why I love annotation routes: everything is in
one place. But don't trust me, find your browser and refresh. It's a traaaap! I mean, it works!
Tip
What exactly are annotations? They're PHP comments that are read as configuration.
26 lines src/Controller/ArticleController.php
... lines 1 - 4
5 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
... lines 6 - 7
8 class ArticleController
9 {
... lines 10 - 17
18 /**
19 * @Route("/news/why-asteroids-taste-like-bacon")
20 */
21 public function show()
22 {
... line 23
24 }
25 }
Eventually, this is how we want our URLs to look. This is called a "slug", it's a URL version of the title. As usual,
return a new Response('Future page to show one space article!') :
26 lines src/Controller/ArticleController.php
... lines 1 - 4
5 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6 use Symfony\Component\HttpFoundation\Response;
7
8 class ArticleController
9 {
... lines 10 - 17
18 /**
19 * @Route("/news/why-asteroids-taste-like-bacon")
20 */
21 public function show()
22 {
23 return new Response('Future page to show one space article!');
24 }
25 }
Perfect! Copy that URL and try it in your browser. It works... but this sucks! I don't want to build a route and
controller for every single article that lives in the database. Nope, we need a route that can match /news/
anything . How? Use {slug} :
29 lines src/Controller/ArticleController.php
... lines 1 - 7
8 class ArticleController
9 {
... lines 10 - 17
18 /**
19 * @Route("/news/{slug}")
20 */
21 public function show($slug)
22 {
... lines 23 - 26
27 }
28 }
This route now matches /news/ anything: that {slug} is a wildcard. Oh, and the name slug could be anything.
But whatever you choose now becomes available as an argument to your "ghob", I mean your action.
So let's refactor our success message to say:
29 lines src/Controller/ArticleController.php
... lines 1 - 7
8 class ArticleController
9 {
... lines 10 - 17
18 /**
19 * @Route("/news/{slug}")
20 */
21 public function show($slug)
22 {
23 return new Response(sprintf(
24 'Future page to show the article: "%s"',
25 $slug
26 ));
27 }
28 }
Try it! Refresh the same URL. Yes! It matches the route and the slug prints! Change it to something else:
/why-asteroids-taste-like-tacos . So delicious! Go back to bacon... because... ya know... everyone knows that's
what asteroids really taste like.
And... yes! We're 3 chapters in and you now know the first half of Symfony: the route & controller system. Sure,
you can do fancier things with routes, like match regular expressions, HTTP methods or host names - but that
will all be pretty easy for you now.
It's time to move on to something really important: it's time to learn about Symfony Flex and the recipe
system. Yum!
Chapter 4: Symfony Flex & Aliases
It's time to demystify something incredible: tractor beams. Well actually, we haven't figured those out yet... so
let's demystify something else, something that's already been happening behind the scenes. First commit
everything, with a nice message:
Tip
Wait! Run git init first before git add . : Symfony no longer creates a Git repo automatically for you :)
$ git init
$ git add .
$ git status
Tip
This package will only be used while developing. So, it would be even better to run
composer require sec-checker --dev .
64 lines composer.json
1 {
... lines 2 - 3
4 "require": {
... lines 5 - 8
9 "symfony/flex": "^1.0",
... lines 10 - 13
14 },
... lines 15 - 62
63 }
Our project began with just a few dependencies. One of them was symfony/flex : this is super important. Flex is
a Composer plugin with two superpowers.
Flex Aliases
The first superpower is the alias system. Find your browser and go to symfony.sh.
This is the Symfony "recipe" server: we'll talk about what that means next. Search for "security". Ah, here's a
package called sensiolabs/security-checker . And below, it has aliases: sec-check , sec-checker , security-check and
more.
Thanks to Flex, we can say composer require sec-checker , or any of these aliases, and it will translate that into
the real package name. Yep, it's just a shortcut system. But the result is really cool. Need a logger?
composer require logger . Need to send emails? composer require mailer . Need a tractor beam? composer require ,
wait, no, we can't help with that one.
64 lines composer.json
1 {
... lines 2 - 14
15 "require-dev": {
16 "sensiolabs/security-checker": "^4.1",
... line 17
18 },
... lines 19 - 62
63 }
Flex Recipes
The second superpower is even better: recipes. Mmmm. Go back to your terminal and... yes! It did install and,
check this out: "Symfony operations: 1 recipe". Then, "Configuring sensiolabs/security-checker ".
$ git status
Woh! We expected composer.json and composer.lock to be updated. But there are also changes to a
symfony.lock file and we suddenly have a brand new config file!
First, symfony.lock : this file is managed by Flex. It keeps track of which recipes have been installed. Basically...
commit it to git, but don't worry about it.
The second file is config/packages/dev/security_checker.yaml :
9 lines config/packages/dev/security_checker.yaml
1 services:
2 SensioLabs\Security\SecurityChecker:
3 public: false
4
5 SensioLabs\Security\Command\SecurityCheckerCommand:
6 arguments: ['@SensioLabs\Security\SecurityChecker']
7 tags:
8 - { name: console.command }
This was added by the recipe and, cool! It adds a new bin/console command to our app! Don't worry about the
code itself: you'll understand and be writing code like this soon enough!
Cool! This is the recipe system in action! Whenever you install a package, Flex will execute the recipe for that
package, if there is one. Recipes can add configuration files, create directories, or even modify files like
.gitignore so that the library instantly works without any extra setup. I love Flex.
By the way, the purpose of the security checker is that it checks to see if there are any known vulnerabilities
for packages used in our project. Right now, we're good!
Of course, composer require added the package. But the recipe added a new script!
64 lines composer.json
1 {
... lines 2 - 40
41 "scripts": {
42 "auto-scripts": {
... lines 43 - 44
45 "security-checker security:check": "script"
46 },
... lines 47 - 52
53 },
... lines 54 - 62
63 }
$ composer install
Oh, and I won't show it right now, but Flex is even smart enough to uninstall the recipes when you remove a
package. That makes testing out new packages fast and easy.
All recipes either live in this repository, or another one called symfony/recipes-contrib . There's no important
difference between the two repositories: but the official recipes are watched more closely for quality.
Next! Let's put the recipe system to work by installing Twig so we can create proper templates.
Chapter 5: The Twig Recipe
Do you remember the only rule for a controller? It must return a Symfony Response object! But Symfony
doesn't care how you do that: you could render a template, make API requests or make database queries and
build a JSON response.
Tip
Technically, a controller can return anything . Eventually, you'll learn how and why to do this.
Really, most of learning Symfony involves learning to install and use a bunch of powerful, but optional, tools
that make this work easier. If your app needs to return HTML, then one of these great tools is called Twig.
Installing Twig
First, make sure you commit all of your changes so far:
$ git status
I already did this. Recipes are so much more fun when you can see what they do! Now run:
By the way, in future tutorials, our app will become a mixture of a traditional HTML app and an API with a
JavaScript front-end. So if you want to know about building an API in Symfony, we'll get there!
This installs TwigBundle, a few other libraries and... configures a recipe! What did that recipe do? Let's find
out:
$ git status
9 lines config/bundles.php
... lines 1 - 2
3 return [
... lines 4 - 6
7 Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
8 ];
Bundles are the "plugin" system for Symfony. And whenever we install a third-party bundle, Flex adds it here
so that it's used automatically. Thanks Flex!
The recipe also created some stuff, like a templates/ directory! Yep, no need to guess where templates go: it's
pretty obvious! It even added a base layout file that we'll use soon.
5 lines config/packages/twig.yaml
1 twig:
2 paths: ['%kernel.project_dir%/templates']
3 debug: '%kernel.debug%'
4 strict_variables: '%kernel.debug%'
But even though this file was added by Flex, it's yours to modify: you can make whatever changes you want.
Oh, and I love this! Why do our templates need to live in a templates/ directory. Is that hardcoded deep inside
Symfony? Nope! It's right here!
5 lines config/packages/twig.yaml
1 twig:
2 paths: ['%kernel.project_dir%/templates']
... lines 3 - 5
Don't worry about this percent syntax yet - you'll learn about that in a future episode. But, you can probably
guess what's going on: %kernel.project_dir% is a variable that points to the root of the project.
Anyways, looking at what a recipe did is a great way to learn! But the main lesson of Flex is this: install a
library and it takes care of the rest.
Now, let's go use Twig!
Chapter 6: Twig ❤️
Back to work! Open ArticleController . As soon as you want to render a template, you need to extend a base
class: AbstractController :
29 lines src/Controller/ArticleController.php
... lines 1 - 5
6 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
... lines 7 - 8
9 class ArticleController extends AbstractController
10 {
... lines 11 - 27
28 }
Obviously, your controller does not need to extend this. But they usually will... because this class gives you
shortcut methods! The one we want is return $this->render() . Pass it a template filename: how about
article/show.html.twig to be consistent with the controller name. The second argument is an array of variables
that you want to pass into your template:
29 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
... lines 11 - 21
22 public function show($slug)
23 {
24 return $this->render('article/show.html.twig', [
... line 25
26 ]);
27 }
28 }
Eventually, we're going to load articles from the database. But... hang on! We're not quite ready yet. So let's
fake it 'til we make it! Pass a title variable set to a title-ized version of the slug:
29 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
... lines 11 - 21
22 public function show($slug)
23 {
24 return $this->render('article/show.html.twig', [
25 'title' => ucwords(str_replace('-', ' ', $slug)),
26 ]);
27 }
28 }
Great! Let's go add that template! Inside templates/ , create an article directory then the file: show.html.twig .
Add an h1 , then print that title variable: {{ title }} :
16 lines templates/article/show.html.twig
1 <h1>{{ title }}</h1>
... lines 2 - 16
Twig Basics
If you're new to Twig, welcome! You're going to love it! Twig only has 2 syntaxes. The first is {{ }} . I call this
the "say something" tag, because it prints. And just like PHP, you can print anything: a variable, a string or a
complex expression.
The second syntax is {% %} . I call this the "do something" tag. It's used whenever you need to, um, do
something, instead of printing, like an if statement or for loop. We'll look at the full list of do something tags
in a minute.
And... yea, that's it! Well, ok, I totally lied. There is a third syntax: {# #} : comments!
At the bottom of this page, I'll paste some extra hard-coded content to spice things up!
16 lines templates/article/show.html.twig
1 <h1>{{ title }}</h1>
2
3 <div>
4 <p>
5 Bacon ipsum dolor amet filet mignon picanha kielbasa jowl hamburger shankle biltong chicken turkey pastrami cupim pork c
6 </p>
7
8 <p>
9 Kielbasa pork belly meatball cupim burgdoggen chuck turkey buffalo ground round leberkas cow shank short loin bacon alca
10 </p>
11
12 <p>
13 Pastrami short loin pork chop, chicken kielbasa swine turducken jerky short ribs beef. Short ribs alcatra shoulder, flank pork c
14 </p>
15 </div>
Let's go try it! Find your browser and refresh! Boom! We have content!
But check it out: if you view the page source... it's just this content: we don't have any layout or HTML structure
yet. But, we will soon!
I'll paste in 3 fake comments. Add a second variable called comments to pass these into the template:
36 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
... lines 11 - 21
22 public function show($slug)
23 {
24 $comments = [
25 'I ate a normal rock once. It did NOT taste like bacon!',
26 'Woohoo! I\'m going on an all-asteroid diet!',
27 'I like bacon too! Buy some from my site! bakinsomebacon.com',
28 ];
29
30 return $this->render('article/show.html.twig', [
... line 31
32 'comments' => $comments,
33 ]);
34 }
35 }
This time, we can't just print that array: we need to loop over it. At the bottom, and an h2 that says
"Comments" and then add a ul :
24 lines templates/article/show.html.twig
... lines 1 - 16
17 <h2>Comments</h2>
18
19 <ul>
... lines 20 - 22
23 </ul>
To loop, we need our first do something tag! Woo! Use {% for comment in comments %} . Most "do" something
tags also have a closing tag: {% endfor %} :
24 lines templates/article/show.html.twig
... lines 1 - 16
17 <h2>Comments</h2>
18
19 <ul>
20 {% for comment in comments %}
... line 21
22 {% endfor %}
23 </ul>
Inside the loop, comment represents the individual comment. So, just print it: {{ comment }} :
24 lines templates/article/show.html.twig
... lines 1 - 16
17 <h2>Comments</h2>
18
19 <ul>
20 {% for comment in comments %}
21 <li>{{ comment }}</li>
22 {% endfor %}
23 </ul>
Try it! Brilliant! I mean, it's really ugly... oof. But we'll fix that later.
This is awesome! See the tags on the left? That is the entire list of possible "do something" tags. Yep, it will
always be {% and then one of these: for , if , extends , tractorbeam . And honestly, you're only going to use
about 5 of these most of the time.
Twig also has functions... which work like every other language - and a cool thing called "tests". Those are a bit
unique, but not too difficult, they allow you to say things like if foo is defined or... if space is empty .
The most useful part of this reference is the filter section. Filters are like functions but with a different, way
more hipster syntax. Let's try our the |length filter.
Go back to our template. I want to print out the total number of comments. Add a set of parentheses and then
say {{ comments|length }} :
24 lines templates/article/show.html.twig
... lines 1 - 16
17 <h2>Comments ({{ comments|length }})</h2>
... lines 18 - 24
That is a filter: the comments value passes from the left to right, just like a Unix pipe. The length filter counts
whatever was passed to it, and we print the result. You can even use multiple filters!
Tip
To unnecessarily confuse your teammates, try using the upper and lower filters over and over again:
{{ name|upper|lower|upper|lower|upper }} !
Template Inheritance
Twig has one last killer feature: it's template inheritance system. Because remember! We don't yet have a real
HTML page: just the content from the template.
26 lines templates/article/show.html.twig
1 {% extends 'base.html.twig' %}
... lines 2 - 26
This refers to the base.html.twig file that was added by the recipe:
13 lines templates/base.html.twig
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="UTF-8">
5 <title>{% block title %}Welcome!{% endblock %}</title>
6 {% block stylesheets %}{% endblock %}
7 </head>
8 <body>
9 {% block body %}{% endblock %}
10 {% block javascripts %}{% endblock %}
11 </body>
12 </html>
It's simple now, but this is our layout file and we'll customize it over time. By extending it, we should at least
get this basic HTML structure.
But when we refresh... surprise! An error! And probably one that you'll see at some point!
A template that extends another one cannot include content outside Twig blocks
Huh. Look at the base template again: it's basically an HTML layout plus a bunch of blocks... most of which are
empty. When you extend a template, you're telling Twig that you want to put your content inside of that
template. The blocks, are the "holes" into which our child template can put content. For example, there's a
block called body , and that's really where we want to put our content:
13 lines templates/base.html.twig
1 <!DOCTYPE html>
2 <html>
... lines 3 - 7
8 <body>
9 {% block body %}{% endblock %}
... line 10
11 </body>
12 </html>
To do that, we need to override that block. At the top of the content, add {% block body %} , and at the bottom,
{% endblock %} :
28 lines templates/article/show.html.twig
1 {% extends 'base.html.twig' %}
2
3 {% block body %}
4 <h1>{{ title }}</h1>
... lines 5 - 21
22 <ul>
23 {% for comment in comments %}
24 <li>{{ comment }}</li>
25 {% endfor %}
26 </ul>
27 {% endblock %}
Now our content should go inside of that block in base.html.twig . Try it! Refresh! Yes! Well, it doesn't look any
different, but we do have a proper HTML body.
Oh, and most of the time, the blocks are empty. But you can give the block some default content, like with
title :
13 lines templates/base.html.twig
1 <!DOCTYPE html>
2 <html>
3 <head>
... line 4
5 <title>{% block title %}Welcome!{% endblock %}</title>
... line 6
7 </head>
... lines 8 - 11
12 </html>
30 lines templates/article/show.html.twig
1 {% extends 'base.html.twig' %}
2
3 {% block title %}Read: {{ title }}{% endblock %}
... lines 4 - 30
Try that! Yes! The page title changes. And... voilà! That's Twig. You're going to love it.
Go Deeper!
Next let's check out one of Symfony's most killer features: the profiler.
Chapter 7: Web Debug Toolbar & the Profiler!
Make sure you've committed all of your changes - I already did. Because we're about to install something
super fun! Like, floating around space fun! Run:
The profiler - also called the "web debug toolbar" is probably the most awesome thing in Symfony. This installs
a few packages and... one recipe! Run:
$ git status
Ok cool! It added a couple of configuration files and even some routes in the dev environment only that help
the profiler work. So... what the heck is the profiler? Go back to your browser, make sure you're on the article
show page and refresh! Voilà!
Oh, and it's packed with info, like which route was matched, what controller was executed, execution time,
cache details and even information about templates.
And as we install more libraries, we're going to get even more icons! But the really awesome thing is that you
can click any of these icons to go into... the profiler.
When you're ready to go back to the original page, you can click the link at the top.
37 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
... lines 11 - 21
22 public function show($slug)
23 {
... lines 24 - 28
29 dump($slug, $this);
... lines 30 - 34
35 }
36 }
Ok, refresh! Beautiful, colored output. And, you can expand objects to dig deeper into them.
Tip
To expand all the nested nodes just press Ctrl and click the arrow.
31 lines templates/article/show.html.twig
... lines 1 - 4
5 {% block body %}
6 {{ dump() }}
... lines 7 - 29
30 {% endblock %}
Tip
If you don't have Xdebug installed, this might fail with a memory issue. But don't worry! In the next
chapter, we'll install a tool to make this even better.
In Twig, you're allowed to use dump() with no arguments. And that's especially useful. Why? Because it dumps
an associative array of all of the variables you have access to. We already knew we had title and comments
variables. But apparently, we also have an app variable! Actually, every template gets this app variable
automatically. Good to know!
But! Symfony has even more debugging tools! Let's get them and learn about "packs" next!
Chapter 8: Debugging & Packs
Symfony has even more debugging tools. The easiest way to get all of them is to find your terminal and run:
Find your browser, surf back to symfony.sh and search for "debug". Ah, so the debug alias will actually install a
package called symfony/debug-pack . So... what's a pack?
Click to look at the package details, and then go to its GitHub repository.
Whoa! It's just a single file: composer.json ! Inside, it requires six other libraries!
Sometimes, you're going to want to install several packages at once related to one feature. To make that easy,
Symfony has a number of "packs", and their whole purpose is give you one easy package that actually installs
several other libraries.
In this case, composer require debug will install Monolog - a logging library, phpunit-bridge - for testing, and even
the profiler-pack that we already installed earlier.
If you go back to the terminal... yep! It downloaded all those libraries and configured a few recipes.
And... check this out! Refresh! Hey! Our Twig dump() got prettier! The debug-pack integrated everything
together even better.
67 lines composer.json
1 {
... lines 2 - 15
16 "require-dev": {
... line 17
18 "symfony/debug-pack": "^1.0",
... line 19
20 "symfony/profiler-pack": "^1.0"
21 },
... lines 22 - 65
66 }
And we now know that the debug-pack is actually a collection of about 6 libraries.
But, packs have a disadvantage... a "dark side". What if you wanted to control the version of just one of these
libraries? Or what if you wanted most of these libraries, but you didn't want, for example, the phpunit-bridge .
Well... right now, there's no way to do that: all we have is this one debug-pack line.
Don't worry brave space traveler! Just... unpack the pack! Yep, at your terminal, run:
The unpack command comes from Symfony flex. And... interesting! All it says is "removing symfony/debug-
pack". But if you look at your composer.json :
71 lines composer.json
1 {
... lines 2 - 15
16 "require-dev": {
17 "easycorp/easy-log-handler": "^1.0.2",
... line 18
19 "symfony/debug-bundle": "^3.3|^4.0",
... line 20
21 "symfony/monolog-bundle": "^3.0",
22 "symfony/phpunit-bridge": "^3.3|^4.0",
23 "symfony/profiler-pack": "^1.0",
24 "symfony/var-dumper": "^3.3|^4.0"
25 },
... lines 26 - 69
70 }
Ah! It did remove symfony/debug-pack , but it replaced it with the 6 libraries from that pack! We can now control
the versions or even remove individual libraries if we don't want them.
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/ :
67 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
3
4 <head>
5 <title>{% block title %}Welcome to the SpaceBar{% endblock %}</title>
6 <meta charset="utf-8">
7 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
8
9 {% block stylesheets %}
10 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity
11 {% endblock %}
12 </head>
13
14 <body>
15 <nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
16 <a style="margin-left: 75px;" class="navbar-brand space-brand" href="#">The Space Bar</a>
17 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls
18 <span class="navbar-toggler-icon"></span>
19 </button>
20 <div class="collapse navbar-collapse" id="navbarNavDropdown">
21 <ul class="navbar-nav mr-auto">
22 <li class="nav-item">
23 <a style="color: #fff;" class="nav-link" href="#">Local Asteroids</a>
24 </li>
25 <li class="nav-item">
26 <a style="color: #fff;" class="nav-link" href="#">Weather</a>
27 </li>
28 </ul>
29 <form class="form-inline my-2 my-lg-0">
30 <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
31 <button class="btn btn-info my-2 my-sm-0" type="submit">Search</button>
32 </form>
33 <ul class="navbar-nav ml-auto">
34 <li class="nav-item dropdown" style="margin-right: 75px;">
35 <a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle
36 <img class="nav-profile-img rounded-circle" src="images/astronaut-profile.png">
37 </a>
38 <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
39 <a class="dropdown-item" href="#">Profile</a>
40 <a class="dropdown-item" href="#">Create Post</a>
41 <a class="dropdown-item" href="#">Logout</a>
42 </div>
43 </li>
</li>
44 </ul>
45 </div>
46 </nav>
47
48 {% block body %}{% endblock %}
49
50 <footer class="footer">
51 <div class="container text-center">
52 <span class="text-muted">Made with <i class="fa fa-heart" style="color: red;"></i> by the guys and gals at <
53 </div>
54 </footer>
55
56
57 {% block javascripts %}
58 <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQg
59 <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrB
60 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFO
61 <script>
62 $('.dropdown-toggle').dropdown();
63 </script>
64 {% endblock %}
65 </body>
66 </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.
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 :
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
3
4 <head>
... lines 5 - 8
9 {% block stylesheets %}
... line 10
11 <link rel="stylesheet" href="/css/font-awesome.css">
... line 12
13 {% endblock %}
14 </head>
... lines 15 - 67
68 </html>
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
3
4 <head>
... lines 5 - 8
9 {% block stylesheets %}
... line 10
11 <link rel="stylesheet" href="/css/font-awesome.css">
12 <link rel="stylesheet" href="/css/styles.css">
13 {% endblock %}
14 </head>
... lines 15 - 67
68 </html>
It's that simple! Refresh! Still not perfect, but much better!
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
3
4 <head>
... lines 5 - 8
9 {% block stylesheets %}
... line 10
11 <link rel="stylesheet" href="{{ asset('css/font-awesome.css') }}">
... line 12
13 {% endblock %}
14 </head>
... lines 15 - 67
68 </html>
Woh! It wrapped the path in a Twig asset() function! Do the same thing below for styles.css :
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
3
4 <head>
... lines 5 - 8
9 {% block stylesheets %}
... line 10
11 <link rel="stylesheet" href="{{ asset('css/font-awesome.css') }}">
12 <link rel="stylesheet" href="{{ asset('css/styles.css') }}">
13 {% endblock %}
14 </head>
... lines 15 - 67
68 </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!
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 :
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
... lines 3 - 15
16 <body>
17 <nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
22 <div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
35 <ul class="navbar-nav ml-auto">
36 <li class="nav-item dropdown" style="margin-right: 75px;">
37 <a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle
38 <img class="nav-profile-img rounded-circle" src="{{ asset('images/astronaut-profile.png') }}">
39 </a>
... lines 40 - 44
45 </li>
46 </ul>
47 </div>
48 </nav>
... lines 49 - 66
67 </body>
68 </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.
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 }} :
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 }} :
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.
Chapter 10: Generating URLs
Most of these links don't go anywhere yet. Whatever! No problem! We're going to fill them in as we continue.
Besides, most of our users will be in hypersleep for at least a few more decades.
But we can hook up some of these - like the "Space Bar" logo text - that should go to the homepage.
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
... lines 3 - 15
16 <body>
17 <nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
18 <a style="margin-left: 75px;" class="navbar-brand space-brand" href="#">The Space Bar</a>
... lines 19 - 47
48 </nav>
... lines 49 - 66
67 </body>
68 </html>
Ok - let's point this link to the homepage. And yep, we could just say href="/" .
But... there's a better way. Instead, we're going to generate a URL to the route. Yep, we're going to ask
Symfony to give us the URL to the route that's above our homepage action:
36 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
11 /**
12 * @Route("/")
13 */
14 public function homepage()
15 {
... line 16
17 }
... lines 18 - 34
35 }
Why? Because if we ever decided to change this route's URL - like to /news - if we generate the URL instead of
hardcoding it, all the links will automatically update. Magic!
$ ./bin/console debug:router
This is an awesome little tool that shows you a list of all of the routes in your app. You can see our two routes
and a bunch of routes that help the profiler and web debug toolbar.
There's one thing about routes that we haven't really talked about yet: each route has an internal name. This
is never shown to the user, it only exists so that we can refer to that route in our code. For annotation routes,
by default, that name is created for us.
Generating URLs with path()
This means, to generate a URL to the homepage, copy the route name, go back to base.html.twig , add
{{ path() }} and paste the route name:
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
... lines 3 - 15
16 <body>
17 <nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
18 <a style="margin-left: 75px;" class="navbar-brand space-brand" href="{{ path('app_article_homepage') }}">The Space
... lines 19 - 47
48 </nav>
... lines 49 - 66
67 </body>
68 </html>
That's it!
But... actually I don't like to rely on auto-created route names because they could change if we renamed
certain parts of our code. Instead, as soon as I want to generate a URL to a route, I add a name option:
name="app_homepage" :
36 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
11 /**
12 * @Route("/", name="app_homepage")
13 */
14 public function homepage()
15 {
... line 16
17 }
... lines 18 - 34
35 }
$ ./bin/console debug:router
The only thing that changed is the name of the route. Now go back to base.html.twig and use the new route
name here:
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
... lines 3 - 15
16 <body>
17 <nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
18 <a style="margin-left: 75px;" class="navbar-brand space-brand" href="{{ path('app_homepage') }}">The Space Bar
... lines 19 - 47
48 </nav>
... lines 49 - 66
67 </body>
68 </html>
It still works exactly like before, but we're in complete control of the route name.
36 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
11 /**
12 * @Route("/", name="app_homepage")
13 */
14 public function homepage()
15 {
16 return $this->render('article/homepage.html.twig');
17 }
... lines 18 - 34
35 }
This template does not exist yet. But if you look again in the tutorial/ directory from the code download, I've
created a homepage template for us. Sweet! Copy that and paste it into templates/article :
81 lines templates/article/homepage.html.twig
1 {% extends 'base.html.twig' %}
2
3 {% block body %}
4 <div class="container">
5 <div class="row">
6
7 <!-- Article List -->
8
9 <div class="col-sm-12 col-md-8">
10
11 <!-- H1 Article -->
12 <a class="main-article-link" href="#">
13 <div class="main-article mb-5 pb-3">
14 <img src="{{ asset('images/meteor-shower.jpg') }}" alt="meteor shower">
15 <h1 class="text-center mt-2">Ursid Meteor Shower: <br>Healthier than a regular shower?</h1>
16 </div>
17 </a>
18
19 <!-- Supporting Articles -->
<!-- Supporting Articles -->
20
21 <div class="article-container my-1">
22 <a href="#">
23 <img class="article-img" src="{{ asset('images/asteroid.jpeg') }}">
24 <div class="article-title d-inline-block pl-3 align-middle">
25 <span>Why do Asteroids Taste Like Bacon?</span>
26 <br>
27 <span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/a
28 <span class="pl-5 article-details float-right"> 3 hours ago</span>
29 </div>
30 </a>
31 </div>
32
33 <div class="article-container my-1">
34 <a href="#">
35 <img class="article-img" src="{{ asset('images/mercury.jpeg') }}">
36 <div class="article-title d-inline-block pl-3 align-middle">
37 <span>Life on Planet Mercury: <br> Tan, Relaxing and Fabulous</span>
38 <br>
39 <span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/a
40 <span class="pl-5 article-details float-right"> 6 days ago</span>
41 </div>
42 </a>
43 </div>
44
45 <div class="article-container my-1">
46 <a href="#">
47 <img class="article-img" src="{{ asset('images/lightspeed.png') }}">
48 <div class="article-title d-inline-block pl-3 align-middle">
49 <span>Light Speed Travel: <br> Fountain of Youth or Fallacy</span>
50 <br>
51 <span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/a
52 <span class="pl-5 article-details float-right"> 2 weeks ago</span>
53 </div>
54 </a>
55 </div>
56
57 </div>
58
59 <!-- Right bar ad space -->
60
61
62 <div class="col-sm-12 col-md-4 text-center">
63 <div class="ad-space mx-auto mt-1 pb-2 pt-2">
64 <img class="advertisement-img" src="{{ asset('images/space-ice.png') }}">
65 <p><span class="advertisement-text">New:</span> Space Ice Cream!</p>
66 <button class="btn btn-info">Buy Now!</button>
67 </div>
68
69 <div class="quote-space pb-2 pt-2 px-5">
70 <h3 class="text-center pb-3">Trending Quotes</h3>
71 <p><i class="fa fa-comment"></i> "Our two greatest problems are gravity and paperwork. We can lick gravity, but
72
73 <p class="pt-4"><i class="fa fa-comment"></i> "Let's face it, space is a risky business. I always considered every l
74
75 <p class="pt-4"><i class="fa fa-comment"></i> "If offered a seat on a rocket ship, don't ask what seat. Just get on.
76 </div>
76 </div>
77 </div>
78 </div>
79 </div>
80 {% endblock %}
It's nothing special: just a bunch of hardcoded information and fascinating space articles. It does make for a
pretty cool-looking homepage. And yea, we'll make this all dynamic once we have a database.
Step 1: now that we want to link to this route, give it a name: article_show :
36 lines src/Controller/ArticleController.php
... lines 1 - 8
9 class ArticleController extends AbstractController
10 {
... lines 11 - 18
19 /**
20 * @Route("/news/{slug}", name="article_show")
21 */
22 public function show($slug)
23 {
... lines 24 - 33
34 }
35 }
Step 2: inside homepage.html.twig , find the article... and... for the href , use {{ path('article_show') }} :
81 lines templates/article/homepage.html.twig
... lines 1 - 2
3 {% block body %}
4 <div class="container">
5 <div class="row">
6
7 <!-- Article List -->
8
9 <div class="col-sm-12 col-md-8">
... lines 10 - 18
19 <!-- Supporting Articles -->
20
21 <div class="article-container my-1">
22 <a href="{{ path('article_show') }}">
... lines 23 - 29
30 </a>
31 </div>
... lines 32 - 56
57 </div>
... lines 58 - 77
78 </div>
79 </div>
80 {% endblock %}
That should work... right? Refresh! No! It's a huge, horrible, error!
Some mandatory parameters are missing - {slug} - to generate a URL for article_show .
That totally makes sense! This route has a wildcard... so we can't just generate a URL to it. Nope, we need to
also tell Symfony what value it should use for the {slug} part.
How? Add a second argument to path() : {} . That's the syntax for an associative array when you're inside Twig
- it's similar to JavaScript. Give this a slug key set to why-asteroids-taste-like-bacon :
81 lines templates/article/homepage.html.twig
... lines 1 - 2
3 {% block body %}
4 <div class="container">
5 <div class="row">
6
7 <!-- Article List -->
8
9 <div class="col-sm-12 col-md-8">
... lines 10 - 18
19 <!-- Supporting Articles -->
20
21 <div class="article-container my-1">
22 <a href="{{ path('article_show', {slug: 'why-asteroids-taste-like-bacon'}) }}">
... lines 23 - 29
30 </a>
31 </div>
... lines 32 - 56
57 </div>
... lines 58 - 77
78 </div>
79 </div>
80 {% endblock %}
Try it - refresh! Error gone! And check this out: the link goes to our show page.
Next, let's add some JavaScript and an API endpoint to bring this little heart icon to life!
Chapter 11: JavaScript & Page-Specific Assets
The topic of API's is... ah ... a huge topic and hugely important these days. We're going to dive deep into API's
in a future tutorial. But... I think we at least need to get to the basics right now.
So here's the goal: see this heart icon? I want the user to be able to click it to "like" the article. We're going to
write some JavaScript that sends an AJAX request to an API endpoint. That endpoint will return the new number
of likes, and we'll update the page. Well, the number of "likes" is just a fake number for now, but we can still
get this entire system setup and working.
69 lines templates/base.html.twig
1 <!doctype html>
2 <html lang="en">
... lines 3 - 15
16 <body>
... lines 17 - 58
59 {% block javascripts %}
60 <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQg
... lines 61 - 65
66 {% endblock %}
67 </body>
68 </html>
In the public/ directory, create a new js/ directory and a file inside called, how about, article_show.js . The idea
is that we'll include this only on the article show page.
11 lines public/js/article_show.js
1 $(document).ready(function() {
... lines 2 - 9
10 });
Now, open show.html.twig and, scroll down a little. Ah! Here is the hardcoded number and heart link:
Yep, we'll start the AJAX request when this link is clicked and update the "5" with the new number.
To set this up, let's make few changes. On the link, add a new class js-like-article . And to target the 5, add a
span around it with js-like-article-count :
11 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
3 e.preventDefault();
... lines 4 - 8
9 });
10 });
11 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
3 e.preventDefault();
4
5 var $link = $(e.currentTarget);
... lines 6 - 8
9 });
10 });
This is the link that was just clicked. I want to toggle that heart icon between being empty and full: do that with
$link.toggleClass('fa-heart-o').toggleClass('fa-heart') :
11 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
3 e.preventDefault();
4
5 var $link = $(e.currentTarget);
6 $link.toggleClass('fa-heart-o').toggleClass('fa-heart');
... lines 7 - 8
9 });
10 });
To update the count value, go copy the other class: js-like-article-count . Find it and set its HTML, for now, to
TEST :
11 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
3 e.preventDefault();
4
5 var $link = $(e.currentTarget);
6 $link.toggleClass('fa-heart-o').toggleClass('fa-heart');
7
8 $('.js-like-article-count').html('TEST');
9 });
10 });
But... we don't really want to include this JavaScript file on every page, we only need it on the article show
page.
But how can we do that? If we add it to the body block, then on the final page, it will appear too early - before
even jQuery is included!
To add our new file at the bottom, we can override the javascripts block. Anywhere in show.html.twig , add
{% block javascripts %} and {% endblock %} :
Add the script tag with src="" , start typing article_show , and auto-complete!
There is still a problem with this... and you might already see it. Refresh the page. Click and... it doesn't work!
$ is not defined
That's not good! Check out the HTML source and scroll down towards the bottom. Yep, there is literally only
one script tag on the page. That makes sense! When you override a block, you completely override that block!
All the script tags from base.html.twig are gone!
Whoops! What we really want to do is append to the block, not replace it. How can we do that? Say
{{ parent() }} :
Try it now! Refresh! And... it works! Next, let's create our API endpoint and hook this all together.
Chapter 12: JSON API Endpoint
When we click the heart icon, we need to send an AJAX request to the server that will, eventually, update
something in a database to show that the we liked this article. That API endpoint also needs to return the new
number of hearts to show on the page... ya know... in case 10 other people liked it since we opened the page.
47 lines src/Controller/ArticleController.php
... lines 1 - 9
10 class ArticleController extends AbstractController
11 {
... lines 12 - 39
40 public function toggleArticleHeart($slug)
41 {
... lines 42 - 44
45 }
46 }
Then add the route above: @Route("/news/{slug}") - to match the show URL - then /heart . Give it a name
immediately: article_toggle_heart :
47 lines src/Controller/ArticleController.php
... lines 1 - 9
10 class ArticleController extends AbstractController
11 {
... lines 12 - 36
37 /**
38 * @Route("/news/{slug}/heart", name="article_toggle_heart")
39 */
40 public function toggleArticleHeart($slug)
41 {
... lines 42 - 44
45 }
46 }
I included the {slug} wildcard in the route so that we know which article is being liked. We could also use an
{id} wildcard once we have a database.
Add the corresponding $slug argument. But since we don't have a database yet, I'll add a TODO: "actually
heart/unheart the article!":
47 lines src/Controller/ArticleController.php
... lines 1 - 9
10 class ArticleController extends AbstractController
11 {
... lines 12 - 36
37 /**
38 * @Route("/news/{slug}/heart", name="article_toggle_heart")
39 */
40 public function toggleArticleHeart($slug)
41 {
42 // TODO - actually heart/unheart the article!
... lines 43 - 44
45 }
46 }
Returning JSON
We want this API endpoint to return JSON... and remember: the only rule for a Symfony controller is that it must
return a Symfony Response object. So we could literally say return new Response(json_encode(['hearts' => 5])) .
But that's too much work! Instead say return new JsonResponse(['hearts' => rand(5, 100)] :
47 lines src/Controller/ArticleController.php
... lines 1 - 6
7 use Symfony\Component\HttpFoundation\JsonResponse;
... lines 8 - 9
10 class ArticleController extends AbstractController
11 {
... lines 12 - 36
37 /**
38 * @Route("/news/{slug}/heart", name="article_toggle_heart")
39 */
40 public function toggleArticleHeart($slug)
41 {
42 // TODO - actually heart/unheart the article!
43
44 return new JsonResponse(['hearts' => rand(5, 100)]);
45 }
46 }
Tip
Note that since PHP 7.0 instead of rand() you may want to use random_int() that generates
cryptographically secure pseudo-random integers. It's more preferable to use unless you hit performance
issue, but with just several calls it's not even noticeable.
There's nothing special here: JsonResponse is a sub-class of Response . It calls json_encode() for you, and also
sets the Content-Type header to application/json , which helps your JavaScript understand things.
Let's try this in the browser first. Go back and add /heart to the URL. Yes! Our first API endpoint!
Tip
47 lines src/Controller/ArticleController.php
... lines 1 - 9
10 class ArticleController extends AbstractController
11 {
... lines 12 - 36
37 /**
38 * @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
39 */
40 public function toggleArticleHeart($slug)
41 {
... lines 42 - 44
45 }
46 }
As soon as we do that, we can no longer make a GET request in the browser: it does not match the route
anymore! Run:
$ ./bin/console debug:router
And you'll see that the new route only responds to POST requests. Pretty cool. By the way, Symfony has a lot
more tools for creating API endpoints - this is just the beginning. In future tutorials, we'll go further!
Actually, there is a really cool bundle called FOSJsRoutingBundle that does allow you to generate routes in
JavaScript. But, I'm going to show you another, simple way.
Back in the template, find the heart section. Let's just... fill in the href on the link! Add path() , paste the route
name, and pass the slug wildcard set to a slug variable:
Actually... there is not a slug variable in this template yet. If you look at ArticleController , we're only passing
two variables. Add a third: slug set to $slug :
48 lines src/Controller/ArticleController.php
... lines 1 - 9
10 class ArticleController extends AbstractController
11 {
... lines 12 - 22
23 public function show($slug)
24 {
... lines 25 - 30
31 return $this->render('article/show.html.twig', [
... line 32
33 'slug' => $slug,
... line 34
35 ]);
36 }
... lines 37 - 46
47 }
That should at least set the URL on the link. Go back to the show page in your browser and refresh. Yep! The
heart link is hooked up.
Why did we do this? Because now we can get that URL really easily in JavaScript. Add $.ajax({}) and pass
method: 'POST' and url set to $link.attr('href') :
16 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
... lines 3 - 5
6 $link.toggleClass('fa-heart-o').toggleClass('fa-heart');
7
8 $.ajax({
9 method: 'POST',
10 url: $link.attr('href')
... lines 11 - 12
13 })
14 });
15 });
That's it! At the end, add .done() with a callback that has a data argument:
16 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
... lines 3 - 7
8 $.ajax({
9 method: 'POST',
10 url: $link.attr('href')
11 }).done(function(data) {
... line 12
13 })
14 });
15 });
The data will be whatever our API endpoint sends back. That means that we can move the article count HTML
line into this, and set it to data.hearts :
16 lines public/js/article_show.js
1 $(document).ready(function() {
2 $('.js-like-article').on('click', function(e) {
... lines 3 - 7
8 $.ajax({
... lines 9 - 10
11 }).done(function(data) {
12 $('.js-like-article-count').html(data.hearts);
13 })
14 });
15 });
Oh, and if you're not familiar with the .done() function or Promises, I'd highly recommend checking out our
JavaScript Track. It's not beginner stuff: it's meant to take your JS up to the next level.
And... I have a surprise! See this little arrow icon in the web debug toolbar? This showed up as soon as we
made the first AJAX request. Actually, every time we make an AJAX request, it's added to the top of this list!
That's awesome because - remember the profiler? - you can click to view the profiler for any AJAX request. Yep,
you now have all the performance and debugging tools at your fingertips... even for AJAX calls.
Oh, and if there were an error, you would see it in all its beautiful, styled glory on the Exception tab. Being able
to load the profiler for an AJAX call is kind of an easter egg: not everyone knows about it. But you should.
I think it's time to talk about the most important part of Symfony: Fabien. I mean, services.
Chapter 13: Services
It's time to talk about the most fundamental part of Symfony: services!
Honestly, Symfony is nothing more than a bunch of useful objects that work together. For example, there's a
router object that matches routes and generates URLs. There's a Twig object that renders templates. And
there's a Logger object that Symfony is already using internally to store things in a var/log/dev.log file.
Actually, everything in Symfony - I mean everything - is done by one of these useful objects. And these useful
objects have a special name: services.
What's a Service?
But don't get too excited about that word - service. It's a special word for a really simple idea: a service is any
object that does work, like generating URLs, sending emails or saving things to a database.
Symfony comes with a huge number of services, and I want you to think of services as your tools.
Like, if I gave you the logger service, or object, then you could use it to log messages. If I gave you a mailer
service, you could send some emails! Tools!
The entire second half of Symfony is all about learning where to find these services and how to use them.
Every time you learn about a new service, you get a new tool, and become just a little bit more dangerous!
$ tail -f var/log/dev.log
I'll clear the screen. Now, refresh the page, and move back. Awesome! This proves that Symfony has some sort
of logging system. And since everything is done by a service, there must be a logger object. So here's the
question: how can we get the logger service so that we can log our own messages?
Here's the answer: inside the controller, on the method, add an additional argument. Give it a LoggerInterface
type hint - hit tab to auto-complete that and call it whatever you want, how about $logger :
51 lines src/Controller/ArticleController.php
... lines 1 - 4
5 use Psr\Log\LoggerInterface;
... lines 6 - 10
11 class ArticleController extends AbstractController
12 {
... lines 13 - 41
42 public function toggleArticleHeart($slug, LoggerInterface $logger)
43 {
... lines 44 - 48
49 }
50 }
Remember: when you autocomplete, PhpStorm adds the use statement to the top for you.
51 lines src/Controller/ArticleController.php
... lines 1 - 10
11 class ArticleController extends AbstractController
12 {
... lines 13 - 41
42 public function toggleArticleHeart($slug, LoggerInterface $logger)
43 {
44 // TODO - actually heart/unheart the article!
45
46 $logger->info('Article is being hearted!');
... lines 47 - 48
49 }
50 }
Before we talk about this, let's try it! Find your browser and click the heart. That hit the AJAX endpoint. Go back
to the terminal. Yes! There it is at the bottom. Hit Ctrl + C to exit tail .
Service Autowiring
Ok cool! But... how the heck did that work? Here's the deal: before Symfony executes our controller, it looks at
each argument. For simple arguments like $slug , it passes us the wildcard value from the router:
51 lines src/Controller/ArticleController.php
... lines 1 - 10
11 class ArticleController extends AbstractController
12 {
... lines 13 - 38
39 /**
40 * @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
41 */
42 public function toggleArticleHeart($slug, LoggerInterface $logger)
43 {
... lines 44 - 48
49 }
50 }
But for $logger , it looks at the type-hint and realizes that we want Symfony to pass us the logger object. Oh,
and the order of the arguments does not matter.
This is a very powerful idea called autowiring: if you need a service object, you just need to know the correct
type-hint to use! So... how the heck did I know to use LoggerInterface ? Well, of course, if you look at the official
Symfony docs about the logger, it'll tell you. But, there's a cooler way.
$ ./bin/console debug:autowiring
Boom! This is a full list of all of the type-hints that you can use to get a service. Notice that most of them say
that they are an alias to something. Don't worry about that too much: like routes, each service has an internal
name you can use to reference it. We'll learn more about that later. Oh, and whenever you install a new
package, you'll get more and more services in this list. More tools!
And remember how I said that everything in Symfony is done by a service? Well, when we call $this->render()
in a controller, that's just a shortcut to fetch the Twig service and call a method on it:
51 lines src/Controller/ArticleController.php
... lines 1 - 10
11 class ArticleController extends AbstractController
12 {
... lines 13 - 23
24 public function show($slug)
25 {
... lines 26 - 31
32 return $this->render('article/show.html.twig', [
... lines 33 - 35
36 ]);
37 }
... lines 38 - 49
50 }
In fact, let's pretend that the $this->render() shortcut does not exist. How could we render a template? No
problem: we just need the Twig service. Add a second argument with an Environment type-hint, because that's
the class name we saw in debug:autowiring . Call the arg $twigEnvironment :
54 lines src/Controller/ArticleController.php
... lines 1 - 9
10 use Twig\Environment;
11
12 class ArticleController extends AbstractController
13 {
... lines 14 - 24
25 public function show($slug, Environment $twigEnvironment)
26 {
... lines 27 - 39
40 }
... lines 41 - 52
53 }
54 lines src/Controller/ArticleController.php
... lines 1 - 9
10 use Twig\Environment;
11
12 class ArticleController extends AbstractController
13 {
... lines 14 - 24
25 public function show($slug, Environment $twigEnvironment)
26 {
... lines 27 - 32
33 $html = $twigEnvironment->render('article/show.html.twig', [
34 'title' => ucwords(str_replace('-', ' ', $slug)),
35 'slug' => $slug,
36 'comments' => $comments,
37 ]);
... lines 38 - 39
40 }
... lines 41 - 52
53 }
The method we want to call on the Twig object is coincidentally the same as the controller shortcut.
54 lines src/Controller/ArticleController.php
... lines 1 - 8
9 use Symfony\Component\HttpFoundation\Response;
10 use Twig\Environment;
11
12 class ArticleController extends AbstractController
13 {
... lines 14 - 24
25 public function show($slug, Environment $twigEnvironment)
26 {
... lines 27 - 32
33 $html = $twigEnvironment->render('article/show.html.twig', [
34 'title' => ucwords(str_replace('-', ' ', $slug)),
35 'slug' => $slug,
36 'comments' => $comments,
37 ]);
38
39 return new Response($html);
40 }
... lines 41 - 52
53 }
Ok, this is way more work than before... and I would not do this in a real project. But, I wanted to prove a point:
when you use the $this->render() shortcut method on the controller, all it really does is call render() on the Twig
service and then wrap it inside a Response object for you.
Try it! Go back and refresh the page. It works exactly like before! Of course we will use shortcut methods,
because they make our life way more awesome. I'll change my code back to look like it did before. But the
point is this: everything is done by a service. If you learn to master services, you can do anything from
anywhere in Symfony.
There's a lot more to say about the topic of services, and so many other parts of Symfony: configuration,
Doctrine & the database, forms, Security and APIs, to just name a few. The Space Bar is far from being the
galactic information source that we know it will be!
But, congrats! You just spent an hour getting an awesome foundation in Symfony. You will not regret your hard
work: you're on your way to building great things and, as always, becoming a better and better developer.
Alright guys, seeya next time!