Make Your Own Controller


Towards the end of the previous video we had created a single route which, when accessed, would render out the contents of our template (e.g. the hello_page.html.twig), and return that as the body of the Response.

Whilst we didn't need to create our own Controller class, or controller method (in prior Symfony versions most often called an Action) to make this work, we did use a Symfony provided Controller (TemplateController) and Action (templateAction) behind the scenes.

I also mentioned that in 99% of cases, you would likely not work in this way, in the real world.

Still, it is interesting to know of this feature, and also it saved us from having to do anything more than we otherwise had to, in order to get started.

Lets now create our own Controller class, and in that controller we will create our very first controller Method.

Making A Controller Class

There are a bunch of ways to create a new Controller class.

We could:

  • Copy and paste code from the docs
  • Manually type everything out from memory
  • Create and use a Live Template
  • Copy / paste a controller class from an existing project

And so on.

Or, and perhaps easiest, we can make one:

php bin/console

# ...

 make
  make:auth                               Creates an empty Guard authenticator
  make:command                            Creates a new console command class
  make:controller                         Creates a new controller class
  make:entity                             Creates a new Doctrine entity class
  make:form                               Creates a new form class
  make:functional-test                    Creates a new functional test class
  make:serializer:encoder                 Creates a new serializer encoder class
  make:subscriber                         Creates a new event subscriber class
  make:twig-extension                     Creates a new Twig extension class
  make:unit-test                          Creates a new unit test class
  make:validator                          Creates a new validator and constraint class
  make:voter                              Creates a new security voter class

What this means is we can type in php bin/console make:controller, and follow the prompt to create a basic controller, and a 'demo' action.

Let's try it:

php bin/console make:controller

 Choose a name for your controller class (e.g. BravePuppyController):
 > WelcomeController

 created: src/Controller/WelcomeController.php

  Success! 

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

Now if we look in our app, under the src/Controller directory, we do indeed see WelcomeController.php.

The contents of WelcomeController.php are:

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class WelcomeController extends Controller
{
    /**
     * @Route("/welcome", name="welcome")
     */
    public function index()
    {
        // replace this line with your own code!
        return $this->render('@Maker/demoPage.html.twig',
          'path' => str_replace($this->getParameter('kernel.project_dir').'/', '', __FILE__)
        ]);
    }
}

I've taken the slight liberty of adjusting the formating of the return statement for ease of reading. The actual generated code will be on a single line.

There's a bunch of interesting things happening in this code, and whilst it would be cool to dive into each part, it would also make things more complex than they need be at this point.

What I will suggest is that we make two quick changes to the generated code:

  • Remove the use Symfony\Component\HttpFoundation\Response; line
  • Change extends Controller to extends AbstractController

Removing use Symfony\Component\HttpFoundation\Response; is easy enough. It's unused, so we can simply delete the entire line. I'm not sure why it's provided in the generated output, and it may not be in future versions.

Next, change:

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

to:

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

And update:

class WelcomeController extends Controller

to:

class WelcomeController extends AbstractController

Whilst not something we will immediately cover, this is a recommended Best Practice for Symfony Controllers. We'll cover what this change does a little later on.

Routin' Tootin'

For the rest of this video we will focus on that new route that is now available in our application, thanks to the routing Annotation generated for us:

    /**
     * @Route("/welcome", name="welcome")
     */
    public function index()

This routing annotation maps the route or path of /welcome to the index method.

Geek Trivia: If you're coming from Symfony 2 or Symfony 3, you may wonder what happened to the Action suffix, as in indexAction. This is possible as our WelcomeController is a service.

We can double check this route is correctly configured by running the bin/console debug:router command again:

bin/console debug:router
 -------------------------- -------- -------- ------ ----------------------------------- 
  Name                       Method   Scheme   Host   Path                               
 -------------------------- -------- -------- ------ ----------------------------------- 
  welcome                    ANY      ANY      ANY    /welcome                           
  _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   
  hello_page                 ANY      ANY      ANY    /hello                             
 -------------------------- -------- -------- ------ ----------------------------------- 

It's not immediately obvious, but the new route is at the top, and our existing route is at the bottom.

If you haven't already done so, head over to http://127.0.0.1:8000/welcome and check out your shiny new controller output:

symfony 4 make:controller output

So far, so good.

Changing Things Up

Let's set our new controller action to respond on the root of our site. In other words, let's set this controller to respond to /.

This is easily achieved. We just need to update the routing annotation:

    /**
     * @Route("/", name="welcome")
     */
    public function index()

Don't hesitate to re-check the output of bin/console debug:router when making changes. If you see the change in your console output, you know Symfony understood and accepted your change.

Next, rather than rendering out the generated template, let's create a new one of our own.

We're going to make use of Bootstrap 4. Feel free to change this up to be any CSS framework you like. Or just skip the CSS, if visuals aren't your cup of tea.

We'll start by creating a new template inside a subdirectory of the templates directory:

mkdir templates/welcome

And into the new templates/welcome directory we will create a new template, index.html.twig:

touch templates/welcome/index.html.twig

This name matches that of our Controller method, but it doesn't have to. On bigger projects, a naming convention like this will make your life a little easier. Use whatever you see fit, of course.

Bootstrap offer a few example templates, and I've slightly customised one of these for the following:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Let's Explore Symfony 4</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"
          integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy"
          crossorigin="anonymous">
</head>

<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
            <div class="container">

                <a class="navbar-brand" href="#">Home</a>

                <div class="collapse navbar-collapse">
                    <ul class="navbar-nav mr-auto">
                        <li class="nav-item">
                            <a class="nav-link" href="/hello">Hello Page</a>
                        </li>
                    </ul>
                </div>
            </div>

        </nav>
    </header>

    <main role="main" class="container main">
        <div>
            <h1>Let's Explore Symfony 4</h1>
            <p class="lead">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras rutrum sapien mauris, venenatis
                facilisis neque tincidunt vel. Maecenas vel felis vel turpis scelerisque eleifend. Fusce nec purus egestas,
                efficitur nisi ac, ultrices nulla. Pellentesque eu mollis tortor, in mollis nisl. Maecenas rhoncus quam non
                lacinia mollis.</p>
        </div>
    </main>
</body>
</html>

Which should look something like this:

lets explore symfony 4 page

It would be nice to push the h1 down a few pixels. We could add that as a style element direct to the template, but a better way is to move styles out to a separate file.

We need our style file(s) to be publicly accessible, so that means they need to live in the public directory.

mkdir public/css
touch public/css/custom-styles.css

Remember, touch is just a nerdy way to create a new, empty file with the given name, from the CLI. Feel free to create the directory, and the file from your IDE.

Rather than style the h1 tag directly, I'm going to "go up" to the main element, and apply my style to the .main class:

.main {
    margin-top: 20px;
}

This won't just work. We need to tell our template about our stylesheet.

I'm going to add my custom stylesheet entry just above the closing head tag. This ensures the Bootstrap CSS is run first, then my custom styles are applied "on top".

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Let's Explore Symfony 4</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="{{ asset('css/custom-style.css') }}" />
</head>

Notice that the stylesheet is not using a hardcoded path. Instead, we can use the asset helper function to let the Asset Component take care of figuring out where our resources are located, which is super useful when our template setups get more complicated.

By using the symfony/website-skeleton we do already have access to a more full-featured front end assets management library: Webpack Encore. We aren't using this just yet. You don't always need everything the framework provides, so don't feel you have to use all the shiny stuff. Just use the bits you need.

Try It Yourself

Symfony 4 best practices for front end asset management (think: JS and CSS, amongst others) is to use Webpack Encore.

Try using the documentation, switching from our current CSS approach to using Webpack Encore.

What other tools / software did you need to install?

Was it worth the extra effort?

Consider leaving a comment below sharing your opinions and experiences.

Better Links

In our navbar we have added a couple of links:

    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container">

            <a class="navbar-brand" href="/">Home</a>

            <div class="collapse navbar-collapse">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="/hello">Hello Page</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

We have Home, which uses the site root (/), and our hello page on /hello.

Remember earlier how we changed the route path from /welcome to /, but kept the route name as welcome?

Well, one of the nicer parts about Twig is that if we don't hardcode our links, but instead, use the link's name, then we can easily change our paths without worrying about breaking our site.

What this means in practice is that instead of using:

<a class="navbar-brand" href="/">Home</a>

We would be better with:

<a class="navbar-brand" href="{{ path('welcome') }}">Home</a>

Likewise, for the Hello Page, we'd be better switching from:

<a class="nav-link" href="/hello">Hello Page</a>

to:

<a class="nav-link" href="{{ path('hello_page') }}">Hello Page</a>

Meaning our revised navbar looks like this:

    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container">

            <a class="navbar-brand" href="{{ path('welcome') }}">Home</a>

            <div class="collapse navbar-collapse">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('hello_page') }}">Hello Page</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

Both path and asset are just some of the many available functions inside your Twig templates. Some you will use more than others.

You can test out how useful this is right now by changing the path of your hello_page to something other than /hello. As soon as you do this, and refresh your browser, you will see the link updates automatically to use the new path. This will keep working as long as you don't change the route's name. If you do change the name, be sure to update any path function calls in your template(s).

About The Hello Page

One kinda glaring problem right now is if we browse to the site root (/) we have a nice Bootstrap 4 page with our navbar, a dash of styling, and we feel quite good about ourselves.

Then, we click the "Hello Page" link and boom, no styles, no navbar, and the only way out is to hit the back button.

Wouldn't it be nicer if our "Hello Page", and any subsequent pages we add, all re-used the same basic layout?

It sure would, Kent.

Let's explore how we can do just that, and more, in the very next video.

Code For This Course

Get the code for this course.

Episodes