Symfony y GraphQL: Mutations

Gerardo Fernández
Jun 24 · 7 min read

Tras el primer artículo sobre Symfony y GraphQL donde vimos la forma en que podíamos crear nuestras primeras queries, he preparado la segunda parte en donde explicaré la forma de trabajar con mutations, es decir, con las llamadas que nos van a permitir crear y actualizar entidades.

Recordad que la integración con GraphQL la estamos llevando a cabo por medio del bundle overblog/graphql-bundle, con el cual estoy bastante contento en lo que a funcionalidad y personalización se refiere, aunque creo que la documentación no es todo lo completa que debería.

Dicho esto, vamos a ver cómo crear mutations y, como bola la extra, la manera en que podemos integrar el componente Validator de Symfony en el proceso el fin de validar (valga la redundancia) la creación y actualización de las entidades del proyecto.

Paso 0. Definiendo la entidad ContactForm y su tipo en GraphQL

Lo primero de todo vamos a definir la entidad sobre la que queremos realizar una mutation. Para este artículo, he escogido la entidad que me sirve para representar formularios de contacto la cual tiene el siguiente aspecto:

Como veis, además de declarar el mapping con Doctrine, he añadido las anotaciones correspondientes a las aserciones que las propiedades de nuestro formulario de contacto a crear/actualizar deben cumplir (concretamente ni el nombre ni el email pueden estar vacíos y además el email debería ser válido).

Además, también añadiremos un nuevo type en GraphQL, para lo cual crearemos dentro de la carpeta config/graphql/types el archivo ContactForm.types.yaml donde definiremos el aspecto del tipo ContactForm :

Paso 1. Definiendo una mutation

Atendiendo a la documentación de GraphQL, una mutation queda definida como una operación que provoca cualquier tipo de cambio en el estado del servidor. Por tanto, del mismo modo que en las API Rest no empleamos llamadas GET para crear o actualizar entidades, en GraphQL esas operaciones las haremos a través de mutations.

Por ejemplo, una mutation que crease un formulario de contacto tendrá el siguiente aspecto:

mutation {
createContactForm(input: {name: "Gerardo Fernández", email: "info@mail.com", telephone: "1212121", message: "ola"}) {
id
}
}

En esa consulta, createContactForm es el nombre de la mutation que tendremos que definir en nuestro proyecto para que todo funcione. Así que comencemos.

Lo primero de todo será asegurarnos que en el archivo donde se encuentra la configuración del bundle OverblogGraphQL ( config/packages/graphql.yaml si lo instalasteis usando flex) tenemos la línea marcada en negrita:

overblog_graphql:
definitions:
schema:
query: Query
mutation: Mutation
mappings:
auto_discover: false
types:
-
type: yaml
dir: "%kernel.project_dir%/config/graphql/types"
suffix: ~

de cara a informar al bundle de que vamos a tener mutations.

A continuación, crearemos el archivo Mutation.types.yaml en la ruta config/graphql/types e introduciremos lo siguiente:

Al igual que sucede con el archivo Query.types.yaml esto habilitará el endpoint Mutation permitiéndonos definir las distintas mutations que empleará nuestra aplicación.

Estas mutations las definiremos dentro del campo fields y, puesto que queremos tener una mutation a través de la cual podamos crear formularios de contacto, añadiremos un nueva nueva clave llamada createContactForm con el siguiente contenido:

  • type: ContactForm , es decir, el tipo que devolverá esta mutation será un ContactForm
  • resolve: "@=mutation('create_contact_form', [args])" , lo que sirve para indicar que esta mutation resuelve al alias create_contact_form al que le pasará todos los argumentos recibidos en la llamada.
  • input: ContactFormInput , es decir, el tipo al que pertenecen los argumentos recibidos.

Puesto que todavía no hemos definido el tipo ContactFormInput lo añadiremos (recordad que el tipo ContactForm lo añadimos en el paso anterior) creando el archivo config/graphql/types/ContactFormInput.types.yaml :

Hecho esto, tan sólo nos queda crear la clase encargada de procesar la mutation que hemos definido y que irá asociada al alias create_contact_form . Esta clase la crearemos dentro de la carpeta src/GraphQL/Mutation :

  • Puesto que nuestra mutation la hemos definido empleando un alias, la clase CreateContactFormMutation implementará la interfaz AliasedInterface con el fin de vincular en el método getAliases el alias create_contact_form con el método create de la clase.
  • En el constructor inyectamos el servicio EntityManagerInterface para poder guardar los formularios.
  • El método create recibe como argumentos un objeto de la clase Argument del cual podremos extraer el arrayinput donde se encontrarán los valores que se hayan enviado a la mutation. Recordad que cuando definimos la mutation createContactForm dentro de args definimos un campo llamado input que iba a tener el tipo ContactFormInput . Es por esta razonque los valores enviados se encuentran dentro de la clave input .
  • Finalmente recorremos el array rawArgs['input'] y rellenamos el objeto de Doctrine ContactForm para posteriormente persistirlo.

Fácil, ¿verdad?

Ahora si todo ha ido bien podremos abrir /graphiql en nuestro navegador y lanzar la consulta:

mutation {
createContactForm(input: {name: "ola", email: "info", telephone: "1212121", message: "ola"}) {
id
}
}

Lo cual nos devolverá:

{
"data": {
"createContactForm": {
"id": 5
}
}
}

Paso 2. Validando mutations

Llegados a este punto cabe preguntarse el modo en que podemos validar nuestras entidades antes de persistirlas con el fin de devolver los errores adecuados cuando los campos enviados no sean válidos.

Para ello, nos valdremos del servicio Validator de Symfony con el fin de validar la entidad antes de persistirla en nuestro método create y lanzar una excepción en el caso de que los datos no sean correctos:

Los cambios introducidos son los siguientes:

  • En el constructor añadiremos ValidatorInterface con el fin de que Symfony inyecte el servicio Validator que nos permitirá validar las aserciones declaradas en nuestra entidad.
  • Antes de persistir el objeto contactForm usaremos el objeto $this->validator para validarlo, lo cual nos devolverá un array con los errores encontrados.
  • En el caso de que haya errores lanzaremos una excepción personalizada de la clase FormException .

La clase FormException tendrá el siguiente aspecto:

Como veis, esta excepción está implementando la interfaz ClientAware del bundle Overblog/GraphQL ya que si no, la excepción lanzada desde nuestra clase encargada de procesar la mutación se mostraría como un error 500 sin posibilidad de editar su contenido. Implementar esta interfaz es la forma que tenemos de decirle al bundle “oye, esta excepción es personalizada y el mensaje que vas a mostrar va a ser el que yo te diga”. (“Invalid data set” almacenado en la constante MESSAGE en vez del típico“Internal server error”).

Además, el constructor de esta excepción recibirá un array con los errores detectados por el validador ( $violations ) el cual queremos devolver al cliente que realizó la llamada para que sea capaz de mostrar los errores producidos en la interfaz gráfica.

Si ahora lanzásemos la siguiente query:

mutation {
createContactForm(input: {name: "ola", email: "i", telephone: "1212121", message: "ola"}) {
id
}
}

Lo que obtendríamos sería un error similar al siguiente:

{
"errors": [
{
"message": "Invalid data set",
"extensions": {
"category": "formException"
},
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createContactForm"
],
"trace": [
{
"file": "/Users/gerardofernandez/LatteAndCode/projects/
} ....
]
}
],
"data": {
"createContactForm": null
}
}

Es decir, veríamos el mensaje Invalid data set que definimos en nuestra excepción FormException pero no el array de errores indicándonos que no es un email válido.

Para conseguir esto será necesario que en algúm momento podamos añadir a ese json que representa el error el array $violations de nuestra excepción. Para ello, crearemos un EventListener con el siguiente código:

En el cual añadiremos al objeto $formattedError (de la clase ArrayObject , por cierto) dos nuevas claves:

  • state, que será un array donde las claves serán los campos y los valores el array de errores.
  • code , que será un array donde las claves serán los campos y los valores el identificador empleado por GraphQL para ese campo.

Ahora tan solo quedará añadir este EventListener a nuestro archivo services.yaml para que escuche el evento graphql.error_formatting :

services:  App\GraphQL\Formatter\FormExceptionFormatter:    tags:      - { name: kernel.event_listener, event: graphql.error_formatting, method: onErrorFormatting }

Hecho esto, si repetimos la consulta anterior (la que tenía un email inválido), obtendremos lo siguiente:

{
"errors": [
{
"message": "Invalid data set",
"extensions": {
"category": "formException"
},
"path": [
"createContactForm"
],
"state": {
"email": [
"The email '\"i\"' is not a valid email."
]
},
"code": {
"email": [
"bd79c0ab-ddba-46cc-a703-a7a4b08de310"
]
}
}
],
"data": {
"createContactForm": null
}
}

Por lo que ya podremos mostrar en nuestra interfaz gráfica los errores de los campos.

Y con esto termina la segunda parte de esta serie de artículos sobre GraphQL. ¡Espero que os haya servido!

Gerardo Fernández

Written by

Mejor amigo de Simba y desarrollador 100% fullstack