1. Code
  2. PHP

Manejo de validaciones y excepciones: Desde la interfaz de usuario hasta el backend

Scroll to top
16 min read

Spanish (Español) translation by steven (you can also view the original English article)

Tarde o temprano en tu carrera de programación, te enfrentarás al dilema de la validación y el manejo de excepciones. Este fue mi caso y el de mi equipo también. Hace un par de años llegamos a un punto en el que tuvimos que tomar acciones de arquitectura para dar cabida a todos los casos excepcionales que nuestro proyecto de software bastante grande necesitaba manejar. A continuación se muestra una lista de prácticas que llegamos a valorar y aplicar cuando se trata de validación y manejo de excepciones.


Validación contra el manejo de excepciones

Cuando comenzamos 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 ingresa solo 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 pueda arrojar?

Llegamos a la 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 debería preocuparse por cómo funciona la lógica del negocio o cómo funciona el sistema. Por ejemplo, nuestro sistema operativo puede esperar, sin protestas, 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 manera impredecible, incorrectamente o no funcionar en absoluto si algunos datos específicos se proporcionan en un formato incorrecto. Por ejemplo, en el ejemplo anterior, si el nombre de usuario ya existe en el sistema, se trata de una excepción. Nuestra lógica de negocio debería poder lanzar la excepción adecuada y la interfaz de usuario la detecta y la maneja 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 del formulario de registro de usuario.

Validando en JavaScript

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

Digamos que tenemos un formulario simple de registro de usuario en index.php, como se describe a continuación.

1
<!DOCTYPE html>
2
<html>
3
	<head>
4
		<title>User Registration</title>
5
		<meta charset="UTF-8">
6
	</head>
7
	<body>
8
		<h3>Register new account</h3>
9
		<form>
10
			Username:
11
			<br/>
12
			<input type="text" />
13
			<br/>
14
			Password:
15
			<br/>
16
			<input type="password" />
17
			<br/>
18
			Confirm:
19
			<br/>
20
			<input type="password" />
21
			<br/>
22
			<input type="submit" name="register" value="Register">
23
		</form>
24
	</body>
25
</html>

Esto generará algo similar a la imagen a continuación:

RegistrationForm

Cada uno de estos formularios debe validar que el texto ingresado en los dos campos de contraseña es igual. Obviamente, esto es para asegurar que el usuario no cometa un error al escribir su contraseña. Con JavaScript, realizar la validación es bastante sencillo.

Primero necesitamos actualizar un poco nuestro código HTML.

1
<form onsubmit="return validatePasswords(this);">
2
	Username:
3
	<br/>
4
	<input type="text" />
5
	<br/>
6
	Password:
7
	<br/>
8
	<input type="password" name="password"/>
9
	<br/>
10
	Confirm:
11
	<br/>
12
	<input type="password" name="confirm"/>
13
	<br/>
14
	<input type="submit" name="register" value="Register">
15
</form>

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

1
<script>
2
	function validatePasswords(form) {
3
		if (form.password.value !== form.confirm.value) {
4
			alert("Passwords do not match");
5
			return false;
6
		}
7
		return true;
8
	}
9
10
</script>

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

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

PasswordDoNotMatchAlertPasswordDoNotMatchAlertPasswordDoNotMatchAlert

Validando con 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. Algún grado de validación de entrada está disponible en HTML5, y la mayoría de los navegadores están felices de aplicarlo. El uso de la validación de HTML5 es más sencillo en algunos casos, aunque ofrece menos flexibilidad.

1
<head>
2
	<title>User Registration</title>
3
	<meta charset="UTF-8">
4
	<style>
5
		input {
6
			width: 200px;
7
		}
8
		input:required:valid {
9
			border-color: mediumspringgreen;
10
		}
11
		input:required:invalid {
12
			border-color: lightcoral;
13
		}
14
	</style>
15
</head>
16
<body>
17
	<h3>Register new account</h3>
18
	<form onsubmit="return validatePasswords(this);">
19
		Username:
20
		<br/>
21
		<input type="text" name="userName" required/>
22
		<br/>
23
		Password:
24
		<br/>
25
		<input type="password" name="password"/>
26
		<br/>
27
		Confirm:
28
		<br/>
29
		<input type="password" name="confirm"/>
30
		<br/>
31
		Email Address:
32
		<br/>
33
		<input type="email" name="email" required placeholder="A Valid Email Address"/>
34
		<br/>
35
		Website:
36
		<br/>
37
		<input type="url" name="website" required pattern="https?://.+"/>
38
		<br/>
39
		<input type="submit" name="register" value="Register">
40
	</form>
41
</body>

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

  • El campo del nombre de usuario (username) es simplemente necesario. Se validará con cualquier cadena de más de cero caracteres.
  • El campo de la dirección de correo electrónico es de tipo "email" y cuando especificamos el atributo "required", los navegadores aplicarán una validación al campo.
  • Finalmente, el campo del sitio web es de tipo "url". También especificamos un atributo "pattern" donde puedes escribir tus expresiones regulares que validan los campos requeridos.

Para que el usuario conozca el 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.

HTMLValidations

El problema con las validaciones HTML es que los diferentes navegadores se comportan de manera diferente cuando intentas enviar el formulario. Algunos navegadores simplemente aplicarán el CSS para informar a los usuarios, otros evitarán el envío del formulario por completo. Te recomiendo que pruebes tus validaciones HTML a fondo en diferentes navegadores y, si es necesario, también proporciones un respaldo de JavaScript para aquellos navegadores que no son lo suficientemente inteligentes.


Validación en modelos

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

HighLevelDesignHighLevelDesignHighLevelDesign

Esencialmente, la lógica de tu negocio debe residir en un lugar separado, bien aislado, organizado para reflejar la arquitectura de tu aplicación, mientras que las vistas y los controladores del framework deben controlar la entrega del contenido al usuario y los modelos pueden descartarse por completo o, si es necesario, utilizado únicamente para realizar operaciones relacionadas con la entrega. Una de esas operaciones es la validación. La mayoría de los frameworks tienen excelentes funciones de validación. Sería una pena no poner tus modelos en funcionamiento y hacer una pequeña validación allí.

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

Validando en un modelo de Laravel

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

Si prefieres escribir tu propia solución, puedes hacer algo similar al modelo a continuación.

1
class UserACL extends Eloquent {
2
    private $rules = array(
3
        'userName' => 'required|alpha|min:5',
4
        'password'  => 'required|min:6',
5
		'confirm' => 'required|min:6',
6
		'email' => 'required|email',
7
		'website' => 'url'
8
    );
9
10
    private $errors;
11
12
    public function validate($data) {
13
        $validator = Validator::make($data, $this->rules);
14
15
        if ($validator->fails()) {
16
            $this->errors = $validator->errors;
17
            return false;
18
        }
19
        return true;
20
    }
21
22
    public function errors() {
23
        return $this->errors;
24
    }
25
}

Puedes usar esto desde tu controlador simplemente creando el objeto UserACL y llamando a validar en él. Probablemente también tengas el método "register" en este modelo, y el register simplemente delegará los datos ya validados a tu lógica de negocio.

Validando en un modelo CakePHP

CakePHP también promueve la validación en modelos. 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.

1
class UserACL extends AppModel {
2
3
    public $validate = [
4
		'userName' => [
5
			'rule' => ['minLength', 5],
6
			'required' => true,
7
			'allowEmpty' => false,
8
			'on' => 'create',
9
			'message' => 'User name must be at least 5 characters long.'
10
		],
11
        'password' => [
12
            'rule'    => ['equalsTo', 'confirm'],
13
            'message' => 'The two passwords do not match. Please re-enter them.'
14
        ]
15
    ];
16
17
    public function equalsTo($checkedField, $otherField = null) {
18
		$value = $this->getFieldValue($checkedField);
19
        return $value === $this->data[$this->name][$otherField];
20
    }
21
22
	private function getFieldValue($fieldName) {
23
	    return array_values($otherField)[0];
24
	}
25
}

Solo ejemplificamos las reglas parcialmente. Basta resaltar el poder de validación en el modelo. CakePHP es particularmente bueno en esto. Tiene una gran cantidad de funciones de validación integradas como "minLength" en el ejemplo y varias formas de proporcionar retroalimentación al usuario. Aún más, conceptos como "required" o "allowEmpty" no son en realidad reglas de validación. Cake los verá al generar tu vista y colocará validaciones HTML también en los campos marcados con estos parámetros. Sin embargo, las reglas son excelentes y pueden extenderse fácilmente simplemente creando métodos en la clase modelo, como hicimos para comparar los dos campos de contraseña. Finalmente, siempre puedes especificar el mensaje que deseas enviar a las vistas en caso de falla de validación. Más sobre la validación de CakePHP en cookbook.

La validación en general a nivel de modelo tiene sus ventajas. Cada framework proporciona un fácil acceso a los campos de entrada y crea el mecanismo para notificar al usuario en caso de fallar la validación. No hay necesidad de declaraciones try-catch ni de ningún otro paso sofisticado. La validación en el lado del servidor también asegura que los datos sean validados, pase lo que pase. El usuario ya no puede engañar a nuestro software como con HTML o JavaScript. Por supuesto, cada validación del lado del servidor tiene el costo de un viaje de ida y vuelta de la red y la potencia informática del lado del proveedor en lugar del lado del cliente.


Lanzar excepciones desde la lógica de negocio

El último paso para verificar los datos antes de enviarlos al sistema es a nivel de nuestra lógica de negocio. La información que llega a esta parte del sistema debe desinfectarse lo suficiente para que sea utilizable. La lógica de negocio solo debe comprobar los casos que son críticos para ella. Por ejemplo, agregar un usuario que ya existe es un caso en el que lanzamos una excepción. Comprobar que la longitud del usuario sea de al menos cinco caracteres no debería suceder en este nivel. Podemos asumir con seguridad que tales limitaciones se hicieron cumplir en niveles superiores.

Por otro lado, comparar las dos contraseñas es un tema de discusión. Por ejemplo, si solo encriptamos y guardamos la contraseña cerca del usuario en una base de datos, podríamos descartar el "check" y asumir que las capas anteriores aseguraron que las contraseñas son iguales. Sin embargo, si creamos un usuario real en el sistema operativo usando una API o una herramienta CLI que realmente requiere un nombre de usuario, contraseña y confirmación de contraseña, es posible que deseemos tomar la segunda entrada también y enviarla a una herramienta CLI. Deja que vuelva a validar si las contraseñas coinciden y estarás listo para lanzar una excepción si no lo hacen. De esta manera modelamos nuestra lógica de negocio para que coincida con el comportamiento del sistema operativo real.

Lanzar excepciones desde PHP

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

1
class UserControlTest extends PHPUnit_Framework_TestCase {
2
	function testBehavior() {
3
		$this->assertTrue(true);
4
	}
5
}

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

1
require_once __DIR__ . '/../UserControl.php';
2
class UserControlTest extends PHPUnit_Framework_TestCase {
3
4
	/**

5
	 * @expectedException Exception

6
	 * @expectedExceptionMessage User can not be empty

7
	 */
8
	function testEmptyUsernameWillThrowException() {
9
		$userControl = new UserControl();
10
		$userControl->add('');
11
	}
12
13
}

La siguiente prueba para escribir es un caso degenerativo. No probaremos para una longitud de usuario específica, 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 al negocio, en todas esas capas de nuestra aplicación. Este código obviamente fallará, porque aún no tenemos una clase.

1
PHP Warning:  require_once([long-path-here]/Test/../UserControl.php):
2
failed to open stream: No such file or directory in
3
[long-path-here]/Test/UserControlTest.php on line 2

Creemos la clase y ejecutemos nuestras pruebas. Ahora tenemos otro problema.

1
PHP Fatal error:  Call to undefined method UserControl::add()

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

1
class UserControl {
2
3
	public function add($username) {
4
5
	}
6
7
}

Ahora podemos tener una buena prueba fallida que nos cuenta toda la historia de nuestro código.

1
1) UserControlTest::testEmptyUsernameWillThrowException
2
Failed asserting that exception of type "Exception" is thrown.

Finalmente, podemos hacer una codificación real.

1
public function add($username) {
2
	if(!$username) {
3
		throw new Exception();
4
	}
5
}

Eso hace que la expectativa de la excepción pase, pero sin especificar un mensaje, la prueba aún fallará.

1
1) UserControlTest::testEmptyUsernameWillThrowException
2
Failed asserting that exception message '' contains 'User can not be empty'.

Es hora de escribir el mensaje de la excepción

1
public function add($username) {
2
	if(!$username) {
3
		throw new Exception('User can not be empty!');
4
	}
5
}

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

1
/**

2
 * @expectedException Exception

3
 * @expectedExceptionMessage Cannot add user George

4
 */
5
function testWillNotAddAnAlreadyExistingUser() {
6
	$command = \Mockery::mock('SystemCommand');
7
	$command->shouldReceive('execute')->once()->with('adduser George')->andReturn(false);
8
	$command->shouldReceive('getFailureMessage')->once()->andReturn('User already exists on the system.');
9
	$userControl = new UserControl($command);
10
	$userControl->add('George');
11
}

Lanzar errores en 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 pedido, devolverá un bonito mensaje de falla. Inyectaremos este comando a la clase UserControl para uso interno.

1
class UserControl {
2
3
	private $systemCommand;
4
5
	public function __construct(SystemCommand $systemCommand = null) {
6
		$this->systemCommand = $systemCommand ? : new SystemCommand();
7
	}
8
9
	public function add($username) {
10
		if (!$username) {
11
			throw new Exception('User can not be empty!');
12
		}
13
	}
14
15
}
16
17
class SystemCommand {
18
19
}

Inyectar la instancia de SystemCommand fue bastante fácil. También creamos una clase SystemCommand 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
1) UserControlTest::testWillNotAddAnAlreadyExistingUser
2
Failed asserting that exception of type "Exception" is thrown.

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

1
public function add($username) {
2
	if (!$username) {
3
		throw new Exception('User can not be empty!');
4
	}
5
6
	if(!$this->systemCommand->execute(sprintf('adduser %s', $username))) {
7
		throw new Exception(
8
				sprintf('Cannot add user %s. Reason: %s',
9
						$username,
10
						$this->systemCommand->getFailureMessage()
11
				)
12
			);
13
	}
14
}

Ahora, esas modificaciones al método add() pueden hacer el truco. Intentamos ejecutar nuestro comando en el sistema, pase lo que pase, y si el sistema dice que no puede agregar el usuario por alguna razón, lanzamos una excepción. El mensaje de esta excepción estará parcialmente codificado, con el nombre del usuario adjunto y luego el motivo 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 tienes un sistema más complejo, también necesitas detectar estas excepciones y tomar diferentes acciones basadas en ellas. Analizar el mensaje de una excepción y tomar medidas únicamente al respecto puede generar 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 en constante cambio conducirá a una pesadilla en la gestión de dependencias. En segundo lugar, llamar a un método getMessage() en la excepción detectada cada vez también es 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 a tomar.

1
/**

2
 * @expectedException ExceptionCannotAddUser

3
 * @expectedExceptionMessage Cannot add user George

4
 */
5
function testWillNotAddAnAlreadyExistingUser() {
6
	$command = \Mockery::mock('SystemCommand');
7
	$command->shouldReceive('execute')->once()->with('adduser George')->andReturn(false);
8
	$command->shouldReceive('getFailureMessage')->once()->andReturn('User already exists on the system.');
9
	$userControl = new UserControl($command);
10
	$userControl->add('George');
11
}

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

1
class ExceptionCannotAddUser extends Exception {
2
3
	public function __construct($userName, $reason) {
4
		$message = sprintf(
5
			'Cannot add user %s. Reason: %s',
6
			$userName, $reason
7
		);
8
		parent::__construct($message, 13, null);
9
	}
10
}

La clase que implementa nuestra excepción personalizada es como cualquier otra clase, pero tiene que extender Exception. 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 de negocio y respetamos el principio de responsabilidad única.

1
public function add($username) {
2
	if (!$username) {
3
		throw new Exception('User can not be empty!');
4
	}
5
6
	if(!$this->systemCommand->execute(sprintf('adduser %s', $username))) {
7
		throw new ExceptionCannotAddUser($username, $this->systemCommand->getFailureMessage());
8
	}
9
}

Lanzar nuestra propia excepción es solo una cuestión de cambiar el antiguo comando "throw" por el nuevo y enviar dos parámetros en lugar de redactar el mensaje aquí. Por supuesto, todas las pruebas están pasando.

1
PHPUnit 3.7.28 by Sebastian Bergmann.
2
3
..
4
5
Time: 18 ms, Memory: 3.00Mb
6
7
OK (2 tests, 4 assertions)
8
9
Done.

Detectar excepciones en tu MVC

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

Si no utilizas un framework web, o una interfaz web para el caso, tu capa de presentación debe encargarse de detectar y transformar estas excepciones.

Si desarrollas una biblioteca, detectar sus excepciones será responsabilidad de tus clientes.


Reflexiones finales

Eso es todo. Atravesamos todas las capas de nuestra aplicación. Validamos en JavaScript, HTML y en nuestros modelos. Hemos lanzado y detectado 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 pequeños a grandes proyectos sin problemas graves. Sin embargo, si tu lógica de validación se está volviendo muy compleja y diferentes partes de tu proyecto utilizan partes de lógica superpuestas, puedes considerar extraer todas las validaciones que se pueden realizar en un nivel específico a un servicio de validación o proveedor de validación. Estos niveles pueden incluir, entre otros, el validador de JavaScript, el validador de PHP backend, el validador de comunicaciones de terceros, etc.

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