Validación y manejo de excepciones de la interfaz de usuario al backend

Tarde o temprano en su carrera de programación, se enfrentará con el dilema de la validación y el manejo de excepciones. Este fue el caso conmigo y con mi equipo también. Hace un par de años, llegamos a un punto en el que tuvimos que tomar medidas arquitectónicas para acomodar todos los casos excepcionales que nuestro gran proyecto de software necesitaba manejar. A continuación, se incluye una lista de prácticas que valoramos y aplicamos cuando se trata de la validación y el manejo de excepciones..


Validación vs. Manejo de Excepciones

Cuando empezamos a discutir nuestro problema, una cosa surgió muy rápidamente. ¿Qué es la validación y qué es el manejo de excepciones? Por ejemplo, en un formulario de registro de usuario, tenemos algunas reglas para la contraseña (debe contener tanto números como letras). Si el usuario solo ingresa letras, es un problema de validación o una excepción. ¿Debería la interfaz de usuario validar eso, o simplemente pasarlo al backend y detectar cualquier excepción que se lance??

Llegamos a una conclusión común de que la validación se refiere a las reglas definidas por el sistema y verificadas con los datos proporcionados por el usuario. Una validación no debe preocuparse por cómo funciona la lógica de negocios o cómo funciona el sistema. Por ejemplo, nuestro sistema operativo puede esperar, sin ninguna protesta, una contraseña compuesta de letras simples. Sin embargo, queremos imponer una combinación de letras y números. Este es un caso de validación, una regla que queremos imponer..

Por otro lado, las excepciones son casos en los que nuestro sistema puede funcionar de forma impredecible, erróneamente o en absoluto si se proporcionan datos específicos en un formato incorrecto. Por ejemplo, en el ejemplo anterior, si el nombre de usuario ya existe en el sistema, es un caso de excepción. Nuestra lógica de negocios debería poder lanzar la excepción apropiada y la UI capturarla y manejarla para que el usuario vea un mensaje agradable.


Validación en la interfaz de usuario

Ahora que dejamos en claro cuáles son nuestros objetivos, veamos algunos ejemplos basados ​​en la misma idea de formulario de registro de usuario..

Validando en JavaScript

Para la mayoría de los navegadores de hoy, JavaScript es una segunda naturaleza. Casi no hay una página web sin algún grado de JavaScript en ella. Una buena práctica es validar algunas cosas básicas en JavaScript.

Digamos que tenemos un simple formulario de registro de usuario en index.php, como se describe abajo.

    registro de usuario    

Registrar una cuenta nueva

Nombre de usuario:

Contraseña:

Confirmar:

Esto producirá algo similar a la imagen de abajo:


Cada formulario debe validar que el texto ingresado en los dos campos de contraseña sea igual. Obviamente, esto es para garantizar que el usuario no cometa un error al escribir su contraseña. Con JavaScript, hacer la validación es bastante simple..

Primero necesitamos actualizar un poco de nuestro código HTML..

 
Nombre de usuario:

Contraseña:

Confirmar:

Añadimos nombres a los campos de entrada de contraseña para que podamos identificarlos. Luego especificamos que al enviar el formulario se debe devolver el resultado de una función llamada validatePasswords (). Esta función es el JavaScript que escribiremos. Guiones simples como este pueden guardarse en el archivo HTML, otros más sofisticados deben ir en sus propios archivos JavaScript.

 

Lo único que hacemos aquí es comparar los valores de los dos campos de entrada llamados "contraseña"y"confirmar". Podemos hacer referencia al formulario por el parámetro que enviamos al llamar a la función. Usamos"esta"en el formulario enviar atributo, por lo que el formulario en sí se envía a la función.

Cuando los valores son los mismos., cierto se devolverá y se enviará el formulario; de lo contrario, se mostrará un mensaje de alerta que indica al usuario que las contraseñas no coinciden.


Validaciones HTML5

Si bien podemos usar JavaScript para validar la mayoría de nuestras entradas, hay casos en los que queremos ir por un camino más fácil. Cierto grado de validación de entrada está disponible en HTML5, y la mayoría de los navegadores están felices de aplicarlos. El uso de la validación HTML5 es más simple en algunos casos, aunque ofrece menos flexibilidad.

  registro de usuario     

Registrar una cuenta nueva

Nombre de usuario:

Contraseña:

Confirmar:

Dirección de correo electrónico:

Sitio web:

Para demostrar varios casos de validación, extendimos un poco nuestro formulario. Hemos añadido una dirección de correo electrónico y un sitio web también. Las validaciones de HTML se establecieron en tres campos.

  • La entrada de texto nombre de usuario es simplemente necesario. Se validará con cualquier cadena más larga que cero caracteres..
  • El campo de la dirección de correo electrónico es de tipo "correo electrónico"y cuando especificamos el"necesario"atributo, los navegadores aplicarán una validación al campo.
  • Finalmente, el campo del sitio web es de tipo "url". También especificamos una"modelo"atributo donde puede escribir sus expresiones regulares que validan los campos requeridos.

Para que el usuario esté al tanto del estado de los campos, también usamos un poco de CSS para colorear los bordes de las entradas en rojo o verde, dependiendo del estado de la validación requerida.


El problema con las validaciones de HTML es que los diferentes navegadores se comportan de manera diferente cuando intenta enviar el formulario. Algunos navegadores solo aplicarán el CSS para informar a los usuarios, otros impedirán el envío del formulario por completo. Le recomiendo que pruebe sus validaciones HTML a fondo en diferentes navegadores y, si es necesario, también proporcione un respaldo de JavaScript para aquellos navegadores que no son lo suficientemente inteligentes..


Validando en Modelos

Por ahora mucha gente conoce la propuesta de arquitectura limpia de Robert C. Martin, en la que el marco MVC es solo para presentación y no para lógica empresarial..


Esencialmente, la lógica de su negocio debe residir en un lugar separado y bien aislado, organizado para reflejar la arquitectura de su aplicación, mientras que las vistas y los controladores del marco deben controlar la entrega del contenido al usuario y los modelos podrían eliminarse por completo o, si fuera necesario. , utilizado solo para realizar operaciones de entrega relacionadas. Una de esas operaciones es la validación. La mayoría de los marcos tienen excelentes características de validación. Sería una pena no poner a trabajar a tus modelos y hacer una pequeña validación allí..

No instalaremos varios marcos web de MVC para demostrar cómo validar nuestros formularios anteriores, pero aquí hay dos soluciones aproximadas en Laravel y CakePHP..

Validación en un modelo de Laravel

Laravel está diseñado para que tenga más acceso a la validación en el Controlador, donde también tiene acceso directo a la entrada del usuario. El tipo de validador incorporado prefiere ser utilizado allí. Sin embargo, hay sugerencias en Internet de que la validación en modelos sigue siendo algo bueno en Laravel. Puede encontrar un ejemplo completo y una solución de Jeffrey Way en su repositorio de Github.

Si prefiere escribir su propia solución, podría hacer algo similar al modelo a continuación..

la clase UserACL extiende Eloquent private $ rules = array ('userName' => 'required | alpha | min: 5', 'password' => 'required | min: 6', 'confirm' => 'required | min: 6 ',' email '=>' required | email ',' website '=>' url '); $ errores privados; función pública validate ($ data) $ validator = Validator :: make ($ data, $ this-> rules); si ($ validador-> falla ()) $ esto-> errores = $ validador-> errores; falso retorno;  devuelve true;  errores de función pública () devolver $ this-> errores; 

Puede usar esto desde su controlador simplemente creando el UsuarioACL objeto y llamada validar en él. Probablemente tendrás el "registro"método también en este modelo, y la registro simplemente delegará los datos ya validados a su lógica empresarial.

Validación en un modelo de CakePHP

CakePHP promueve la validación en modelos también. Tiene una amplia funcionalidad de validación a nivel de modelo. Aquí se explica cómo se vería una validación para nuestro formulario en CakePHP.

la clase UserACL extiende AppModel public $ validate = ['userName' => ['rule' => ['minLength', 5], 'required' => true, 'allowEmpty' => false, 'on' => 'create ',' mensaje '=>' El nombre de usuario debe tener al menos 5 caracteres. ' ], 'contraseña' => ['regla' => ['es igual a', 'confirmar'], 'mensaje' => 'Las dos contraseñas no coinciden. Por favor, vuelva a introducirlos. ]]; la función pública es igual a To ($ checkedField, $ otherField = null) $ value = $ this-> getFieldValue ($ checkedField); devuelve $ value === $ this-> data [$ this-> name] [$ otherField];  la función privada getFieldValue ($ fieldName) return array_values ​​($ otherField) [0]; 

Solo ejemplificamos las reglas parcialmente. Basta con resaltar el poder de validación en el modelo. CakePHP es particularmente bueno en esto. Tiene un gran número de funciones de validación integradas como "longitud mínima"En el ejemplo y varias formas de proporcionar comentarios al usuario. Más aún, conceptos como"necesario"o"allowEmpty"en realidad no son reglas de validación. Cake las verá cuando genere su vista y coloque validaciones HTML también en los campos marcados con estos parámetros. Sin embargo, las reglas son excelentes y se pueden extender fácilmente simplemente creando métodos en la clase modelo como lo hicimos para compare los dos campos de contraseña. Por último, siempre puede especificar el mensaje que desea enviar a las vistas en caso de error de validación. Más información sobre la validación de CakePHP en el libro de recetas.

La validación en general a nivel de modelo tiene sus ventajas. Cada marco proporciona un acceso fácil a los campos de entrada y crea el mecanismo para notificar al usuario en caso de error de validación. No hay necesidad de declaraciones de prueba de captura o cualquier otro paso sofisticado. La validación en el lado del servidor también asegura que los datos se validen, sin importar qué. El usuario no puede engañar más a nuestro software como con HTML o JavaScript. Por supuesto, cada validación del lado del servidor viene con el costo de un viaje de ida y vuelta de la red y la potencia de cómputo del lado del proveedor en lugar del lado del cliente.


Lanzar excepciones de la lógica de negocios

El último paso para verificar los datos antes de enviarlos al sistema se encuentra en el nivel de nuestra lógica empresarial. La información que llega a esta parte del sistema se debe desinfectar lo suficiente como para poder utilizarla. La lógica de negocios solo debe verificar los casos que son críticos para ella. Por ejemplo, agregar un usuario que ya existe es un caso cuando lanzamos una excepción. Comprobar que la longitud del usuario sea de al menos cinco caracteres no debería ocurrir en este nivel. Podemos asumir con seguridad que tales limitaciones se aplicaron en niveles más altos.

Por otro lado, comparar las dos contraseñas es un tema de discusión. Por ejemplo, si simplemente ciframos y guardamos la contraseña cerca del usuario en una base de datos, podríamos eliminar la comprobación y asumir que las capas anteriores se aseguraron de que las contraseñas sean iguales. Sin embargo, si creamos un usuario real en el sistema operativo utilizando una API o una herramienta CLI que realmente requiere un nombre de usuario, contraseña y confirmación de contraseña, es posible que también deseamos tomar la segunda entrada y enviarla a una herramienta CLI. Deje que vuelva a validarse si las contraseñas coinciden y esté preparado para lanzar una excepción si no lo hacen. De esta manera, modelamos nuestra lógica de negocios para que coincida con el comportamiento del sistema operativo real..

Lanzar excepciones desde PHP

Lanzar excepciones de PHP es muy fácil. Creemos nuestra clase de control de acceso de usuario y demostremos cómo implementar una funcionalidad de adición de usuario.

la clase UserControlTest extiende PHPUnit_Framework_TestCase function testBehavior () $ this-> assertTrue (true); 

Siempre me gusta comenzar con algo simple que me ponga en marcha. Crear una prueba estúpida es una gran manera de hacerlo. También me obliga a pensar en lo que quiero implementar. Una prueba llamada UserControlTest significa que pensé que voy a necesitar una Control de usuario clase para implementar mi método.

require_once __DIR__. '/… /UserControl.php'; la clase UserControlTest extiende PHPUnit_Framework_TestCase / ** * @expectedException Exception * @expectedExceptionMessage El usuario no puede estar vacío * / function testEmptyUsernameWillThrowException () $ userControl = nuevo UserControl (); $ userControl-> add (");

La siguiente prueba para escribir es un caso degenerativo. No probaremos la longitud de un usuario específico, pero queremos asegurarnos de que no queremos agregar un usuario vacío. A veces es fácil perder el contenido de una variable de la vista a la empresa, en todas las capas de nuestra aplicación. Este código obviamente fallará, porque todavía no tenemos una clase.

Advertencia de PHP: require_once ([long-path-here] / Test /… /UserControl.php): no se pudo abrir la secuencia: No hay tal archivo o directorio en [long-path-here] /Test/UserControlTest.php en la línea 2

Vamos a crear la clase y ejecutar nuestras pruebas. Ahora tenemos otro problema.

Error grave de PHP: llamada a un método indefinido UserControl :: add ()

Pero podemos arreglar eso, también, en solo un par de segundos..

clase UserControl función pública agregar ($ nombre de usuario) 

Ahora podemos tener un buen fracaso en la prueba que nos cuenta la historia completa de nuestro código.

1) UserControlTest :: testEmptyUsernameWillThrowException Error al afirmar que se lanza la excepción de tipo "Exception".

Finalmente podemos hacer algunos códigos reales..

función pública agregar ($ nombre de usuario) si (! $ nombre de usuario) lanzar una nueva excepción (); 

Eso hace que la expectativa para la excepción pase, pero sin especificar un mensaje, la prueba seguirá fallando.

1) UserControlTest :: testEmptyUsernameWillThrowException Falló al afirmar que el mensaje de excepción "contiene 'El usuario no puede estar vacío'.

Hora de escribir el mensaje de la excepción.

función pública agregar ($ nombre de usuario) si (! $ nombre de usuario) lanzar una nueva excepción ('El usuario no puede estar vacío!'); 

Ahora, eso hace que nuestra prueba pase. Como puede observar, PHPUnit verifica que el mensaje de excepción esperado esté contenido en la excepción lanzada realmente. Esto es útil porque nos permite construir mensajes dinámicamente y solo verificar la parte estable. Un ejemplo común es cuando lanza un error con un texto base y al final especifica el motivo de esa excepción. Las razones son usualmente proporcionadas por bibliotecas o aplicaciones de terceros.

/ ** * @expectedException Exception * @expectedExceptionMessage No se puede agregar el usuario George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> once () -> with ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('El usuario ya existe en el sistema.'); $ userControl = new UserControl ($ command); $ userControl-> add ('George'); 

Lanzar errores a usuarios duplicados nos permitirá explorar la construcción de este mensaje un paso más allá. La prueba anterior crea un simulacro que simulará un comando del sistema, fallará y, a petición, devolverá un mensaje de error agradable. Inyectaremos este comando a la Control de usuario clase para uso interno.

clase UserControl private $ systemCommand; función pública __construct (SystemCommand $ systemCommand = null) $ this-> systemCommand = $ systemCommand? : nuevo SystemCommand ();  función pública agregar ($ nombre de usuario) si (! $ nombre de usuario) lanzar una nueva excepción ('El usuario no puede estar vacío!');  clase SystemCommand 

Inyectando el a SystemCommand La instancia fue bastante fácil. También creamos un SystemCommand Clase dentro de nuestra prueba solo para evitar problemas de sintaxis. No lo implementaremos. Su alcance excede el tema de este tutorial. Sin embargo, tenemos otro mensaje de error de prueba.

1) UserControlTest :: testWillNotAddAnAlreadyExistingUser Error al afirmar que se lanza la excepción de tipo "Exception".

Sí. No estamos lanzando ninguna excepción. Falta la lógica para llamar al comando del sistema e intentar agregar el usuario.

función pública agregar ($ nombre de usuario) si (! $ nombre de usuario) lanzar una nueva excepción ('El usuario no puede estar vacío!');  if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) throw new Exception (sprintf ('No se puede agregar el usuario% s. Motivo:% s', $ username, $ this-> systemCommand-> getFailureMessage ())); 

Ahora, esas modificaciones a la añadir() El método puede hacer el truco. Intentamos ejecutar nuestro comando en el sistema, sin importar qué, y si el sistema dice que no puede agregar al usuario por cualquier razón, lanzamos una excepción. El mensaje de esta excepción estará parcialmente codificado, con el nombre del usuario adjunto y luego la razón del comando del sistema concatenado al final. Como puedes ver, este código hace que nuestra prueba pase.

Excepciones personalizadas

Lanzar excepciones con diferentes mensajes es suficiente en la mayoría de los casos. Sin embargo, cuando tiene un sistema más complejo, también necesita capturar estas excepciones y realizar diferentes acciones basadas en ellas. Analizar el mensaje de una excepción y tomar medidas únicamente sobre eso puede llevar a algunos problemas molestos. Primero, las cadenas son parte de la interfaz de usuario, la presentación y tienen una naturaleza volátil. Basar la lógica en cadenas siempre cambiantes llevará a la pesadilla de la administración de dependencias. Segundo, llamando a getMessage () El método de la excepción capturada cada vez es también una forma extraña de decidir qué hacer a continuación..

Con todo esto en mente, crear nuestras propias excepciones es el siguiente paso lógico que se debe tomar.

/ ** * @expectedException ExceptionCannotAddUser * @expectedExceptionMessage No se puede agregar el usuario George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> once () -> with ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('El usuario ya existe en el sistema.'); $ userControl = new UserControl ($ command); $ userControl-> add ('George'); 

Modificamos nuestra prueba para esperar nuestra propia excepción personalizada., ExceptionCannotAddUser. El resto de la prueba no ha cambiado..

la clase ExceptionCannotAddUser extiende Exception public function __construct ($ userName, $ reason) $ message = sprintf ('No se puede agregar el usuario% s. Reason:% s', $ userName, $ reason); parent :: __ construct ($ message, 13, null); 

La clase que implementa nuestra excepción personalizada es como cualquier otra clase, pero tiene que extenderse Excepción. El uso de excepciones personalizadas también nos proporciona un excelente lugar para realizar toda la manipulación de cadenas relacionada con la presentación. Moviendo la concatenación aquí, también eliminamos la presentación de la lógica empresarial y respetamos el principio de responsabilidad única.

función pública agregar ($ nombre de usuario) si (! $ nombre de usuario) lanzar una nueva excepción ('El usuario no puede estar vacío!');  if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) lanza el nuevo ExceptionCannotAddUser ($ username, $ this-> systemCommand-> getFailureMessage ()); 

Lanzar nuestra propia excepción es solo cuestión de cambiar lo viejo "lanzar"orden al nuevo y enviar dos parámetros en lugar de redactar el mensaje aquí. Por supuesto, todas las pruebas están pasando.

PHPUnit 3.7.28 por Sebastian Bergmann ... Tiempo: 18 ms, Memoria: 3.00Mb OK (2 pruebas, 4 aserciones) Hecho.

Atrapando excepciones en tu MVC

Las excepciones deben detectarse en algún momento, a menos que desee que su usuario las vea como son. Si está utilizando un marco MVC, es probable que desee detectar excepciones en el controlador o modelo. Una vez que se captura la excepción, se transforma en un mensaje para el usuario y se procesa dentro de su vista. Una forma común de lograr esto es crear un "tryAction ($ action)"en el controlador o modelo base de su aplicación y siempre llámelo con la acción actual. En ese método, puede hacer la lógica de captura y la generación de mensajes agradables para adaptarse a su marco.

Si no utiliza un marco web, o una interfaz web, la capa de presentación debería ocuparse de capturar y transformar estas excepciones..

Si desarrolla una biblioteca, la captura de sus excepciones será responsabilidad de sus clientes.


Pensamientos finales

Eso es. Atravesamos todas las capas de nuestra aplicación. Validamos en JavaScript, HTML y en nuestros modelos. Hemos generado y capturado excepciones de nuestra lógica empresarial e incluso hemos creado nuestras propias excepciones personalizadas. Este enfoque de validación y manejo de excepciones se puede aplicar desde proyectos pequeños a grandes sin ningún problema grave. Sin embargo, si su lógica de validación se está volviendo muy compleja, y diferentes partes de su proyecto utilizan partes de lógica superpuestas, puede considerar extraer todas las validaciones que se pueden realizar a un nivel específico a un servicio de validación o proveedor de validación. Estos niveles pueden incluir, pero no es necesario que estén limitados a un validador de JavaScript, un validador de PHP backend, un validador de comunicación de terceros, etc..

Gracias por leer. Que tengas un buen día.