Excepciones personalizadas en PHP: crear jerarquías de errores propias

Crear excepciones personalizadas en PHP te permite capturar errores de forma precisa según el dominio de tu aplicación. En lugar de capturar RuntimeException genérica y adivinar el origen, puedes definir una jerarquía propia donde cada tipo de error tiene su clase, su información adicional y su lugar exacto en el árbol de excepciones.

La base: extender Exception o RuntimeException

<?php
// Excepción raíz de la aplicación
class AppException extends RuntimeException {}

// Subclases por dominio
class ValidationException extends AppException
{
    public function __construct(
        private readonly array $errores,
        string $message = 'Error de validación',
        int $code = 422,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, $code, $previous);
    }

    public function getErrores(): array
    {
        return $this->errores;
    }
}

class DatabaseException extends AppException
{
    public function __construct(
        string $consulta,
        string $message = 'Error de base de datos',
        ?Throwable $previous = null,
    ) {
        parent::__construct("$message — consulta: $consulta", 500, $previous);
    }
}

class NotFoundException extends AppException
{
    public function __construct(string $recurso, int|string $id)
    {
        parent::__construct("$recurso con id '$id' no encontrado", 404);
    }
}
?>

Usar la jerarquía en el código

<?php
class UsuarioServicio
{
    private array $usuarios = [
        1 => ['nombre' => 'Ana', 'email' => '[email protected]'],
    ];

    public function crear(array $datos): array
    {
        $errores = [];
        if (empty($datos['nombre'])) {
            $errores['nombre'] = 'El nombre es obligatorio';
        }
        if (empty($datos['email']) || !filter_var($datos['email'], FILTER_VALIDATE_EMAIL)) {
            $errores['email'] = 'Email inválido';
        }

        if ($errores) {
            throw new ValidationException($errores);
        }

        $id = count($this->usuarios) + 1;
        $this->usuarios[$id] = $datos;
        return ['id' => $id] + $datos;
    }

    public function encontrar(int $id): array
    {
        if (!isset($this->usuarios[$id])) {
            throw new NotFoundException('Usuario', $id);
        }
        return $this->usuarios[$id];
    }
}

$servicio = new UsuarioServicio();

// Captura específica
try {
    $user = $servicio->crear(['nombre' => '', 'email' => 'invalido']);
} catch (ValidationException $e) {
    echo $e->getMessage() . "n";
    print_r($e->getErrores());
}
// Error de validación
// Array ( [nombre] => El nombre es obligatorio [email] => Email inválido )

try {
    $user = $servicio->encontrar(99);
} catch (NotFoundException $e) {
    echo $e->getMessage(); // Usuario con id '99' no encontrado
}
?>

Capturar por clase padre: AppException

<?php
// En un controlador o middleware puedes capturar toda la jerarquía
function manejarPeticion(callable $controlador): array
{
    try {
        return $controlador();
    } catch (ValidationException $e) {
        return ['status' => 422, 'errores' => $e->getErrores()];
    } catch (NotFoundException $e) {
        return ['status' => 404, 'mensaje' => $e->getMessage()];
    } catch (AppException $e) {
        return ['status' => $e->getCode(), 'mensaje' => $e->getMessage()];
    } catch (Throwable $e) {
        // Errores inesperados del sistema
        error_log($e->getMessage());
        return ['status' => 500, 'mensaje' => 'Error interno del servidor'];
    }
}

$servicio = new UsuarioServicio();
$respuesta = manejarPeticion(fn() => $servicio->encontrar(99));
// ['status' => 404, 'mensaje' => "Usuario con id '99' no encontrado"]
?>

Añadir contexto adicional a la excepción

<?php
class ApiException extends AppException
{
    public function __construct(
        private readonly int    $statusHttp,
        private readonly string $codigoError,
        string $message,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, $statusHttp, $previous);
    }

    public function getStatusHttp(): int    { return $this->statusHttp; }
    public function getCodigoError(): string { return $this->codigoError; }

    public function toArray(): array
    {
        return [
            'codigo'  => $this->codigoError,
            'mensaje' => $this->getMessage(),
            'status'  => $this->statusHttp,
        ];
    }
}

throw new ApiException(429, 'RATE_LIMIT_EXCEEDED', 'Demasiadas peticiones');
?>

La documentación oficial sobre extender excepciones en PHP y el catálogo de excepciones SPL muestran las clases base disponibles y cuándo es apropiado usar cada una en lugar de crear una nueva.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP