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:
- API senzilla amb els mètodes que ens proporciona la classe
AbstractController
. - Fer ús d'un bundle com FOSRestBundle que ens permet una major flexibilitat.
- 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
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
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.
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
- En Primeros pasos con Symfony 5 como API REST es genera una API basada en els mateixos components que hem utilitzat.
- En Getting started REST API with Symfony 4 s'utilitzen diversos components addicionals com el Fos-Rest-Bundle i JMS Serializer.
- En Curso de Symfony 5. Creando una API desde cero teniu un curs molt complet sobre com crear una API en Symfony 5 des de cero.
- API Platform és un framework que permet crear API de forma quasi automàtica en Symfony.
- https://medium.com/francisco-ugalde/restful-api-con-symfony-4-jwt-parte-1-2accdf59a0ae