Symfony 4 Workshop

Controller and Template

So far we have a stupid controller in src/Controller/DefaultController. Delete it, as we are going to do a view of a movie.

Create a new controller in src/Controller/MovieController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use App\Entity\Movie;

class MovieController extends Controller
{
    public function movie(int $movieId)
    {
        $movie = $this->getDoctrine()
            ->getRepository(Movie::class)
            ->find($movieId);

        return $this->render('movies/movie.html.twig', [
            'movie' => $movie,
        ]);
    }
}

Notice that we are now extending use Symfony\Bundle\FrameworkBundle\Controller\Controller;.

This allows us to have access to some useful methods. Especifically, we can have now access to the service container (more on this later), and the methods implemented in ControllerTrait.

Exercise

Take a moment to read the contents of ControllerTrait.

What is doing getDoctrine()?

What is doing render()?

Let's focus on this part:

1
2
3
4
5

$movie = $this->getDoctrine()
->getRepository(Movie::class)
->find($movieId);
        

First, we are getting access to Doctrine from the controller. Then, we are accessing the repository of the Entity Movie. Once we have the repository, we retrieve the movie that has the id we have asked for. With that data, Doctrine will populate a collection of Movies, translating the results from the SQL query to our object.

Once we have the movie, we want to render a template with them. This is where this second part plays its role:

1
2
3
4
5

return $this->render('movies/index.html.twig', [
    'movie' => $movie,
]);
        

Here we are saying: Render this template and pass movie as a parameter.

Our first Template

Theory

Some words about Twig.

Although it is possible to use PHP as a templating language, almost every Symfony project uses Twig, the templating language.

Let's install it:

composer req twig

Since we are using Flex to install the packages, we receive some setup already done for us.

In this case, we receive, among other things, a base template in templates/base.html.twig:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

We can extend this template with the extends tag. When we are extendig a template, we can override the blocks of the parent template.

Let's extend this template overriding title and body.

Create a new directory templates/movies and put this in templates/movies/movie.html.twig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{% extends 'base.html.twig' %}

{% block title %}{{movie.name}}{% endblock %}

{% block body %}
    <div class="movie">
        <h2>{{ movie.name }}</h2>
        <p>{{ movie.director }}</p>
        <p>{{ movie.year }}</p>
        <div class="actors">
            {% for actor in movie.actors %}
                <div>{{ actor.name}}</div>
            {% else %}
                This movie has no actors.
            {% endfor %}
        </div>
    </div>
{% endblock %}

Notice the for...else construction. This is a nice way of displaying an empty state if there are no items to iterate.

Now let's see this new page we have just created in a browser.

We should update the routing now, but updating the routing file every time we add a new route is tiresome. Luckyly, there is a nice helper for that.

Let's install the annotations recipe.

composer req annotations

This gives us access to a set of annotations that we can use. One of them is the route annotation.

Now let's update config/routes.yml for the last time. Replace its contents with:

1
2
3
controllers:
    resource: ../src/Controller/
    type: annotation
Tip
Make sure that you are editing config/routes.yml and not config/packages/routing.yml. It is easy to make that mistake!

With this configuration we are saying that the definition of the routes will be written as annotations in our Controllers.

Let's update our controller to use the route annotation:

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
<?php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Movie;

class MovieController extends Controller
{
    /**
    * @Route("/movie/{movieId}")
    */
    public function movie(int $movieId)
    {
        $movie = $this->getDoctrine()
            ->getRepository(Movie::class)
            ->find($movieId);

        return $this->render('movies/movie.html.twig', [
            'movie' => $movie,
        ]);
    }
}

Notice that the only changes are that we are only importing the Route annotation and adding it in a comment.

The parameters declared in the route, in this case movieId must match the names we use in the parameter declaration of our controller function ($movieId).

You can visit now the page that we have created by visiting the url http://localhost:8000/movie/1

The url may vary depending on the id of the elements in your database.

Exercise

Can you create a controller function for the list of movies?

Some hints:

  • In this function we don't have to declare any parameter.
  • The route must be accessible from the root URI: /.
  • In order to fetch all the movies, and not only one, you have to use the method of the repository findAll().
Reveal the solution
Exercise

Can you create a template that renders the list of movies?

Can you link the page of movie from this listing?

Reveal the solution

Although the link that we just wrote works, we can do better. What happens if at some point we change the url /movie/{movieId} for something else like/event/{movieId}. We would have to scan the templates to replace every link.

That is tiresome and error prone. Let's use Url generation instead.

Modify the route annotations to give them a name:.

1
2
3
4
5
6
7
8
9

/**
* @Route("/", name="index")
*/

/**
 * @Route("/movie/{movieId}", name="movie")
 */
            

Now we can use path function of Twig to generate a Url. Modify the link in /templates/movies/movie.html.twig to use this function

1
2
3

<a href="{{ path('movie', {'movieId': movie.id}) }}">
        

Improving our queries

This is working, but we can do some things better.

To see what are we talking about, let's install the profile tools:

composer req profiler

This gives us all sorts of useful information.

If we visit the page of a movie, we click on the debug toolbar, and we visit the Doctrine section, we will se something like this:

Why two database queries? We are only querying for a movie.

The problem here is that Doctrine does lazy loading, and we must be aware of that. When we retrieve the actors of the movie, Doctrine makes a second call to retrieve them, but it cannot know beforehand that we are going to access the relationships.

Let's fix that. Change the query in our controller to this:

1
2
3
4
5
6
7

$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT m, a FROM App:Movie m LEFT JOIN m.actors a WHERE m.id = :id'
)->setParameter('id', $movieId);
$movie = $query->getOneOrNullResult();
        

This looks like SQL, but it is DQL (Doctrine Query Language). It is aware of the properties in our entities and, as you can see, manages relationships by us, so in this case we don't have to say anything about the intermediate movie_actor table.

In this case, we are doing a LEFT JOIN with actors and adding it to the SELECT statement.

DQL works, but we have another option. In many cases it is convenient to build the query step by step, so we can reuse parts of the query to compose other queries. Let's see this in action using Docrine's QueryBuilder

1
2
3
4
5
6
7
8
9
10

$em = $this->getDoctrine()->getManager();
$query = $em->getRepository(Movie::class)
    ->createQueryBuilder('m')
    ->select('m, a')
    ->leftJoin('m.actors', 'a')
    ->where('m.id = :id')
    ->setParameter('id', $movieId)
    ->getQuery();
$movie = $query->getOneOrNullResult();

This query is completely equivalent, but written with a fluent interface that allows us to modify it programatically.

Which style to use is completely up to you.

In any case, if you check the profiler, we are now doing only one query.

There is still one thing that we could improve. It is generally a good practice to write queries in a separated place to avoid cluttering the Controller and to reuse them in the future from other parts of our project.

Let's create a Doctrine Repository to store this query in src/Repository/MovieRepository.php.

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
<?php

namespace App\Repository;

use App\Entity\Movie;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

class MovieRepository extends ServiceEntityRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Movie::class);
    }

    public function findOneWithActors(int $movieId)
    {
        $query = $this->createQueryBuilder('m')
            ->select('m, a')
            ->leftJoin('m.actors', 'a')
            ->where('m.id = :id')
            ->setParameter('id', $movieId)
            ->getQuery();

        return $query->getOneOrNullResult();
    }
}

We have also to specify in the Entity that we are using this repository. Edit the annotations of src/Enitity/Movie.php to reflect it:

1
2
3
4
5
/**
 * @ORM\Entity(repositoryClass="App\Repository\MovieRepository")
 * @ORM\Table(name="movie")
 */
class Movie

Now we can use this query from the controller in src/Controller/MovieController.php:

1
2
3
4
5
6
7
8
9
10
11
/**
 * @Route("/movie/{movieId}", name="movie")
 */
public function movie(int $movieId)
{
    $em = $this->getDoctrine()->getManager();
    
    return $this->render('movies/movie.html.twig', [
        'movie' => $em->getRepository(Movie::class)->findOneWithActors($movieId),
    ]);
}

With this our controller is much cleaner now. It is important to take the time to clean our controllers, and isolate pieces of code in functions that do only one thing.

With this we have a functional listing -> detail going on. But it is very ugly. Let's make it pretty.