Lección 51 de 75 10 min de lectura

Excepciones Personalizadas

Las excepciones genéricas funcionan, pero crear tus propias excepciones te permite representar errores específicos de tu dominio, añadir información extra y capturarlas de forma más precisa.

Crear una excepción básica

Para crear una excepción personalizada, simplemente extiende de Exception o cualquiera de sus subclases:

PHP
<?php

declare(strict_types=1);

// Excepción simple sin modificaciones
class UsuarioNoEncontradoException extends Exception
{
}

// Uso
function buscarUsuario(int $id): array
{
    $usuarios = [
        1 => ['nombre' => 'Ana', 'email' => 'ana@example.com'],
        2 => ['nombre' => 'Luis', 'email' => 'luis@example.com'],
    ];

    if (!isset($usuarios[$id])) {
        throw new UsuarioNoEncontradoException("Usuario con ID $id no encontrado");
    }

    return $usuarios[$id];
}

try {
    $usuario = buscarUsuario(999);
} catch (UsuarioNoEncontradoException $e) {
    // Captura específica para este tipo de error
    echo 'No se encontró el usuario: ' . $e->getMessage();
}

Añadir datos adicionales

Las excepciones personalizadas pueden incluir información extra relevante para el error:

PHP
<?php

declare(strict_types=1);

class ValidacionException extends Exception
{
    private array $errores;

    public function __construct(array $errores, string $mensaje = 'Error de validación')
    {
        parent::__construct($mensaje);
        $this->errores = $errores;
    }

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

// Uso
function validarFormulario(array $datos): array
{
    $errores = [];

    if (empty($datos['email'])) {
        $errores['email'] = 'El email es requerido';
    } elseif (!filter_var($datos['email'], FILTER_VALIDATE_EMAIL)) {
        $errores['email'] = 'El email no es válido';
    }

    if (empty($datos['nombre'])) {
        $errores['nombre'] = 'El nombre es requerido';
    }

    if (!empty($errores)) {
        throw new ValidacionException($errores);
    }

    return $datos;
}

try {
    $datos = validarFormulario(['email' => 'invalido', 'nombre' => '']);
} catch (ValidacionException $e) {
    echo $e->getMessage() . "\n";

    foreach ($e->getErrores() as $campo => $error) {
        echo "- $campo: $error\n";
    }
    // Error de validación
    // - email: El email no es válido
    // - nombre: El nombre es requerido
}

Jerarquía de excepciones

Para aplicaciones más grandes, es útil crear una jerarquía de excepciones que represente los diferentes tipos de errores:

PHP
<?php

declare(strict_types=1);

// Excepción base de la aplicación
class AppException extends Exception
{
}

// Excepciones de dominio
class UsuarioException extends AppException
{
}

class UsuarioNoEncontradoException extends UsuarioException
{
    public function __construct(int $id)
    {
        parent::__construct("Usuario con ID $id no encontrado", 404);
    }
}

class UsuarioDuplicadoException extends UsuarioException
{
    public function __construct(string $email)
    {
        parent::__construct("Ya existe un usuario con email: $email", 409);
    }
}

// Excepciones de autenticación
class AuthException extends AppException
{
}

class CredencialesInvalidasException extends AuthException
{
    public function __construct()
    {
        parent::__construct('Email o contraseña incorrectos', 401);
    }
}

class SesionExpiradaException extends AuthException
{
    public function __construct()
    {
        parent::__construct('Tu sesión ha expirado', 401);
    }
}

// Uso con captura por jerarquía
try {
    autenticarUsuario($email, $password);
} catch (CredencialesInvalidasException $e) {
    // Error específico de credenciales
    echo 'Credenciales incorrectas';
} catch (AuthException $e) {
    // Cualquier otro error de autenticación
    echo 'Error de autenticación: ' . $e->getMessage();
} catch (AppException $e) {
    // Cualquier error de la aplicación
    echo 'Error: ' . $e->getMessage();
}

Excepción con contexto HTTP

Para APIs y aplicaciones web, es útil que las excepciones incluyan información sobre el código HTTP correspondiente:

PHP
<?php

declare(strict_types=1);

class HttpException extends Exception
{
    private int $statusCode;
    private array $headers;

    public function __construct(
        string $message,
        int $statusCode = 500,
        array $headers = [],
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $statusCode, $previous);
        $this->statusCode = $statusCode;
        $this->headers = $headers;
    }

    public function getStatusCode(): int
    {
        return $this->statusCode;
    }

    public function getHeaders(): array
    {
        return $this->headers;
    }
}

class NotFoundException extends HttpException
{
    public function __construct(string $recurso = 'Recurso')
    {
        parent::__construct("$recurso no encontrado", 404);
    }
}

class UnauthorizedException extends HttpException
{
    public function __construct(string $message = 'No autorizado')
    {
        parent::__construct($message, 401, [
            'WWW-Authenticate' => 'Bearer'
        ]);
    }
}

class ForbiddenException extends HttpException
{
    public function __construct(string $message = 'Acceso denegado')
    {
        parent::__construct($message, 403);
    }
}

// Manejador de errores para API
function manejarExcepcion(Throwable $e): void
{
    $statusCode = 500;
    $body = ['error' => 'Error interno del servidor'];

    if ($e instanceof HttpException) {
        $statusCode = $e->getStatusCode();
        $body = ['error' => $e->getMessage()];

        foreach ($e->getHeaders() as $nombre => $valor) {
            header("$nombre: $valor");
        }
    }

    http_response_code($statusCode);
    header('Content-Type: application/json');
    echo json_encode($body);
}

// Uso
try {
    $usuario = buscarUsuario(999);
} catch (Throwable $e) {
    manejarExcepcion($e);
}

Ejemplo práctico: Sistema de pagos

PHP
<?php

declare(strict_types=1);

// Excepciones del dominio de pagos
class PagoException extends Exception
{
    protected string $transaccionId;

    public function __construct(string $mensaje, string $transaccionId = '')
    {
        parent::__construct($mensaje);
        $this->transaccionId = $transaccionId;
    }

    public function getTransaccionId(): string
    {
        return $this->transaccionId;
    }
}

class SaldoInsuficienteException extends PagoException
{
    private float $saldoActual;
    private float $montoRequerido;

    public function __construct(float $saldoActual, float $montoRequerido)
    {
        $faltante = $montoRequerido - $saldoActual;
        parent::__construct("Saldo insuficiente. Faltan \$$faltante");
        $this->saldoActual = $saldoActual;
        $this->montoRequerido = $montoRequerido;
    }

    public function getSaldoActual(): float
    {
        return $this->saldoActual;
    }

    public function getMontoFaltante(): float
    {
        return $this->montoRequerido - $this->saldoActual;
    }
}

class TarjetaRechazadaException extends PagoException
{
    private string $codigoRechazo;

    public function __construct(string $codigoRechazo, string $transaccionId)
    {
        $mensajes = [
            'INSUFFICIENT_FUNDS' => 'Fondos insuficientes en la tarjeta',
            'EXPIRED_CARD' => 'La tarjeta ha expirado',
            'INVALID_CVV' => 'CVV incorrecto',
            'BLOCKED_CARD' => 'La tarjeta está bloqueada',
        ];

        $mensaje = $mensajes[$codigoRechazo] ?? 'Tarjeta rechazada';
        parent::__construct($mensaje, $transaccionId);
        $this->codigoRechazo = $codigoRechazo;
    }

    public function getCodigoRechazo(): string
    {
        return $this->codigoRechazo;
    }
}

// Servicio de pagos
class ServicioPago
{
    private float $saldo = 100.00;

    public function realizarPago(float $monto, string $metodoPago): string
    {
        if ($monto <= 0) {
            throw new InvalidArgumentException('El monto debe ser positivo');
        }

        $transaccionId = uniqid('TXN_');

        if ($metodoPago === 'saldo') {
            if ($this->saldo < $monto) {
                throw new SaldoInsuficienteException($this->saldo, $monto);
            }
            $this->saldo -= $monto;

        } elseif ($metodoPago === 'tarjeta') {
            // Simular rechazo aleatorio
            if (rand(0, 1) === 0) {
                $codigos = ['INSUFFICIENT_FUNDS', 'EXPIRED_CARD', 'INVALID_CVV'];
                throw new TarjetaRechazadaException(
                    $codigos[array_rand($codigos)],
                    $transaccionId
                );
            }
        }

        return $transaccionId;
    }
}

// Uso
$servicio = new ServicioPago();

try {
    $txnId = $servicio->realizarPago(150.00, 'saldo');
    echo "Pago exitoso: $txnId";

} catch (SaldoInsuficienteException $e) {
    echo $e->getMessage();
    echo "\nTe faltan: $" . $e->getMontoFaltante();

} catch (TarjetaRechazadaException $e) {
    echo $e->getMessage();
    echo "\nCódigo: " . $e->getCodigoRechazo();
    echo "\nTransacción: " . $e->getTransaccionId();

} catch (PagoException $e) {
    echo "Error en el pago: " . $e->getMessage();
}

Cuándo crear excepciones personalizadas

Guía práctica

Crea una excepción personalizada cuando necesites: capturarla de forma específica, añadir datos adicionales, o representar un error de tu dominio. Si solo necesitas un mensaje diferente, usa las excepciones SPL existentes.

PHP
<?php

declare(strict_types=1);

// NO necesitas excepción personalizada para esto:
throw new InvalidArgumentException('El email no es válido');

// SÍ necesitas cuando quieres:

// 1. Capturar de forma específica
try {
    // ...
} catch (EmailInvalidoException $e) {
    // Manejar específicamente emails inválidos
}

// 2. Añadir datos del dominio
class ProductoAgotadoException extends Exception
{
    public function __construct(
        private string $sku,
        private int $stockActual
    ) {
        parent::__construct("Producto $sku agotado");
    }

    public function getSku(): string { return $this->sku; }
    public function getStockActual(): int { return $this->stockActual; }
}

// 3. Agrupar errores relacionados
try {
    procesarPedido($datos);
} catch (PedidoException $e) {
    // Captura todas las excepciones relacionadas con pedidos
}

Ejercicios

Ejercicio 1: Excepción de carrito

Crea una excepción CarritoVacioException que se lance cuando se intente procesar un carrito sin productos. Incluye un método para obtener el ID del carrito.

Ver solución
<?php

declare(strict_types=1);

class CarritoVacioException extends Exception
{
    private string $carritoId;

    public function __construct(string $carritoId)
    {
        parent::__construct("El carrito $carritoId está vacío");
        $this->carritoId = $carritoId;
    }

    public function getCarritoId(): string
    {
        return $this->carritoId;
    }
}

class Carrito
{
    private string $id;
    private array $productos = [];

    public function __construct(string $id)
    {
        $this->id = $id;
    }

    public function procesar(): void
    {
        if (empty($this->productos)) {
            throw new CarritoVacioException($this->id);
        }
        // Procesar carrito...
    }
}

// Uso
try {
    $carrito = new Carrito('CART-123');
    $carrito->procesar();
} catch (CarritoVacioException $e) {
    echo $e->getMessage() . "\n";
    echo "ID del carrito: " . $e->getCarritoId();
}

Ejercicio 2: Jerarquía de archivos

Crea una jerarquía de excepciones para operaciones con archivos: ArchivoException (base), ArchivoNoEncontradoException, PermisosDenegadosException, ArchivoCorruptoException. Cada una debe tener la información relevante (ruta, permisos, etc.).

Ver solución
<?php

declare(strict_types=1);

class ArchivoException extends Exception
{
    protected string $ruta;

    public function __construct(string $mensaje, string $ruta)
    {
        parent::__construct($mensaje);
        $this->ruta = $ruta;
    }

    public function getRuta(): string
    {
        return $this->ruta;
    }
}

class ArchivoNoEncontradoException extends ArchivoException
{
    public function __construct(string $ruta)
    {
        parent::__construct("Archivo no encontrado: $ruta", $ruta);
    }
}

class PermisosDenegadosException extends ArchivoException
{
    private string $permisosRequeridos;

    public function __construct(string $ruta, string $permisos)
    {
        parent::__construct("Permisos insuficientes para: $ruta", $ruta);
        $this->permisosRequeridos = $permisos;
    }

    public function getPermisosRequeridos(): string
    {
        return $this->permisosRequeridos;
    }
}

class ArchivoCorruptoException extends ArchivoException
{
    private string $razon;

    public function __construct(string $ruta, string $razon)
    {
        parent::__construct("Archivo corrupto: $ruta", $ruta);
        $this->razon = $razon;
    }

    public function getRazon(): string
    {
        return $this->razon;
    }
}

// Uso
try {
    throw new PermisosDenegadosException('/etc/passwd', 'lectura');
} catch (ArchivoException $e) {
    echo $e->getMessage() . "\n";
    echo "Ruta: " . $e->getRuta();
}

Ejercicio 3: API con excepciones HTTP

Crea un manejador de excepciones para una API que convierta diferentes excepciones en respuestas JSON con el código HTTP apropiado. Por ejemplo, NotFoundException → 404, ValidacionException → 422.

Ver solución
<?php

declare(strict_types=1);

class NotFoundException extends Exception
{
    public function __construct(string $recurso)
    {
        parent::__construct("$recurso no encontrado", 404);
    }
}

class ValidacionException extends Exception
{
    private array $errores;

    public function __construct(array $errores)
    {
        parent::__construct('Error de validación', 422);
        $this->errores = $errores;
    }

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

function manejarExcepcionApi(Throwable $e): void
{
    $codigo = match (true) {
        $e instanceof NotFoundException => 404,
        $e instanceof ValidacionException => 422,
        $e instanceof InvalidArgumentException => 400,
        default => 500
    };

    $respuesta = ['error' => true, 'message' => $e->getMessage()];

    if ($e instanceof ValidacionException) {
        $respuesta['errores'] = $e->getErrores();
    }

    http_response_code($codigo);
    header('Content-Type: application/json');
    echo json_encode($respuesta, JSON_PRETTY_PRINT);
}

// Uso
try {
    throw new ValidacionException([
        'email' => 'Email inválido',
        'edad' => 'Debe ser mayor de 18'
    ]);
} catch (Throwable $e) {
    manejarExcepcionApi($e);
}

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales y soporte.

Descubrir cursos premium