Step 6: Creating a Controller

5.0 version
Maintained

Creating a Controller

Our guestbook project is already live on production servers but we cheated a little bit. The project doesn’t have any web pages yet. The homepage is served as a boring 404 error page. Let’s fix that.

When an HTTP request comes in, like for the homepage (http://localhost:8000/), Symfony tries to find a route that matches the request path (/ here). A route is the link between the request path and a PHP callable, a function that creates the HTTP response for that request.

These callables are called “controllers”. In Symfony, most controllers are implemented as PHP classes. You can create such a class manually, but because we like to go fast, let’s see how Symfony can help us.

Being Lazy with the Maker Bundle

To generate controllers effortlessly, we can use the symfony/maker-bundle package:

1
$ symfony composer req maker --dev

As the maker bundle is only useful during development, don’t forget to add the --dev flag to avoid it being enabled in production.

The maker bundle helps you generate a lot of different classes. We will use it all the time in this book. Each “generator” is defined in a command and all commands are part of the make command namespace.

The Symfony Console built-in list command lists all commands available under a given namespace; use it to discover all generators provided by the maker bundle:

1
$ symfony console list make

Choosing a Configuration Format

Before creating the first controller of the project, we need to decide on the configuration formats we want to use. Symfony supports YAML, XML, PHP, and annotations out of the box.

For configuration related to packages, YAML is the best choice. This is the format used in the config/ directory. Often, when you install a new package, that package’s recipe will add a new file ending in .yaml to that directory.

For configuration related to PHP code, annotations are a better choice as they are defined next to the code. Let me explain with an example. When a request comes in, some configuration needs to tell Symfony that the request path should be handled by a specific controller (a PHP class). When using YAML, XML or PHP configuration formats, two files are involved (the configuration file and the PHP controller file). When using annotations, the configuration is done directly in the controller class.

To manage annotations, we need to add another dependency:

1
$ symfony composer req annotations

You might wonder how you can guess the package name you need to install for a feature? Most of the time, you don’t need to know. In many cases, Symfony contains the package to install in its error messages. Running symfony make:controller without the annotations package for instance would have ended with an exception containing a hint about installing the right package.

Generating a Controller

Create your first Controller via the make:controller command:

1
$ symfony console make:controller ConferenceController

The command creates a ConferenceController class under the src/Controller/ directory. The generated class consists of some boilerplate code ready to be fine-tuned:

src/Controller/ConferenceController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

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

The @Route("/conference", name="conference") annotation is what makes the index() method a controller (the configuration is next to the code that it configures).

When you hit /conference in a browser, the controller is executed and a response is returned.

Tweak the route to make it match the homepage:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -8,7 +8,7 @@ use Symfony\Component\Routing\Annotation\Route;
 class ConferenceController extends AbstractController
 {
     /**
-     * @Route("/conference", name="conference")
+     * @Route("/", name="homepage")
      */
     public function index()
     {

The route name will be useful when we want to reference the homepage in the code. Instead of hard-coding the / path, we will use the route name.

Instead of the default rendered page, let’s return a simple HTML one:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -3,6 +3,7 @@
 namespace App\Controller;

 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;

 class ConferenceController extends AbstractController
@@ -12,8 +13,13 @@ class ConferenceController extends AbstractController
      */
     public function index()
     {
-        return $this->render('conference/index.html.twig', [
-            'controller_name' => 'ConferenceController',
-        ]);
+        return new Response(<<<EOF
+<html>
+    <body>
+        <img src="/images/under-construction.gif" />
+    </body>
+</html>
+EOF
+        );
     }
 }

Refresh the browser:

The main responsibility of a controller is to return an HTTP Response for the request.

Adding an Easter Egg

To demonstrate how a response can leverage information from the request, let’s add a small Easter egg. Whenever the homepage contains a query string like ?hello=Fabien, let’s add some text to greet the person:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -3,6 +3,7 @@
 namespace App\Controller;

 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;

@@ -11,11 +12,17 @@ class ConferenceController extends AbstractController
     /**
      * @Route("/", name="homepage")
      */
-    public function index()
+    public function index(Request $request)
     {
+        $greet = '';
+        if ($name = $request->query->get('hello')) {
+            $greet = sprintf('<h1>Hello %s!</h1>', htmlspecialchars($name));
+        }
+
         return new Response(<<<EOF
 <html>
     <body>
+        $greet
         <img src="/images/under-construction.gif" />
     </body>
 </html>

Symfony exposes the request data through a Request object. When Symfony sees a controller argument with this type-hint, it automatically knows to pass it to you. We can use it to get the name item from the query string and add an <h1> title.

Try hitting / then /?hello=Fabien in a browser to see the difference.

Note

Notice the call to htmlspecialchars() to avoid XSS issues. This is something that will be done automatically for us when we switch to a proper template engine.

We could also have made the name part of the URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -9,13 +9,19 @@ use Symfony\Component\Routing\Annotation\Route;
 class ConferenceController extends AbstractController
 {
     /**
-     * @Route("/", name="homepage")
+     * @Route("/hello/{name}", name="homepage")
      */
-    public function index()
+    public function index(string $name = '')
     {
+        $greet = '';
+        if ($name) {
+            $greet = sprintf('<h1>Hello %s!</h1>', htmlspecialchars($name));
+        }
+
         return new Response(<<<EOF
 <html>
     <body>
+        $greet
         <img src="/images/under-construction.gif" />
     </body>
 </html>

The {name} part of the route is a dynamic route parameter - it works like a wildcard. You can now hit /hello then /hello/Fabien in a browser to get the same results as before. You can get the value of the {name} parameter by adding a controller argument with the same name. So, $name.


This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.