Construyendo un RestFull API con Symfony y AngularJS parte 2

June 18, 2021

En esta sección le damos continuidad a: creando un RESTfull API con Symfony, para ello, instalaremos el resto de las dependencias con composer, generaremos nuestra entidad y completaremos un CRUD utilizando el componente de formularios de Symfony, FOSRestBundle, JMSSerializerBundle y NelmioApiDocBundle para completar el backend de nuestra aplicación.

NelmioApiDocBundle

Es un bundle que nos facilita la labor de ofrecer una documentación legible y eficiente de nuestra API, además de ofrecer características como permitir probar nuestros servicios directamente en el recurso generado por el bundle. Para la instalación corremos la siguiente línea desde nuestra consola dentro de la carpeta backend.

[prism:php] composer require nelmio/api-doc-bundle [/prism:php]

Una vez instalado nuestro Bundle de documentación el siguiente paso es cargar las rutas del mismo, para eso editamos el archivo routing.yml ubicado en app/config:

[prism:php] NelmioApiDocBundle: resource: "@NelmioApiDocBundle/Resources/config/routing.yml" prefix: /api/doc [/prism:php]

Esto generará una página ubicada en la ruta api/doc con la información de nuestra API según las especificaciones que le demos para cada una de las rutas que vamos a implementar. Dichas especificaciones se hacen en anotaciones en nuestras acciones de la siguiente manera:

[prism:php] /**

  • @ApiDoc(
  • description=“Descripción de lo que la ruta va a hacer",
  • section=“Sección para agrupar funcionalidad",
  • statusCodes={
  • XXX=“Estados que devolverá nuestra ruta”
  • },
  • parameters={
  • {“name"="nombre del parámetro", “dataType"="tipo de dato del parámetro", "required"=true|false, “description"="Descripción del parámetro"}
  • }
  • ) */ public function testAction() {} [/prism:php]

JMSSerializerBundle

Este bundle nos va permitir extender la funcionalidad del serializer de symfony habilitando una fácil interfaz para la implementación de características como ocultar ciertos campos de un objeto al momento de ser serializado, así mismo, es implementado por FOSRestBundle para dar la respuesta adecuada a los diferentes formatos permitidos en las solicitudes de nuestra API, destacando que si el bundle esta habilitado el mismo será implementado preferencialmente por FOSRestBundle.

Generando nuestra entidad

Para generar la entidad utilizaremos el siguiente comando de doctrine:

[prism:php] php bin/console doctrine:generate:entity [/prism:php]

Seleccionamos el bundle que generamos y annotation para nuestro formato de configuración llamando a nuestra entidad Task. Creamos un campo de nombre content de tipo text y un campo datetime de nombre created_at. Seleccionamos la opciones por defecto que nos da el promt de symfony, lo que generará los siguientes directorios y clases en nuestro bundle:

Procedemos a modificar nuestro entity para demostrar la configuración de serialización con JMSSerializerBundle, indicar que nuestros campos son requeridos y asignar el valor por defecto de nuestro campo datetime:

[prism:php] <?php

namespace TaskBundle\Entity;

use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation\ExclusionPolicy; use JMS\Serializer\Annotation\Exclude;

/**

  • Task
  • @ORM\Table(name="task")
  • @ORM\Entity(repositoryClass="TaskBundle\Repository\TaskRepository")
  • @ExclusionPolicy("None") */ class Task { /**

    • @var int
    • @ORM\Column(name="id", type="integer")
    • @ORM\Id
    • @ORM\GeneratedValue(strategy="AUTO") */ private $id;

    /**

    • @var string
    • @ORM\Column(name="content", type="text")
    • @Assert\NotBlank() */ private $content;

    /**

    • @var \DateTime
    • @ORM\Column(name="created_at", type="datetime")
    • @Assert\NotBlank()
    • @Exclude */ private $createdAt;

    public function __construct() { $this->setCreatedAt(new \DateTime()); }

    /**

    • Get id
    • @return int */ public function getId() { return $this->id; }

    /**

    • Set content
    • @param string $content
    • @return Task */ public function setContent($content) { $this->content = $content;

      return $this; }

    /**

    • Get content
    • @return string */ public function getContent() { return $this->content; }

    /**

    • Set createdAt
    • @param \DateTime $createdAt
    • @return Task */ public function setCreatedAt($createdAt) { $this->createdAt = $createdAt;

      return $this; }

    /**

    • Get createdAt
    • @return \DateTime */ public function getCreatedAt() { return $this->createdAt; } }

[/prism:php]

En esta oportunidad importamos tres clases: ExclusionPolicy, Exclude y Constraints as Assert. ExclusionPolicy permite indicar al serializador como manejara la exclusión de campos de nuestra clase Task, que en este caso es ninguna para poder tener un poco mas de control. Exclude, es el encargo de indicar si el campo es excluido o no de la serialización, por último Assert, permite añadir validaciones a nuestros campos. Por último generamos el formulario para nuestra clase task con el siguiente comando, que generará el TaskType dentro de la carpeta Form de nuestro bundle:

[prism:php] doctrine:generate:form TaskBundle:Task [/prism:php]

Removemos el campo de fecha del builder y se deshabilita la protección csrf para no tener inconvenientes con nuestra RESTAPI de la siguiente manera:

[prism:php] <?php

namespace TaskBundle\Form;

use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType { /**

  • @param FormBuilderInterface $builder
  • @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('content') ; }

    /**

  • @param OptionsResolver $resolver */ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'TaskBundle\Entity\Task', 'csrf_protection' => false )); } } [/prism:php]

Generando nuestras rutas

La primera tarea es indicarle a symfony que nuestro controlador es de tipo Rest, para ello, modificamos routing.yml ubicado en app/config: cambiando el tipo annotation a rest:

[prism:php] task: resource: "@TaskBundle/Controller/" type: rest prefix: / [/prism:php]

El siguiente paso es definir nuestra primera ruta en el controlador generado por symfony por defecto al correr el comando de generar bundle. Procedemos a eliminar las importaciones de clases que tiene colocando las siguientes:

[prism:php] use FOS\RestBundle\Controller\FOSRestController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use TaskBundle\Entity\Task; use TaskBundle\Form\TaskType; use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\ApiDoc; [/prism:php]

Luego hacemos que la clase DefaultControler extienda de FOSRestController:

[prism:php] class DefaultController extends FOSRestController [/prism:php]

Procedemos a crear nuestra primera acción que la llamaremos getListAction(), tomando en cuenta que todas las acciones de un controlador en symfony deben terminar con el sufijo action:

[prism:php] /**

  • @Rest\Get("task")
  • @return \Symfony\Component\HttpFoundation\Response
  • @ApiDoc(
  • description="Return the task list.",
  • section="Task",
  • statusCodes={
  • 200="Returned when successful",
  • 500="Returned when a non expected error happened getting the list"
  • }
  • ) */ public function getListAction() { $tasks = $this->getDoctrine()->getRepository('TaskBundle:Task')->findAll();

    return $this->handleView($this->view(array('list' => $tasks)));

    } [/prism:php]

En este snippet creamos una ruta task que automáticamente FOSRestBundle le asignará un nombre (en este caso get_list) de método get y en el cual buscamos los items de nuestra lista utilizando el repositorio creado para la clase task retornando una vista de FOSRest con un objeto que contiene un atributo list conformado por la colección serializada devuelta por doctrine. Lo que sucede aquí es que utilizamos el método view de FOSRestController del cual nuestro DefaultController extiende para crear una respuesta adecuada y ser manejada por el método handleView el cual es el encargado de transformar nuestro response al formato del cual el request solicita, así no nos preocuparnos si un request solicita nuestra respuesta en formato XML o JSON, ya que FOSRest se encargara de traducir correctamente lo que nuestra acción está generando. Ahora crearemos una ruta para obtener un task por ID:

[prism:php] /**

  • @param Task $task
  • @Rest\Get("task/{id}")
  • @return \Symfony\Component\HttpFoundation\Response
  • @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
  • @ApiDoc(
  • description="Returns an item by it's id.",
  • section="Task",
  • statusCodes={
  • 200="Returned when an item was found",
  • 404="Returned when there is no item with the id passed"
  • },
  • parameters={
  • {"name"="id", "dataType"="integer", "required"=true, "description"="Task id"}
  • }
  • ) */ public function getAction(Task $task) { return $this->handleView($this->view(array('task' => $task))); } [/prism:php]

En este caso utilizamos el helper ParamConverter para que symfony convierta automáticamente el id esperado por parámetros definido en la ruta en un objeto de la clase Task, automáticamente si no se consigue un objeto con el ID pasado se devolverá un estatus 404 Not Found, de lo contrario retornara el objeto serializado. Para eliminar se utiliza la misma forma anterior, pero definimos la ruta como delete y hacemos el proceso de eliminar devolviendo un estado de 204:

[prism:php] /**

  • @Rest\Delete("task/{id}")
  • @param Task $task
  • @return \Symfony\Component\HttpFoundation\Response
  • @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
  • @ApiDoc(
  • description="Deletes an item by it's id.",
  • section="Task",
  • statusCodes={
  • 204="Returned when an item was deleted successfully",
  • 404="Returned when there is no item with the id passed"
  • },
  • parameters={
  • {"name"="id", "dataType"="integer", "required"=true, "description"="Item id"}
  • }
  • ) */ public function deleteAction(Task $task) { $em = $this->getDoctrine()->getManager(); $em->remove($task); $em->flush(); return $this->handleView($this->view(null, 204)); } [/prism:php]

La creación y la edición la dejamos de ultimo para introducir un método bastante importante en nuestro CRUD que es el processForm encargado de utilizar el componente de formularios de symfony para serializar las nuevas entradas y ediciones en base de datos. Agregamos la siguiente función en nuestro controlador:

[prism:php] /**

  • Validates and saves the task
  • @param Task $task
  • @param Request $request
  • @param bool $new is new object
  • @return \Symfony\Component\HttpFoundation\Response */ private function processForm(Task $task, Request $request, $new = false) { $statusCode = $new ? 201 : 204; $form = $this->createForm(TaskType::class, $task); $data = $request->request->all(); $children = $form->all(); $toBind = array_intersect_key($data, $children); $form->submit($toBind); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($task); $em->flush(); return $this->handleView($this->view($new ? $task : null, $statusCode)); } return $this->handleView($this->view($form, 400)); } [/prism:php]

Lo primero que hace la función es validar es si el objeto que se le está pasando es nuevo, si es así, generará un estado de respuesta 201, de lo contrario 204. Luego procede a crear el formulario con el tipo de nuestra clase task, haciendo, un submit del objeto que viene del request independientemente del formato que venga, y si es válida persiste el objeto en la base de datos devolviendo una respuesta adecuada, si no es válido devuelve una plantilla de nuestro formulario con el código 400 Bad Request indicando que ha ocurrido un error. Ahora el siguiente paso es generar nuestras acciones para crear y actualizar nuestros recursos. Para crear solo tenemos que pasar un objeto vacío de nuestra clase task al método processForm indicando que es un objeto nuevo:

[prism:php] /**

  • Creates
  • @Rest\Post("task")
  • @param Request $request
  • @return \Symfony\Component\HttpFoundation\Response
  • @ApiDoc(
  • description="Creates an item.",
  • section="Task",
  • statusCodes={
  • 201="Returned when an item was created successfully",
  • 400="Returned when there is invalid data sent"
  • },
  • parameters={
  • {"name"="content", "dataType"="string", "required"=true, "description"="The new element content"}
  • }
  • ) */ public function postAction(Request $request) { return $this->processForm(new Task(), $request, true); } [/prism:php]

Para editar, utilizamos la misma metodología que con el método get/{id} y delete/{id} pasando un objeto inyectado a nuestra acción por symfony a processForm junto con el request:

[prism:php] /**

  • @Rest\Put("task/{id}")
  • @param Task $task
  • @param Request $request
  • @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
  • @return \Symfony\Component\HttpFoundation\Response
  • @ApiDoc(
  • description="Updates an item.",
  • section="Task",
  • statusCodes={
  • 204="Returned when an item was updated successfully",
  • 400="Returned when there is invalid data sent",
  • 404="Returned when there is no item with the id passed",
  • },
  • parameters={
  • {"name"="id", "dataType"="integer", "required"=true, "description"="Item id"},
  • {"name"="content", "dataType"="string", "required"=true, "description"="The new element content"}
  • }
  • ) */ public function putAction(Task $task, Request $request) { return $this->processForm($task, $request); } [/prism:php]

Con esto completamos nuestro backend, por lo que procederemos a probar nuestros endpoints: En formato json: En formato xml:

Revisando el API doc

Bueno, ya nuestro backend está listo obteniendo como resultado unos servicios sencillos pero que cubre la mayoría de los aspectos del trabajo de restAPI con symfony. Espero que este tutorial les sirva de ayuda y base, la próxima parte de esta serie configuraremos y generaremos los servicios para comunicarnos desde angular con nuestra API.