Series index
- Setup
- Routes, templates and controllers (You are here)
- Working with the database (Not yet finished)
- 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.twigis the path to the new file inside of ourtemplatesfolder. This is the path to our Twig template.- As a second argument we pass an array, containing
controller_namewith the valueDefaultController. 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 thebase.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 basich1tag. 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_nameto the template. Print it out as ah2tag under theh1. Feel free to add a short description. - Given the information that there is a
stylesheetsblock in the base template, go ahead and make the background of the bodylightgreen. 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.twigtemplate
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/helloto/hello/Worldwhere theWorldpart 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:
pathurl
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.