MidТеория5 min

Исключения

try/catch/finally, custom exceptions, Exception vs Error, SPL exceptions

Иерархия Throwable

Throwable (interface)
├── Error
│   ├── TypeError
│   ├── ValueError
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   ├── FiberError
│   ├── UnhandledMatchError
│   └── ...
└── Exception
    ├── RuntimeException
    │   ├── OverflowException
    │   ├── UnderflowException
    │   ├── OutOfBoundsException
    │   └── UnexpectedValueException
    ├── LogicException
    │   ├── InvalidArgumentException
    │   ├── BadMethodCallException
    │   ├── DomainException
    │   ├── LengthException
    │   └── OutOfRangeException
    ├── BadFunctionCallException
    └── ...
<?php
declare(strict_types=1);

// Throwable is the base interface (PHP 7+)
// You CANNOT implement Throwable directly!
// You MUST extend Exception or Error

// Exception — recoverable errors (your code should handle)
// Error — unrecoverable errors (bugs, type errors, etc.)

// Catch both:
try {
    // risky code
} catch (\Throwable $e) {
    // catches both Exception and Error
    echo $e->getMessage();
}

Ловушка экзамена: Throwable — это интерфейс, НЕ класс. Нельзя напрямую реализовать Throwable — нужно наследоваться от Exception или Error. Только Exception и Error могут быть базовыми классами для пользовательских исключений.

try/catch/finally

<?php
declare(strict_types=1);

// Basic try/catch
try {
    $result = riskyOperation();
} catch (RuntimeException $e) {
    echo "Runtime: {$e->getMessage()}";
} catch (InvalidArgumentException $e) {
    echo "Invalid arg: {$e->getMessage()}";
} catch (Exception $e) {
    echo "General: {$e->getMessage()}";
}

// Multi-catch (PHP 8.0+)
try {
    $data = processData($input);
} catch (InvalidArgumentException | TypeError $e) {
    echo "Bad input: {$e->getMessage()}";
}

// finally — ALWAYS executes
function readConfig(string $path): array
{
    $handle = fopen($path, 'r');
    try {
        $data = fread($handle, filesize($path));
        return json_decode($data, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        return [];
    } finally {
        // Executes even after return!
        fclose($handle);
    }
}

// try without catch (only finally)
function cleanup(): void
{
    $resource = acquireResource();
    try {
        useResource($resource);
    } finally {
        releaseResource($resource);  // Always runs
    }
}

Запомни: Блок finally выполняется ВСЕГДА — даже после return, throw, или break. Если и в try, и в finally есть return, то возвращается значение из finally. Если finally содержит throw, исключение из try/catch теряется.

Создание пользовательских исключений

<?php
declare(strict_types=1);

// Basic custom exception
class OrderNotFoundException extends RuntimeException
{
    public function __construct(
        private readonly int $orderId,
        ?\Throwable $previous = null,
    ) {
        parent::__construct(
            message: "Order #{$orderId} not found",
            code: 404,
            previous: $previous,
        );
    }

    public function getOrderId(): int
    {
        return $this->orderId;
    }
}

// Exception with context data
class ValidationException extends RuntimeException
{
    /** @param array<string, string[]> $errors */
    public function __construct(
        private readonly array $errors,
        ?\Throwable $previous = null,
    ) {
        $message = 'Validation failed: ' . implode(', ', array_keys($errors));
        parent::__construct($message, 422, $previous);
    }

    /** @return array<string, string[]> */
    public function getErrors(): array
    {
        return $this->errors;
    }
}

// Usage
try {
    throw new ValidationException([
        'email' => ['Invalid email format'],
        'age' => ['Must be at least 18'],
    ]);
} catch (ValidationException $e) {
    foreach ($e->getErrors() as $field => $messages) {
        echo "{$field}: " . implode(', ', $messages) . "\n";
    }
}

Exception chaining (Previous)

<?php
declare(strict_types=1);

// Chain exceptions to preserve original cause
class DatabaseException extends RuntimeException {}
class UserServiceException extends RuntimeException {}

function findUserInDb(int $id): array
{
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
        $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: throw new RuntimeException('Not found');
    } catch (PDOException $e) {
        // Wrap low-level exception in domain exception
        throw new DatabaseException(
            message: "Failed to query user #{$id}",
            code: 0,
            previous: $e,  // Preserve original exception
        );
    }
}

function getUser(int $id): array
{
    try {
        return findUserInDb($id);
    } catch (DatabaseException $e) {
        throw new UserServiceException(
            message: "Cannot get user",
            previous: $e,
        );
    }
}

// Traverse exception chain
try {
    $user = getUser(42);
} catch (UserServiceException $e) {
    $current = $e;
    while ($current !== null) {
        echo get_class($current) . ": {$current->getMessage()}\n";
        $current = $current->getPrevious();
    }
}

Ловушка экзамена: Всегда передавайте $previous в пользовательские исключения! Это сохраняет цепочку ошибок для отладки. getPrevious() возвращает null если предыдущего исключения нет.

SPL Exceptions

<?php
declare(strict_types=1);

// Logic errors — bugs that should be fixed in code
throw new InvalidArgumentException('Age must be positive');
throw new BadMethodCallException('Method not implemented');
throw new DomainException('Value outside allowed domain');
throw new LengthException('String too long');
throw new OutOfRangeException('Index 10 is out of range 0-5');

// Runtime errors — can occur during normal operation
throw new RuntimeException('Cannot connect to service');
throw new OverflowException('Queue is full');
throw new UnderflowException('Stack is empty');
throw new OutOfBoundsException('Key does not exist');
throw new UnexpectedValueException('Expected int, got string');

// Best practice: choose the most specific SPL exception
function divide(float $a, float $b): float
{
    if ($b === 0.0) {
        throw new InvalidArgumentException('Divisor cannot be zero');
    }
    return $a / $b;
}

function getElement(array $arr, int $index): mixed
{
    if ($index < 0 || $index >= count($arr)) {
        throw new OutOfRangeException("Index {$index} is out of range");
    }
    return $arr[$index];
}
Тип Когда использовать
LogicException Ошибка в логике программы (баг)
InvalidArgumentException Некорректный аргумент
BadMethodCallException Вызов несуществующего метода
RuntimeException Ошибка во время выполнения
OverflowException Переполнение контейнера
OutOfBoundsException Ключ/индекс не существует
UnexpectedValueException Неожиданное значение

set_exception_handler

<?php
declare(strict_types=1);

// Global exception handler for uncaught exceptions
set_exception_handler(function (\Throwable $e): void {
    error_log(sprintf(
        "[%s] %s in %s:%d\nStack trace:\n%s",
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString(),
    ));

    if (PHP_SAPI !== 'cli') {
        http_response_code(500);
        echo '<h1>Internal Server Error</h1>';
    } else {
        fwrite(STDERR, "Fatal: {$e->getMessage()}\n");
    }
});

// Restore default handler
restore_exception_handler();

Запомни: set_exception_handler() перехватывает НЕперехваченные исключения. После выполнения обработчика скрипт завершается. Это последний рубеж — основная обработка должна быть через try/catch.

Exception в PHP 8.0+

<?php
declare(strict_types=1);

// PHP 8.0: throw is an expression (not statement)
$value = $input ?? throw new InvalidArgumentException('Input required');

$name = match ($role) {
    'admin' => 'Administrator',
    'user' => 'User',
    default => throw new UnexpectedValueException("Unknown role: {$role}"),
};

// PHP 8.0: non-capturing catch
try {
    riskyOperation();
} catch (SpecificException) {
    // No need for $e variable if not used
    log('Operation failed');
}

// PHP 8.0: Union types in catch
try {
    parseData($raw);
} catch (JsonException | InvalidArgumentException $e) {
    handleParseError($e);
}

// PHP 8.1: never return type for functions that always throw
function fail(string $message): never
{
    throw new RuntimeException($message);
}

// PHP 8.1: Enums + exceptions
enum HttpStatus: int
{
    case NotFound = 404;
    case ServerError = 500;

    public function toException(): RuntimeException
    {
        return match ($this) {
            self::NotFound => new RuntimeException('Not Found', 404),
            self::ServerError => new RuntimeException('Server Error', 500),
        };
    }
}

Ловушка экзамена: В PHP 8.0 throw стал выражением (expression), а не инструкцией (statement). Это позволяет использовать его в ??, ?:, match, arrow functions и других контекстах выражений.

Паттерн: Exception → JSON Response

<?php
declare(strict_types=1);

// API exception handler
set_exception_handler(function (\Throwable $e): void {
    $statusCode = match (true) {
        $e instanceof InvalidArgumentException => 400,
        $e instanceof AuthenticationException => 401,
        $e instanceof AuthorizationException => 403,
        $e instanceof NotFoundException => 404,
        $e instanceof ValidationException => 422,
        default => 500,
    };

    http_response_code($statusCode);
    header('Content-Type: application/json');

    $response = ['error' => $e->getMessage()];

    // Include details in non-production
    if (getenv('APP_ENV') !== 'production') {
        $response['exception'] = get_class($e);
        $response['file'] = $e->getFile() . ':' . $e->getLine();
        $response['trace'] = $e->getTrace();
    }

    echo json_encode($response, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
});

Вопросы с экзамена ZCE

Проверь себя

5 из 8

В какой из следующих ситуаций вы используете функцию set_exception_handler()?

Что нового в PHP 8.0 для `throw`?

Какие из следующих утверждений об исключениях НЕ верны?

Выберите все правильные варианты

Какой SPL exception для некорректного аргумента?

Выполнится ли блок `finally` после `return` в `try`?