Skip to main content

7. Gestión de errores y excepciones

Descripción General

El sistema implementa un sofisticado mecanismo de gestión de errores y excepciones que proporciona captura, formateo y registro consistente de errores en toda la aplicación. Este enfoque garantiza que los errores sean manejados adecuadamente, devolviendo respuestas de error bien estructuradas a los clientes y manteniendo un registro detallado para debugging y auditoría.

7.1. Jerarquía de excepciones

La API implementa una jerarquía de excepciones personalizada que extiende la clase base Exception de PHP para proporcionar manejo específico según el tipo de error.

Excepciones principales:

Exception (PHP nativa)
├── BadRequestException (400)
├── ValidationException (400)
├── UnauthorizedException (401)
├── ForbiddenException (403)
├── NotFoundException (404)
├── NotSameOriginException
├── MapeoException
└── BadNumberResultsException (400)

Cada excepción personalizada tiene un código HTTP asociado y puede contener información adicional específica:

// Ejemplo de ValidationException
class ValidationException extends Exception
{
protected $errors;

public function __construct($message = "", $errors = [], $code = 0, Exception $previous = null)
{
parent::__construct($message, $code, $previous);
$this->errors = $errors;
}

public function getErrors()
{
return $this->errors;
}
}

Uso práctico:

// Validación de datos
if (!$ruleInstance->validate($field, $value, $arguments)) {
if ($type === 'Body') {
$this->errors[$field][] = $rule;
} else {
$this->errors[$field][] = 'header ' . $field . ' ' . $rule;
}
}

// Si hay errores, lanzar excepción
if (count($this->errors) > 0) {
throw new ValidationException("Validation failed", $this->errors, 400);
}

7.2. Formateo de respuestas de error

El sistema formatea automáticamente las excepciones y errores en estructuras JSON consistentes. Esta funcionalidad se implementa principalmente en ErrorResponse.php, que procesa excepciones y las convierte en respuestas estructuradas:

// En App\Helpers\ErrorResponse.php
public static function handle($th)
{
if ($th instanceof \Throwable) {
return self::handleTh($th, QuApp::getTraceErrors());
}
if ($th instanceof ErrorContent) {
return self::handleErrorContent($th);
}
return self::handleLastError($th);
}

Estructura de respuesta de error:

{
"error": "Mensaje de error descriptivo",
"code": 400,
"errors": {
"campo1": ["Regla de validación fallida"],
"campo2": ["Otra regla fallida"]
}
}

En el entorno de desarrollo (ENVIRONMENT == "DEV"), se incluye información adicional como el archivo, línea y posiblemente un stack trace:

{
"error": "Mensaje descriptivo",
"code": 400,
"file": "/ruta/al/archivo.php",
"line": 123,
"trace": [...]
}

7.3. Captura de errores no controlados del legacy

El sistema implementa un mecanismo especial para manejar errores que provienen del sistema legacy (Quartup) a través de QuErrorsToException.php, que convierte errores PHP en excepciones:

class QuErrorsToException
{
protected $errorLevelsToConvert;

public function __construct($errorLevelsToConvert = E_ALL)
{
$this->errorLevelsToConvert = $errorLevelsToConvert;
}

public function register()
{
set_error_handler([$this, 'handleError']);
}

public function handleError($severity, $message, $file, $line)
{
if (!($severity & $this->errorLevelsToConvert)) {
return false;
}

throw new ErrorException($message, 0, $severity, $file, $line);
}
}

Este mecanismo se registra en bootstrap.php con diferentes configuraciones según el entorno:

if (ENVIRONMENT == 'DEV') {
$converter = new QuErrorsToException(E_ALL);
} else {
$converter = new QuErrorsToException(E_ALL & ~(E_NOTICE | E_WARNING));
}
$converter->register();

Además, para errores fatales o salidas no capturadas, el sistema utiliza un manejador de cierre con register_shutdown_function():

function handleShutdown()
{
if (!QuApp::$normalTermination) {
$lastError = error_get_last();
$msg = ob_get_contents();

if ($lastError !== null) {
$error = $lastError;
} else if ($msg) {
ob_end_clean();
$error = ErrorContent::get($msg);
} else {
$error = false;
}

if ($error) {
QuApp::setErrorResponse($error);
QuApp::sendResponseAndEnd();
exit(0);
}
}
}
register_shutdown_function('handleShutdown');

7.4. Logging y auditoría

La API implementa un sistema completo de logging para errores, información y transacciones mediante la clase Log:

class Log
{
private static $instance = null;
private $logDirectory;

// Método para registrar errores
public static function error($message)
{
$instance = self::getInstance();
$instance->write($message, 'error');
}

// Método para registrar información
public static function info($message, $type = 'info')
{
$pre = ucfirst($type);
$message = is_array($message) ? "{$pre} " . json_encode($message) : "{$pre} " . $message;
$instance = self::getInstance();
$instance->write($message, $type);
}

// Método para registrar transacciones completas
public static function transaction($transaction)
{
$instance = self::getInstance();
$instance->writeTransaction($transaction);
}
}

Registro de transacciones:

El sistema registra cada transacción (petición) con detalles completos en archivos JSON, incluyendo:

  • Método HTTP
  • URL
  • Timestamp
  • Datos de la petición (headers, query parameters, body)
  • Información del usuario
  • Acciones realizadas
  • Respuesta proporcionada
  • Errores si ocurrieron
protected function writeTransaction($message)
{
$filename = $this->logDirectory . 'transaction_' . date('Y-m-d') . '.json';

if (file_exists($filename)) {
$currentContent = file_get_contents($filename);
$entries = json_decode($currentContent, true);
if (!is_array($entries)) {
$entries = [];
}
} else {
$entries = [];
}

$entries[] = $message;
$jsonContent = json_encode($entries, JSON_PRETTY_PRINT);
file_put_contents($filename, $jsonContent);
}

Los logs se organizan por tipo y fecha, facilitando su análisis:

  • error_YYYY-MM-DD.log - Registros de errores
  • info_YYYY-MM-DD.log - Información general
  • transaction_YYYY-MM-DD.json - Registro detallado de transacciones

7.5. Transacciones y manejo de estados

Es importante diferenciar dos conceptos de "transacciones" en la API:

  1. Registro de transacciones: Registro completo de cada petición HTTP, sus datos, acciones y respuestas (explicado en 7.4).

  2. Transacciones SQL: Mecanismo de base de datos para envolver operaciones en unidades atómicas con capacidad de commit/rollback.

Transacciones SQL:

Los modelos proporcionan funciones para manejar transacciones SQL, permitiendo agrupar operaciones que deben ejecutarse como una unidad atómica:

public function startTransaction()
{
try {
$ko = CoreQuartup::startTransaction();
if ($ko) {
throw new Exception($ko, 500);
}
} catch (\Throwable $th) {
throw $th;
}
}

public function rollbackTransaction($err)
{
try {
CoreQuartup::finishTransaction($err);
if ($err) {
throw new Exception($err, 400);
}
} catch (\Throwable $th) {
throw $th;
}
}

public function commitTransaction()
{
try {
$err = '';
CoreQuartup::finishTransaction($err);
if ($err) {
throw new Exception($err, 500);
}
} catch (\Throwable $th) {
throw $th;
}
}

La clase Repository proporciona un método de conveniencia transaction() que encapsula este proceso:

public function transaction($callable)
{
$this->model->startTransaction();
try {
$res = call_user_func($callable);
$this->model->commitTransaction();
return $res;
} catch (\Throwable $th) {
$this->model->rollbackTransaction($th->getMessage());
throw new Exception("Error durante la transacción: " . $th->getMessage(), 500, $th);
}
}

Ejemplo de uso:

$repository->transaction(function() use ($repository, $data) {
// Realizar múltiples operaciones
$entity1 = $repository->store($data['entity1']);
$entity2 = $repository->store($data['entity2']);

// Si todo sale bien, se hará commit automáticamente
// Si ocurre un error, se hará rollback automáticamente

return ['entity1' => $entity1, 'entity2' => $entity2];
});

Manejo de estados:

El sistema mantiene un registro de las acciones realizadas durante el procesamiento de cada solicitud utilizando QuApp::$actions:

public function store(array $request)
{
$entity = $this->model->store($request);

$action = [$this->model->table => ['stored' => $entity]];
$actions = QuApp::getActions();
$actions[] = $action;
QuApp::setActions($actions);

return $this->selectById($entity['id']);
}

Estas acciones se registran en el log de transacciones, proporcionando un historial completo de las modificaciones realizadas en la base de datos durante cada petición.

Consideraciones Técnicas

  1. Entornos configurables: El sistema ajusta automáticamente la verbosidad y el tipo de información de error según el entorno (DEV vs PROD).

  2. Codificación de caracteres: El sistema maneja automáticamente problemas de codificación entre UTF-8 y Latin1 mediante la clase SwitchUtf8Latin1.

  3. Middleware de transacciones: El ciclo de vida de la petición está envuelto en middlewares (TransactionStartMiddleware y TransactionEndMiddleware) que capturan toda la información necesaria para logging completo.

  4. Flujo de captura de errores:

    • Las excepciones son capturadas en el manejador principal
    • Se procesan a través de ErrorResponse::handle()
    • Se convierten en respuestas HTTP formateadas
    • Se registran en logs
    • Se envían al cliente