Building controllers to handle HTTP requests

May 9, 2023 MrAnyx 5 min de lecture
Intermédiaire

Controlers with Syfmony allow to manage HTTP requests that are received by our application. We will be able to define the urls available for our application, the possible methods (GET, POST, ...), the mandatory parameters, ... all in order to return or not HTML, JSON... In the case of an api, we will only return JSON content.

Creation of the controller

To create a controller, simply use the make:controller command using the maker-bundle.

php bin/console make:controller --no-template TodoController

The --no-template flag allows to not create a twig template associated to the controller. For an api, this is exactly what we want.

A new file should have been created in the src/Controller folder.

A first /todo route should be available. In order to check that everything works, go to your api tester. Enter the url http://localhost:port/todos with the GET method and check the result.

You should see the following content:

{
    "message": "Welcome to your new controller!",
    "path": "src/Controller/TodoController.php"
}

Creation of the first api route

Now that we have our first functional controller, we can create our first custom route.

This route will be used to list all the todo that have been created.

To do this, we will add new annotations to the controller and to the existing route. We will add :

  • The #[Route("/api", "api_")] annotation on top of the class
  • The #[Route('/todos', name: 'todos', methods: ["GET"])] annotation above the index method

The annotation for the class allows to define a common url and name prefix for all routes.

Then, the method annotation allows to define precisely the url that should be used to request the route. We can also specify the possible methods to request this route (GET, POST, PATCH, ...).

You should finally have the following result.

// src/Controller/TodoController.php

namespace App\Controller;

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

#[Route("/api", "api_")]
class TodoController extends AbstractController
{
    #[Route('/todos', name: 'todos', methods: ["GET"])]
    public function index(): JsonResponse
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/TodoController.php',
        ]);
    }
}

Now, instead of requesting the url /todo we will have to request the url /api/todos.

Serialization of the result

At the moment, we have the same text for each request, which is not the goal. Instead, we want to return the list of todo that have been created in a JSON format.

First, we will have to retrieve the elements that will be serialized later.

To do this, we will use the Repository associated with the Todo entity we created in a previous lesson.

Thanks to the controller dependency injection, we can retrieve this repository by adding a simple parameter to our method.

// src/Controller/TodoController.php

use App\Repository\TodoRepository;

public function index(TodoRepository $todoRepository): JsonResponse
{
    //...
}

We will now be able to use this repository in our method to retrieve all the elements.

To do so, we will use the findAll method of the repository.

// src/Controller/TodoController.php

$todos = $todoRepository->findAll();

If we debug the $todos variable with Symfony's dd method, we'll see that there are the 5 todos we created earlier.

This is a good sign 😄

Now we just have to serialize these elements. To do this, we just have to use the json method and then fill in the list of todos.

// src/Controller/TodoController.php

$todos = $todoRepository->findAll();

return $this->json($todos);

This gives us the following result:

[
    {},
    {},
    {},
    {},
    {}
]

Problem is, it doesn't match the data we debugged.

If we take a closer look at the json method in the AbstractController class, we can notice something.

// vendor/symfony/framework-bundle/Controller/AbstractController.php

/**
 * Returns a JsonResponse that uses the serializer component if enabled, or json_encode.
 *
 * @param int $status The HTTP status code (200 "OK" by default)
 */
protected function json(mixed $data, int $status = 200, array $headers = [], array $context = []): JsonResponse
{
    if ($this->container->has('serializer')) {
        $json = $this->container->get('serializer')->serialize($data, 'json', array_merge([
            'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS,
        ], $context));

        return new JsonResponse($json, $status, $headers, true);
    }

    return new JsonResponse($data, $status, $headers);
}

Indeed, a first condition is executed to check if the Symfony serializer is registered in the dependency container. In our case, this is not the case, so our Todo list is simply put in the JsonResponse object without really caring about its format.

To fix this problem, we just need to install the serializer package with Symfony Flex.

composer require serializer

Now, if we test this same route again, we should see a different result.

[
    {
        "id": 1,
        "title": "Voluptatem maiores omnis velit odit. Neque quaerat ea sed enim sint. Nostrum eaque autem dolores voluptatem sed mollitia. Maxime voluptates cum sint nisi impedit. Maxime reprehenderit ad voluptatibus. Sed ea quaerat blanditiis cupiditate.",
        "createdAt": "2023-04-17T21:09:53+00:00",
        "updatedAt": "2023-04-17T21:09:53+00:00",
        "completed": false
    },
    {
        "id": 2,
        "title": "Veniam fugit ut autem quis culpa minima vitae. Esse beatae qui est quis perferendis delectus. Quisquam sit voluptatem ducimus eaque quae. Aliquam ducimus et voluptatum eum libero vero. Rem quae minus quia minus architecto saepe exercitationem.",
        "createdAt": "2023-04-17T21:09:53+00:00",
        "updatedAt": "2023-04-17T21:09:53+00:00",
        "completed": false
    },
    {
        "id": 3,
        "title": "Facilis est reiciendis magnam veniam expedita voluptatem molestias. Porro et reprehenderit ratione aliquam nesciunt et. Aut quisquam ipsa culpa.",
        "createdAt": "2023-04-17T21:09:53+00:00",
        "updatedAt": "2023-04-17T21:09:53+00:00",
        "completed": false
    },
    {
        "id": 4,
        "title": "Aut a ut suscipit tempora. Aut enim temporibus officia necessitatibus aut numquam. Molestias iusto minus omnis tempore eum minus. Ut numquam est autem et.",
        "createdAt": "2023-04-17T21:09:53+00:00",
        "updatedAt": "2023-04-17T21:09:53+00:00",
        "completed": false
    },
    {
        "id": 5,
        "title": "Pariatur excepturi minus doloribus nemo. Aut et animi reiciendis delectus veniam quasi hic. Sequi id tempore quis ut eius nihil dolor. In odio molestias nulla tenetur sed voluptas. Quam et qui officiis. Fugiat optio aut rerum excepturi.",
        "createdAt": "2023-04-17T21:09:53+00:00",
        "updatedAt": "2023-04-17T21:09:53+00:00",
        "completed": false
    }
]

Indeed, now we can see our list of todos. It's perfect 👍

In summary

In this lesson we discovered how to create a controller with Symfony's maker-bundle and we saw how to define possible urls with the Route annotation that can be placed before a controller or a method.

We also learned how to return an object or a list of objects in a serialized version thanks to the serializer package of Symfony.

In the next lesson, we'll see more details about the routes structure of an api and how to create routes to perform CRUD operations (create, read, update, delete)

Cette œuvre est mise à disposition selon les termes de la licence Licence Creative Commons