Salta el contingut

Desenvolupament de serveis REST amb Symfony

Construint una API REST bàsica

Vegem ara quins passos donar per a construir una API REST que done suport a les operacions bàsiques sobre una o diverses entitats: consultes (GET), insercions (POST), modificacions (PUT) i esborrats (DELETE).

A l'hora d'implementar una solució en Symfony ens trobem en tres opcions:

  1. API senzilla amb els mètodes que ens proporciona la classe AbstractController.
  2. Fer ús d'un bundle com FOSRestBundle que ens permet una major flexibilitat.
  3. Fer ús d'API platform, una ferramenta molt completa basada en PHP per a crear REST API.

L'elecció d'una solució o altra dependrà de cada projecte. En aquest curs emprarem FOSRestBundle.

Arrencada del projecte

El projecte d'exemple serà una projecte independent basat en Movies. Així podrem aprofitar el model de dades.

Primerament crearem el projecte de Symfony:

composer create-project symfony/skeleton api-movies-symfony ^5.4
En aquesta ordre crearem un projecte bàsic de Symfony en la versió 5.4, es tracta d'un projecte que no inclou ni Doctrine, ni Twig, per exemple.

Twig no serà necessari ja que es tracta d'una API però Doctrine, sí. A més, caldran altres components que instal·larem a continuació:

composer require orm
composer require orm-fixtures --dev
composer require security
composer require symfony/maker-bundle
composer require symfony/validator
composer require symfony/apache-pack
composer require sensio/framework-extra-bundle

El model del dades, les entitats i els repositoris el copiarem del projecte Movies.

Caldrà instal·lar també els components que s'usen per a la generació de les dades:

composer require woodsandwalker/faker-picture --dev
composer require FakerPHP/Faker --dev
Llevarem les referències a VichUploader, ja que usarem els formularis d'una altra forma.

Instal·lant els bundles necessaris

A més del que ja tenim instal·lat caldrà fer ús d'un bundle específic per a desenvolupar APIs, anomenat FOSRestBundle. Està creat per l'equip de desenvolupament Friends Of Symfony, responsable de diversos bundles populars per a aquest framework. A més, aquest bundle requereix d'un altre addicional, que s'encarregarà de serializar/deserializar les dades que s'envien client i servidor, emprant el format JSON (encara que es pot triar un altre format, com XML o HTML, però ens centrarem en JSON). Aquest bundle s'anomena Serializer .

Abans de continuar, alguns conceptes:

  • Serialitzar. La serialització és el procés de convertir un objecte en una altre format per a emmagatzemar-lo o transmetre'l i posteriorment ser decodificat.
  • Normalitzar. Ajuden en el procés de serialització, convertint els objectes en un element intermig, com pot ser una array.

Component serialitzador

Resumint, aquests són els comandos que necessitarem (i en aquest ordre):

composer require symfony/serializer
composer require friendsofsymfony/rest-bundle

Configurant el serialitzador

# config/packages/framework.yaml
framework:
    ...
    serializer:
        enabled: true
        mapping:
            paths: ['%kernel.project_dir%/config/serializer/']

Definim els grups de serialització de les entitats.

# config/serializer/Movie.yaml
App\Entity\Movie:
    attributes:
        id:
            groups: ['movie']
        title:
            groups: ['movie']
        poster:
            groups: ['movie']
        overview:
            groups: ['movie']
        rating:
            groups: ['movie']
        releaseDate:
            groups: ['movie']

Configurem FOSRestbundle.

# config/packages/fos_rest.yaml

# Read the documentation: https://symfony.com/doc/master/bundles/FOSRestBundle/index.html
fos_rest:
    param_fetcher_listener: true
    view:
        empty_content: 200
        view_response_listener: true
        failed_validation: HTTP_BAD_REQUEST
        formats:
            json: true
            xml: false
    body_listener:
        decoders:
            json: fos_rest.decoder.json
    format_listener:
        rules:
            - { path: '/api', priorities: [ 'json' ], fallback_format: json, prefer_extension: false }
            - { path: '^/', stop: true, fallback_format: html }
    exception:
        enabled: true
    serializer:
        serialize_null: true

Definint els serveis

Ara que ja tenim instal·lat el necessari per a començar a definir els serveis, anem al que és important. Crearem una nova classe, on definirem els serveis bàsics sobre l'entitat Movie. Cridarem a aquesta classe ApiMovieController, i l'afegirem en la carpeta src/Controller:

La classe té una anotació @Route, que implica que qualsevol ruta que indiquem dins va a tenir aqueix prefix (en aquest cas, totes les rutes dels mètodes interns tindran el prefix /api/v1/movies).

Llistat de tots els elements (GET /)

Afegirem un mètode a la nostra classe anterior perquè torne, en format JSON totes les pel·lícules de la base de dades. El codi del mètode és el següent:

class ApiMovieController extends AbstractFOSRestController
{
    /**
     * @Rest\Get(path="/api/v1/movies", name="api_movies")
     * @Rest\View(serializerGroups={"movie"}, serializerEnableMaxDepthChecks=true)
     */
    public function list(MovieRepository $movieRepository) {
        return $movieRepository->findAll();
    }
}

Analitzem alguns aspectes importants que no hem vist abans:

  • El mètode list té una anotació @Rest\Get que és similar a @Route però permet especificar la sol·licitud que s'atendrà (GET, en aquest cas), i la ruta associada (hem indicat /api/v1/movies, la qual cosa significa que s'atenen peticions GET a /api/v1/movies, que és la ruta base de la classe).
  • Dins del mètode, simplement obtenim, el llistat de totes les pel·lícules que ja es mostra serialitzat a JSON, gràcies a la configuració que hem realitzat.

Inserció de recursos POST (/api/v1/movies)

Hi ha diverses alternatives però per simplificar la gestió de les insercions la farem amb l'ajuda del component symfony/form que caldrà instal·lar.

composer require form

Amb l'ordre make:form crearem un el formulari que quedarà així:

namespace App\Form;

use App\Entity\Movie;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MovieType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title')
            ->add('poster')
            ->add('overview')
            ->add('releaseDate', DateType::class, ["widget"=>"single_text"])
            ->add('updatedAt')
            ->add('genre')
            ->add('user')
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Movie::class, 
            'csrf_protection'=>false
        ]);
    }
}

En aquest formulari no ens preocupem pel tipus de control a mostrar perquè no és rellevant excepte en el cas de la data, que sí que cal especificar que espera la data com un text. Afegim també les propietats que formen part de relacions user i genre.

En el cas de les opcions de configuració cal indicar que no usarem protecció CSRF. El sistema de control d'accés l'implementarem més endavant.

El controlador quedaria així

    /**
     * @Rest\Post(path="/movies", name="api_movies_create")
     * @Rest\View(serializerGroups={"movie"}, serializerEnableMaxDepthChecks=true)
     */
    public function new(Request $request, EntityManagerInterface $entityManager)
    {       
        $movie = new Movie();
        $form = $this->createForm(MovieType::class, $movie);

        $data = json_decode($request->getContent(), true);
        $form->submit($data);

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->persist($movie);
            $entityManager->flush();

            return $this->view($movie, Response::HTTP_CREATED);
        }
        return $this->view($form, Response::HTTP_BAD_REQUEST);
    }

Després de crear els dos objectes el que fem és obtenir les dades del body de la sol·licitud, les convertim en un array associatiu i les processem mitjançant el formulari. Com que volem modificar el codi d'estat passem la informació a la vista.

Respecte a les entitats relacionades el que fem és enviar l'identificador en sol·licitud JSON, per exemple:

{ 
    "title": "Ava",
    "releaseDate": "2021-09-24",
    "genre": 9,
    "overview": "Lorem ipsum ...",
    "poster": "unnamed.jpg",
    "user": 7
}    

La resta de verbs (PUT i DELETE) es poden implementar seguint la mateixa estructura.

Recursos