Una propuesta para validar formularios y modelos de forma conjunta en Laravel.
Hace unos días, dándole vueltas al código de un proyecto mientras hacía algunos tests me di cuenta que había cosas que no estaba haciendo del todo bien o al menos las estaba haciendo de forma incompleta.
Problema.
Todo tenía que ver con la validación de los datos que estaba haciendo. En mis aplicaciones, de modo general, la entrada de datos a las aplicaciones se realiza mediante un formulario. La validación de los datos de entrada de estos formularios los hacía, como se suelen hacer en Laravel, mediante una clase FormRequest que inyectada en un método de un controller ‘ella sola’ hace que si no se cumplen las validaciones, se retorne a la petición anterior y se muestren los mensajes de error en el formulario…etc. Estas validaciones pueden ser de cualquier tipo y lo complejas que uno quiera o deba hacerlas para cumplir con los requisitos del negocio. Hasta aquí bien.
Según fuera el caso, y simplificando, una vez realizadas estas validaciones de formulario realizaba operaciones con modelos, es decir, con la base de datos. Inserciones, actualizaciones, borrados. ¿Es esto correcto? Si. ¿Es completamente correcto? Creo que no, y me explico.
Propuesta de solución.
Considero que si queremos mantener la consistencia de todos nuestros datos almacenados estos deben ser validados antes de realizar cualquier operación con la base de datos. Una cosa es validar un formulario y otra cosa validar el modelo. Puede ser que ambas validaciones sean 1:1, es decir, que coincidan…o puede ser que no. Puede ser que al enviar un formulario todos sus campos coincidan con los de la tabla que queremos actualizar o como digo, puede ser que no. O incluso el proceso generado al enviar un formulario actualice no una sino varias tablas en la base de datos. Por lo tanto, tenemos ‘información extra’ respecto de la enviada en el formulario que no ha sido validada al procesar dicho formulario y que por lo tanto deberíamos validar igualmente para, como digo, mantener la consistencia de todos los datos.
Además, podría ocurrir que la actualización de un modelo no tuviera que venir necesariamente por formulario. Pongámonos en el caso de que la información viniera a través de un llamada de un API, que ocurriera desde un proceso de sistema por CLI o desde la importación de un archivo. En este caso no tendríamos la validación del formulario pero deberíamos igualmente tener la validación de los datos de entrada. En definitiva, hay que validar.
Código (SHOW ME THE CODE)
Disclaimer: Vaya por delante que es una propuesta personal. Una primera aproximación la cual está completamente abierta a modificaciones, sugerencias, correcciones etc. De hecho, al final expongo alguna posible mejora.
En GitHub he creado un proyecto llamado Blackboard (pizarra en inglés). En dicho proyecto iré añadiendo código de problemas, soluciones, propuestas y mejoras de código o implementaciones con las que me voy encontrando en mi día a día desarrollando con Laravel u otras tecnologías, con el objetivo de mostrarlas y ojalá recibir feedback sobre como mejorar lo que haga.
De momento he añadido un ejemplo muy simple para mostrar la problemática expuesta y como lo he resuelto reutilizando el código de validaciones por formRequest y validaciones de la lógica de negocio en el modelo. Dicho ejemplo está basado en la creación de la inserción de un post con su título y contenido asociado al usuario logueado.
En primer lugar, echemos un vistazo al método del controller que va a recibir la petición del formulario. Aquí lo interesante y conocido es el parámetro PostRequest que tendrá la validación de los campos de dicho formulario. Posteriormente se prepara el command y el command handler que realizan el proceso. No nos ocuparemos de momento de las capturas de las excepciones.
El código interesante del handler CreatePost es el método execute que es el que coordina el proceso de creación del post. Se ‘reconfigura’ el command añadiendo el usuario logueado, valida el command y crea el modelo en el repositorio. Aquí lo interesante está en la implementación del método validateCommand() que validará los datos del command vengan estos de un formulario, de un API, de un fichero…etc. Van a estar validados si o si.
Fijémonos ahora en la parte de la validación. Para dicha validación tengo una clase base de la cual extenderán el resto de clases ‘validadoras’ específicas y la cual será la que contendrá el método validate() que lógicamente realizará la validación de las reglas que hayamos configurado en el atributo $rules de cada clase hija.
Y ahora la clase específica de validación de creación de un post que va a validar el contenido antes de pasarlo al modelo. En esta clase definimos las reglas de los campos. Estas reglas, como veremos a continuación, serán las utilizadas además en el PostRequest de manera que conseguimos tenerlas en un único lugar sin repetirlas.
Es aquí dónde está la clave de todo. Como podemos ver el método validateCommand lo primero que hace es validar contra las reglas indicadas en el atributo $rules (usadas también en el PostRequest) y posteriormente valida una regla de negocio específica que es que al menos tengamos el usuario dueño del post también especificado. Es decir, venga de dónde venga el command y configure quién lo configure lo vamos a validar contras las reglas usadas para el FormRequest y reglas específicas del negocio.
Por último, la implementación del PostRequest. Aquí vemos que lo interesante es la reutilización de las reglas definidas en la clase anterior de validación del formulario.
Resumen
¿Qué he conseguido en esta aproximación? Lo principal ha sido validar los datos antes de actualizar un modelo reutilizando las reglas definidas para validar formularios añadiendo reglas específicas del negocio no definidas en las reglas del formulario.
Mejoras
Así de primeras se me ocurre una. Podría quitar el PostRequest del método createPost del controller.
public function createPost(PostRequest $request)
¿Por qué quitarlo? Pues porque en realidad esas validaciones ya las estoy haciendo al validar el comando.
¿Por qué NO quitarlo? Supongamos el caso en el que método createPost fuera más largo o el proceso hasta llegar a la validación del command fuera complejo con muchas consultas a la base de datos, apis de terceros…etc. Si dejamos el PostRequest y tuviéramos algún error de validación esto nos serviría para ahorrarnos dicho proceso posterior que igualmente no se completaría al haber un error en la validación del command.
Otra soluciones o ayudas
Buscando información sobre todo esto, encontré un componente para Laravel que facilita la validación de los datos antes de cualquier operación en el modelo. Me ha parecido muy interesante y en cuánto tenga tiempo le echaré un vistazo. En mi caso este componente serviría para configurar lo que yo tengo implementado en estas líneas:
// Business logic validation
if (is_null($command->userId)) {
throw new CustomValidationException ('User is required');
}
Os dejo el enlace.
https://github.com/dwightwatson/validating
Como dije al principio, cualquier comentario, sugerencia, corrección…etc será bienvenida. Por favor, ¡guiadme! :p
Chimpún.