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.twig
is the path to the new file inside of ourtemplates
folder. This is the path to our Twig template.- As a second argument we pass an array, containing
controller_name
with 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 basich1
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 ah2
tag under theh1
. 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 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.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 theWorld
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.