Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

A basic guide to PHP Symfony - Routes, templates and controllers

profile picture
Author
Moritz Fromm
Published
2019-04-16
Time to read
14 minutes reading time

About the author-

Series index

  1. Setup
  2. Routes, templates and controllers (You are here)
  3. Working with the database (Not yet finished)
  4. Security (Not yet finished)

Symfony handles routing for us, that is one of its super powers. Routing means, that when we open /hello/World in our browser, it will match the request to the function generating our response.

Symfony is based around the concept of routes, a route is the path that comes after our domain. In the case of https://example.com/hello/World the /hello/World is our route. The functions Symfony will match that request to, are organized in multiple controllers. It is possible, and therefore highly recommended, to use multiple well named controllers. Example given: in an online Shop we may have a ProductController, a PrivacyController (may the GDPR be blessed), a PurchaseController and so on. If someone looks at our code it immediately is clear what this controller is for.

Step 1 - Creating our first route

At the beginning we generate all of our dynamic page contents inside of a Controller. We will use bin/console to create our first controller. After running php bin/console make:controller a prompt will ask us for the controller name. For now we will use DefaultController.

(Click to expand) Output of the command
$ php bin/console make:controller

 Choose a name for your controller class (e.g. TinyChefController):
 > DefaultController

 created: src/Controller/DefaultController.php
 created: templates/default/index.html.twig


  Success!


 Next: Open your new controller class and add some pages!

Our first controller with routes has just been created, congratulations!

In our editor we can see two new files. Those being src/Controller/DefaultController.php and templates/default/index.html.twig. The name of our controller, default, is included in both of those paths.

We notice more similarities when opening our two files, preferably side by side. In DefaultController.php the following is called:

return $this->render('default/index.html.twig', [
	'controller_name' => 'DefaultController',
]);

Here we can see two things:

  • default/index.html.twig is the path to the new file inside of our templates folder. This is the path to our Twig template.
  • As a second argument we pass an array, containing controller_name with the value DefaultController. We use those parameters inside of our template. More on that later.

Before we go ahead it's vital to understand the created code.

Before the index function, inside of our new controller, are so called Annotations.

/**
 * @Route("/default", name="default")
 */
public function index()
{
    return $this->render('default/index.html.twig', [
        'controller_name' => 'DefaultController',
    ]);
}

We can see in this comment-block, that our route is /default

When we open localhost:8000/default, given your development server is running at this port, we should see an output containing Hello DefaultController! ✅. This is brought to us by the return $this->render expression inside of our index function. This generates the HTML content shown to the user, based on the passed template, which is default/index.html.twig in our case. Parameters are also passed to our template. They are located inside of the square brackets at the end.

At the moment we only pass the argument controller_name with the value 'DefaultController'. In the next step we will learn how to use those arguments passed.

Step 3 - What the template?

Symfony uses Twig as a template engine. Such a template engine is handy because:

  • we can reuse parts of it
  • it doesn't clutter our PHP code
  • it's easier to split the work for the backend / frontend

For now we look at the templates/default/index.html.twig. In here we notice a couple of things:

  • {% extends 'base.html.twig' %}: This indicates that all of our content is based on the base.html.twig. In here we would include our CSS and JS. Navbars would preferably be defined here to.
  • {% block body %} [...] {% endblock %}: In our base template we define several blocks. They act like placeholders. If a block is previously defined in another template, it will be overwritten. By utilizing that we may define default values, for example in the title.
  • <h1>Hello {{ controller_name }}! ✅</h1>: This is a basic h1 tag. The {{ controller_name }} is the most interesting part in here. In the previous section we passed an argument with the same name to our template.
(Click to expand) full index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello DefaultController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! :white_check_mark:</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/home/user/Documents/hetzneronline/a-basic-guide-to-php-symfony/src/Controller/DefaultController.php'|file_link(0) }}">src/Controller/DefaultController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/home/user/Documents/hetzneronline/a-basic-guide-to-php-symfony/templates/default/index.html.twig'|file_link(0) }}">templates/default/index.html.twig</a></code></li>
    </ul>
</div>
{% endblock %}

This is a simple task to consolidate our knowledge:

  • Pass the argument owner_name to the template. Print it out as a h2 tag under the h1. Feel free to add a short description.
  • Given the information that there is a stylesheets block in the base template, go ahead and make the background of the body lightgreen. Note: you have to write the style tags out too. The stylesheets block is not surrounded with them by default.
(Click to expand) Solution

DefaultController.php

 class DefaultController extends AbstractController
     {
         return $this->render('default/index.html.twig', [
             'controller_name' => 'DefaultController',
+            'owner_name' => 'Moritz',
         ]);
     }
 }

index.html.twig

 {% extends 'base.html.twig' %}

 {% block title %}Hello DefaultController!{% endblock %}
+{% block stylesheets %}
+<style>
+body {
+    background-color: lightgreen;
+}
+</style> {% endblock %}

 {% block body %}
 <style>
 <div class="example-wrapper">
     <h1>Hello {{ controller_name }}! :white_check_mark:</h1>
+    <h2>This application was braught to you by {{ owner_name }}</h2>

     This friendly message is coming from:
     <ul>

We now know how templates work and how we pass arguments to them. For a more in depth documentation of the functions Twig offers please refer to the official Twig documentation.

Step 4 - Creating our first custom routes

Creating our first custom route is the first step to take full advantage of Symfony's Routing feature. An infamous hello world page will be enough for now:

  • Add the route /default/hello
  • Create a hello.html.twig template

We also want to pass who to greet and display it in the template.

We need a route, for now we stick to our DefaultController with that. The route is defined by using the aforementioned annotations. As mentioned, our route is /default/hello. The resulting annotation look like so:

/**
 * @Route("/default/hello", name="default_hello")
 */

We put our hello function right bellow that. It handles the rendering, generating html code, from our template. The who parameter, stating who to greet, is also passed to our template. For now this will have the value World, that's because we want to greet the world.

public function hello()
{
    return $this->render('default/hello.html.twig', [
        'who' => 'World',
    ]);
}

Our template isn't complex either. We expand the base.html.twig, set the title and print it out in a h1 block. Because we as programmers are lazy by nature we only want to write the title once. Twigs set function is made for that. We set the title variable to Hello + who we want to greet + an exclamation mark(!). Using the tilde (~) we connect (concatenate) our strings.

{% set title = 'Hello ' ~ who ~ '!' %}

This variable can be used everywhere, being in our title block or inside of our body:

{% block title %}{{ title }}{% endblock %}

{% block body %}
    <h1>{{ title }}</h1>
{% endblock %}
(Click to expand) Full code

templates/default/hello.html.twig

{% extends "base.html.twig" %}

{% set title = 'Hello ' ~ who ~ '!' %}

{% block title %}{{ title }}{% endblock %}

{% block body %}
    <h1>{{ title }}</h1>
{% endblock %}

DefaultController.php

             'owner_name' => 'Moritz',
         ]);
     }
+
+    /**
+     * @Route("/default/hello", name="default_hello")
+     */
+    public function hello()
+    {
+        return $this->render('default/hello.html.twig', [
+            'who' => 'World',
+        ]);
+    }
 }

We now have learned how simple it is to create a custom route by hand. In the next step we learn why the concept of routes is so powerful and how to pass arguments with our routes.

Step 5 - Passing arguments with our routes

We have all seen it, for example in an online shop: the name or ID of a product is part of the url. In some cases those parameters may be passed using so called GET parameters (e.g. ?item=674). In other cases you may see something like this: /item/674. This is exactly what we are creating.

The content of the following steps is:

  • outsourcing our hello world into a HelloController
  • rename the route /default/hello to /hello/World where the World part is modifiable by the user and is passed to the template

We create our new controller by using the command php bin/console make:controller HelloController. In Step 1 this was already covered. The /default/hello route is no longer needed and can therefore be removed. We also move our hello.html.twig from templates/default/hello.html.twig to templates/hello/index.html.twig. The hello/index.html.twig can be overwritten / deleted beforehand.

A placeholder must be defined to access the URL parameters. Curly brackets indicate the placeholder. The name of it is stated inside of them. Our route therefore is /hello/{who}. As a result of that, a request to /hello/World has a who value of World. We pass this parameter just like we would pass any other parameter to a PHP function. Inside of the function it is available just like any other variable and can simply be passed to our template.

The resulting code now looks like this:

/**
 * @Route("/hello/{who}", name="hello")
 */
public function index($who)
{
    return $this->render('hello/index.html.twig', [
        'who' => $who,
    ]);
}

Please note: the name of the placeholder must match the name of the function parameter!

When we open localhost:8000/hello/World. We should be greeted by a friendly Hello World.

That's just what we had before. But what if we want to greet Hetzner as a thank you for this community space? Well, just replace the World with Hetzner and we're greeted by a Hello Hetzner!.

This is cool isn't it? But what happens when we open /hello/? Now we are greeted by a No route found for "GET /hello/": HTTP 404 Not Found. This is neither our template nor even anything nice at all. But what can we do about it? We can define default values! Since the route parameters are passed as function parameters, we can define this directly at the function.

We just change the function from public function index($who) to public function index($who = "World")! This is the exact same way we set default values for a function parameter in php!

If you want to do more reading on route parameters, for example how to verify them by using regular expressions, the Symfony Documentation on Routing is great.

Step 6 - Linking and redirecting

You may have noticed that we set a name for each and every one of our routes. But we haven't used them yet, so it's reasonable to assume that it's unneeded overhead. First of all, it's not needed! We do not have to write the name="default" part of the annotation. But why are we doing it anyways? Because you can do powerful things with it!

There are two main features we will take a look at for now. Those being redirecting and linking.

Redirecting

Given we want to redirect our users from / to /hello/Guest in order to greet our new visitors.

In normal PHP we would do it like this:

header('Location: /hello/Guest');

But this sucks, Symfony can do this way easier! Inside of the function that generates our content we utilize the following function:

return $this->redirectToRoute('hello', [
    'who' => 'Guest'
]);

The name of our route is the first parameter passed to this function. hello in our case. The parameters we want to pass with our route come in second place. Guest will be the value of our who parameter.

Now go ahead and:

  • create the / route inside of our DefaultController
  • redirect from / to /hello/Guest
(Click to expand) Full code
 class DefaultController extends AbstractController
 {
     /**
-     * @Route("/default", name="default")
+     * @Route("/", name="index")
      */
     public function index()
+    {
+        return $this->redirectToRoute('hello', [
+            'who' => 'Guest'
+        ]);
+    }
+
+    /**
+     * @Route("/default", name="default")
+     */
+    public function default()

This keeps us away from problems if we may change the route from /hello/{who} to /h/{who}. While it indeed is a bit more code, it's worth it because as long as we don't change the route name we won't have to change anything.

Linking

It is super easy to generate links inside of our template. Those we can use for an a href but also everywhere else.

Two Twig functions will be employed by us in the following steps:

  • path
  • url

The first one we mostly use for linking on the website because it only generates the relative URL. So it spits out the /hello/Guest. When the user is already on our website this is perfectly fine. But when we want to offer users a share link or anything like this, it isn't feasible to only have the relative path. The url function is used when you want to include the domain in the output. Passing a simple argument we can get the result of the other function too, it's way clearer when we stick to path for on-site linking and url for anything that comes from of-site.

We do the following:

  • link from /default/ to /hello/You
  • show the user a shareable url on /hello/{who}

By adding one line to our templates/default/index.html.twig we can accomplish the first thing. Since this is a HTML Hyperlink, we are using the a tag.

<li><a href="{{ path('hello', {'who': 'You'}) }}">Hello You!</a></li>

We pass two parameters to our path function, the same as we did when redirecting. First is the route name and in second position the route parameters.

By adding the following lines to our hello page, we show our users a shareable link:

<br>
<p>Share this:</p>
<a href="{{ url('hello', {'who': who}) }}">{{ url('hello', {'who': who}) }}</a>

Same syntax as the path function, but the result should be like this:

<br>
<p>Share this:</p>
<a href="http://localhost:8000/hello/You">http://localhost:8000/hello/You</a>

Conclusion

We have created multiple routes, and passed values with then. We also know how we can use route names. This is essential to go ahead with the next parts.

Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue

Discover our

Dedicated Servers

Configure your dream server. Top performance with an excellent connection at an unbeatable price!

Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more