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 erroresinfo_YYYY-MM-DD.log- Información generaltransaction_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:
-
Registro de transacciones: Registro completo de cada petición HTTP, sus datos, acciones y respuestas (explicado en 7.4).
-
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
-
Entornos configurables: El sistema ajusta automáticamente la verbosidad y el tipo de información de error según el entorno (DEV vs PROD).
-
Codificación de caracteres: El sistema maneja automáticamente problemas de codificación entre UTF-8 y Latin1 mediante la clase
SwitchUtf8Latin1. -
Middleware de transacciones: El ciclo de vida de la petición está envuelto en middlewares (
TransactionStartMiddlewareyTransactionEndMiddleware) que capturan toda la información necesaria para logging completo. -
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