Symfony Controllers - Making Your App Work For You
We've taken our standalone Twig template about as far as I'd like to go without using a custom controller method.
Let's now create our second custom controller method, and replicate - then enhance - the functionality we've seen so far.
I'm going to start by removing the hello_page
route defined in config/routes.yaml
.
# config/routes.yaml
- hello_page:
- path: /hello
- controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController::templateAction
- defaults:
- template: hello_page.html.twig
This momentarily breaks our /hello
page.
Before the helpdesk warning klaxon starts blaring, let's quickly fix this by creating a new method inside our WelcomeController
:
<?php
namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class WelcomeController extends AbstractController
{
/**
* @Route("/", name="welcome")
*/
public function index()
{
return $this->render('welcome/index.html.twig');
}
/**
* @Route("/hello-page", name="hello_page")
*/
public function hello()
{
return $this->render('hello_page.html.twig');
}
}
Ok, so some interesting points to cover:
We could have generated a new Controller class for this new route. It wouldn't have mattered. Feel free to do that, if you'd like.
I've changed the route path from /hello
to /hello-page
. This is to illustrate that changing the path makes no difference, so long as the route name
remains the same.
On that front, our route name
has stayed the same - it's still hello_page
. This keeps Twig happy.
Our method name - hello
- can really be anything. Anything, that is, except index
, which is already in use.
Finally, I haven't moved the hello_page.html.twig
template.
I haven't moved this template to illustrate the point that templates can live anywhere in the templates
directory. It would be better, in my personal opinion, to move this template under the welcome
subdirectory, simply to maintain order as our site grows. For now, I'm going to leave this as is.
php bin/console debug:router
-------------------------- -------- -------- ------ -----------------------------------
Name Method Scheme Host Path
-------------------------- -------- -------- ------ -----------------------------------
welcome ANY ANY ANY /
hello_page ANY ANY ANY /hello-page
_twig_error_test ANY ANY ANY /_error/{code}.{_format}
_wdt ANY ANY ANY /_wdt/{token}
_profiler_home ANY ANY ANY /_profiler/
_profiler_search ANY ANY ANY /_profiler/search
_profiler_search_bar ANY ANY ANY /_profiler/search_bar
_profiler_phpinfo ANY ANY ANY /_profiler/phpinfo
_profiler_search_results ANY ANY ANY /_profiler/{token}/search/results
_profiler_open_file ANY ANY ANY /_profiler/open
_profiler ANY ANY ANY /_profiler/{token}
_profiler_router ANY ANY ANY /_profiler/{token}/router
_profiler_exception ANY ANY ANY /_profiler/{token}/exception
_profiler_exception_css ANY ANY ANY /_profiler/{token}/exception.css
-------------------------- -------- -------- ------ -----------------------------------
After changing routes like this it's always a good idea to validate the new configuration has taken. I'm doing this using bin/console debug:router
.
Browsing around the site now shows that everything still works, and our "Hello Page" now uses the updated path (/hello-page
).
Passing In Variables
Let's pass in a variable from our controller, and update our hello_page.html.twig
template to display this variable:
{% extends 'base.html.twig' %}
{% block title %}Some custom title{% endblock title %}
{% block body %}
+ <div>
Hello, {{ app.request.query.get('name') | default('CodeReviewVideos') }}!
+ </div>
+ <div>
+ My lovely variable: {{ some_variable_name }}
+ </div>
{% endblock %}
I've added the extra div
tags here to make the output look a bit better.
Given that I am using a variable by the name of some_variable_name
in my Twig template, it stands that I need to pass in this variable - with this exact name - from my controller method.
If I don't, I'm going to see a big red 500
error:

As we're in development mode currently, I do see a nice error page explaining what went wrong, and why:
"Variable "some_variable_name" does not exist."
We saw a solution to this in the previous video: set a default
:
{% extends 'base.html.twig' %}
{% block title %}Some custom title{% endblock title %}
{% block body %}
<div>
Hello, {{ app.request.query.get('name') | default('CodeReviewVideos') }}!
</div>
<div>
My lovely variable: {{ some_variable_name | default('I am a lovely default value') }}
</div>
{% endblock %}
This "fixes" the problem. Refreshing now we see:
"My lovely variable: I am a lovely default value"
We haven't really solved the problem though.
Fortunately, doing so is easy.
Our controller method return
's the outcome of a call to $this->render(...)
:
/**
* @Route("/hello-page", name="hello_page")
*/
public function hello()
{
return $this->render('hello_page.html.twig');
}
If you're using an IDE like PhpStorm, ctrl+clicking render
will take you to the Symfony ControllerTrait.php
file where the render
method is defined.
From here, we can see the render
method's expected arguments:
/**
* Renders a view.
*
* @final since version 3.4
*/
protected function render(string $view, array $parameters = array(), Response $response = null): Response
The only mandatory argument is the first argument: string $view
.
We fulfill this criteria by passing in our template name, e.g. hello_page.html.twig
.
The second argument is an array
of $parameters
.
This is how we pass our variables into the template.
Knowing this, we can easily pass in our some_variable_name
parameter:
/**
* @Route("/hello-page", name="hello_page")
*/
public function hello()
{
return $this->render('hello_page.html.twig', [
'some_variable_name' => 'whatever we want here'
]);
}
Now refresh once again, and we see:
"My lovely variable: whatever we want here"
Easy, right?
These variables can be as simple or as complex as you need.
Repeating What We Had
In the previous video we pulled the name
query parameter from the URL, and displayed the contents in our template.
If you followed along you'll remember we used app.request.query.get('name')
.
We can also get access to the Request
object from a controller method. We simply need to inject it. In order to do this, we provide the correct typehint, and set a variable name - usually something vary similar to the typehint:
<?php
namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+ use Symfony\Component\HttpFoundation\Request;
class WelcomeController extends AbstractController
{
/**
* @Route("/", name="welcome")
*/
public function index()
{
return $this->render('welcome/index.html.twig');
}
/**
* @Route("/hello-page", name="hello_page")
*/
- public function hello()
+ public function hello(Request $request)
{
return $this->render('hello_page.html.twig', [
'some_variable_name' => 'whatever we want here'
]);
}
}
We can then use this object in our code.
Symfony is all about the journey from Request to Response.
It therefore stands that Symfony's Request
object is one of the most useful, and most frequently used objects you will work with. Injecting the Request
object in this way is commonplace in many of my controllers.
A decent IDE like PhpStorm will help you massively in understanding what the Request
object offers in terms of methods and properties.
We're going to start by accessing the query
public property. From this query
property we can get
access to the any query parameters we set on the current request. As a quick reminder, query parameters are those things on the URL after the ?
, like ?name=bob&age=25
.
This is very similar to what we did in the previous video, only with slightly different syntax:
/**
* @Route("/hello-page", name="hello_page")
*/
public function hello(Request $request)
{
$someVar = $request->query->get('someVar');
return $this->render('hello_page.html.twig', [
'some_variable_name' => $someVar,
]);
}
}
Now if you refresh your page, you should either see the default value from your template, or if you browse to http://127.0.0.1:8000/hello-page?someVar=whatever+goes+here
then:
"My lovely variable: whatever goes here"
If you followed along with the extra example in the previous video, then because the name
property is used directly in our Twig template, we could also set that via the query parameter still, too:
http://127.0.0.1:8000/hello-page?someVar=whatever+goes+here&name=Peter
And then we see:
" Hello, Peter! My lovely variable: whatever goes here "
One last thing before we move on.
The $someVar
local variable is redundant. We can inline the call to $request->query->get('someVar')
:
/**
* @Route("/hello-page", name="hello_page")
*/
public function hello(Request $request)
{
return $this->render('hello_page.html.twig', [
'some_variable_name' => $request->query->get('someVar'),
]);
}
And everything should behave the same.
In a real application we would need to validate any provided input such as shown using these query parameters. We could add in this logic to our controller method. Or we could use functionality provided to us out of the box as part of Symfony's routing annotations.
Route Placeholders
We've been using query parameters so far. It not only looks nicer, as well as offering us a bunch of extra functionality if we make use of routing placeholders.
What I mean here is instead of having:
http://127.0.0.1:8000/hello-page?name=Peter
We might switch to:
http://127.0.0.1:8000/hello/CodeReviewVideos
Let's make this change:
/**
* @Route("/hello/{name}", name="hello_page")
* @param string $name
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function hello(string $name)
{
return $this->render('hello_page.html.twig', [
'person_name' => $name,
]);
}
And update the templates/hello_page.html.twig
template:
{% extends 'base.html.twig' %}
{% block title %}Some custom title{% endblock title %}
{% block body %}
<div>
Hello, {{ person_name }}!
</div>
{% endblock %}
Now if we try and visit e.g. http://127.0.0.1:8000/hello/chris
, all should be good here, right?

Bzzzzt. Wrong, sir! Wrong.
Everything we were doing was technically correct. However:
We have our hello_page
as a link on our Navbar.
In order to visit the hello_page
, we need to provide a name
to meet the placeholder requirements. This is a mandatory piece of data, and we haven't provided a default.
Fortunately, fixing this is as easy as providing a default value in the hello
method's $name
argument:
- public function hello(string $name)
+ public function hello(string $name = 'CodeReviewVideos')
Cool. Now try refreshing and boom, our page loads as expected.
If we browse to:
http://127.0.0.1:8000/hello/chris
We see "Hello, Chris!"
If we browse to:
http://127.0.0.1:8000/hello
We see "Hello, CodeReviewVideos!"
And if we browse to:
http://127.0.0.1:8000/hello/
(note the trailing slash)
We get a 404
error: 'No route found for "GET /hello/"'
Ack. Well, I show you this because this is a real world problem. It's all well and good going through tutorials that show you the happy path. I prefer a more realistic approach. There is a solution to this problem. Feel free to add it in to your project.
As a side note here, there are often multiple ways to achieve the same goal when programming. Rather than provide a default value to your method's $name
argument, you could also use the defaults
annotation, e.g.:
/**
* @Route("/hello/{name}", name="hello_page", defaults={"name" = "CodeReviewVideos"})
* @param string $name
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function hello(string $name)
{
return $this->render('hello_page.html.twig', [
'person_name' => $name,
]);
}
There's no real difference here, to the best of my knowledge. However, across your project it's best to pick one approach and stick to it.
Routing Restrictions
Right now it's possible to browse to:
http://127.0.0.1:8000/hello/chris%20123
Try it.
What do you see?
Well, I will tell you anyway, even if you haven't.
As before we pass in chris
, but then also the URL encoded representation of a space (%20
), and then some numbers (123
).
Maybe we don't want to allow spaces, or numbers.
Symfony has us covered here, too.
/**
- * @Route("/hello/{name}", name="hello_page")
+ * @Route("/hello/{name}", name="hello_page", requirements={"name"="[A-Za-z]+"})
* @param string $name
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function hello(string $name = 'CodeReviewVideos')
{
return $this->render('hello_page.html.twig', [
'person_name' => $name,
]);
}
By specifying our route requirements
we can restrict what routes this controller method will match on.
In this instance I've stated that the name
placeholder of our route must be one or more character in capitals or lowercase, A to Z.
The tricky part of the requirements
section is that you must use a Regular Expression for your match. I like sites like regexr to help me come up with regular expression patterns that work. Often examples already exist to solve the vast majority of circumstances you will encounter. My advice: keep it simple.
There's more to learn about Routing in Symfony 4, but what we've covered in this and the previous video are more than enough to get you up and running.