PHP: Обработка ошибок

11 вопросов

1 Чем ошибки (errors) отличаются от исключений (exceptions)?

В PHP 7+ иерархия ошибок:

Throwable (interface)
├── Error (внутренние ошибки PHP)
│   ├── TypeError
│   ├── ValueError
│   ├── ArithmeticError
│   ├── DivisionByZeroError
│   └── ...
└── Exception (пользовательские исключения)
    ├── RuntimeException
    ├── LogicException
    ├── InvalidArgumentException
    └── ...

Ошибки (Error) - критические проблемы: вызов несуществующего метода, неправильный тип аргумента. Исключения (Exception) - предсказуемые ситуации: файл не найден, невалидные данные.

Оба ловятся через try/catch (Throwable). Но Error обычно сигнализирует о баге в коде, Exception - о предсказуемой проблеме.

Открыть отдельно →
2 Какие уровни ошибок есть?
  • E_ERROR (1) - фатальная, скрипт останавливается
  • E_WARNING (2) - предупреждение, скрипт продолжается
  • E_NOTICE (8) - замечание (неопределенная переменная)
  • E_STRICT (2048) - рекомендации (удален в PHP 8)
  • E_DEPRECATED (8192) - устаревший функционал
  • E_PARSE (4) - синтаксическая ошибка
  • E_RECOVERABLE_ERROR (4096) - перехватываемая фатальная ошибка

Рекомендация: error_reporting(E_ALL) на разработке. На проде - логирование всех ошибок, display_errors = Off.

Открыть отдельно →
3 Что такое Error vs Exception в PHP 7+?

До PHP 7 фатальные ошибки нельзя было перехватить. С PHP 7 введен интерфейс Throwable:

try {
    $result = call_to_undefined_function();
} catch (Error $e) {
    echo "Ошибка PHP: " . $e->getMessage();
} catch (Exception $e) {
    echo "Исключение: " . $e->getMessage();
} catch (Throwable $e) {
    echo "Любая ошибка: " . $e->getMessage();
}

Нельзя создать класс, реализующий Throwable напрямую - нужно наследовать Error или Exception.

Открыть отдельно →
4 Что такое try/catch/finally?
try {
    $conn = new PDO($dsn);
    $result = $conn->query($sql);
} catch (PDOException $e) {
    // Обработка ошибки БД
    log($e->getMessage());
    throw new DatabaseException('Query failed', 0, $e); // re-throw
} catch (Exception $e) {
    // Любое другое исключение
    log($e->getMessage());
} finally {
    // Выполняется ВСЕГДА (даже после return или throw)
    $conn = null; // закрываем соединение
}

Блоки catch проверяются по порядку - ставьте более конкретные исключения первыми. finally используется для очистки ресурсов.

Открыть отдельно →
5 Что произойдет, если исключение не поймано?

Если исключение не поймано в try/catch, оно всплывает по стеку вызовов до глобального обработчика. Если обработчика нет - PHP выдает Fatal Error: "Uncaught Exception" и скрипт завершается.

// Установка глобального обработчика
set_exception_handler(function (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500);
    echo 'Internal Server Error';
});

Фреймворки (Laravel, Symfony) имеют свои exception handler-ы, которые логируют ошибку и возвращают подходящий HTTP-ответ.

Открыть отдельно →
6 Что такое custom exception classes?
// Базовое исключение приложения
class AppException extends RuntimeException {}

// Специализированные исключения
class NotFoundException extends AppException {
    public static function forEntity(string $entity, mixed $id): self {
        return new self("$entity with ID $id not found");
    }
}

class ValidationException extends AppException {
    public function __construct(
        private array $errors,
        string $message = 'Validation failed',
    ) {
        parent::__construct($message);
    }

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

// Использование
throw NotFoundException::forEntity('User', 42);

Создавайте иерархию исключений для разных типов ошибок - это позволяет ловить их группами.

Открыть отдельно →
7 Можно ли создавать собственные классы исключений?

Да, наследуя Exception или RuntimeException/LogicException:

class InsufficientFundsException extends RuntimeException {
    public function __construct(
        public readonly float $balance,
        public readonly float $amount,
    ) {
        parent::__construct(
            sprintf('Insufficient funds: balance %.2f, requested %.2f', $balance, $amount)
        );
    }
}

try {
    throw new InsufficientFundsException(100.0, 150.0);
} catch (InsufficientFundsException $e) {
    echo "Balance: {$e->balance}, needed: {$e->amount}";
}

Рекомендации: наследуйте RuntimeException для ошибок в runtime, LogicException для ошибок в логике кода.

Открыть отдельно →
8 Что такое set_error_handler() и set_exception_handler()?
// Пользовательский обработчик ошибок (warnings, notices)
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

// Глобальный обработчик исключений (если не поймали в try/catch)
set_exception_handler(function (Throwable $e) {
    error_log(sprintf("[%s] %s in %s:%d", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
    if (php_sapi_name() !== 'cli') {
        http_response_code(500);
        echo 'Internal Server Error';
    }
});

// Для фатальных ошибок (E_ERROR, E_PARSE) - register_shutdown_function
register_shutdown_function(function () {
    $error = error_get_last();
    if ($error && ($error['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR))) {
        error_log("Fatal: {$error['message']}");
    }
});
Открыть отдельно →
9 Что такое SPL exceptions?

SPL предоставляет иерархию стандартных исключений:

LogicException (ошибки в логике, могут быть найдены при code review)
├── BadFunctionCallException
├── BadMethodCallException
├── DomainException        // нарушение бизнес-правила
├── InvalidArgumentException // неверный аргумент
├── LengthException
├── OutOfRangeException    // индекс вне диапазона (compile-time)
└── OverflowException

RuntimeException (ошибки, обнаруживаемые только в runtime)
├── OutOfBoundsException   // индекс вне диапазона (runtime)
├── RangeException
├── UnderflowException
└── UnexpectedValueException

Используйте эти классы (или наследуйте от них) вместо голого Exception - это позволяет ловить ошибки по категориям.

Открыть отдельно →
10 Когда использовать исключения, а когда коды ошибок?

Исключения предпочтительнее в PHP:

  • Невозможно игнорировать (в отличие от кодов)
  • Содержат stack trace для отладки
  • Можно группировать по иерархии
  • Разделяют happy path и error handling

Коды ошибок (return values) допустимы когда:

  • Ошибка ожидаема и часта (проверка валидности)
  • Вызывающий код обязан обработать результат
  • Высокопроизводительный код (исключения дорогие)
// Плохо: null как код ошибки
function find(int $id): ?User { return null; }

// Лучше: Result type или исключение
function findOrFail(int $id): User {
    return $this->repo->find($id) ?? throw new NotFoundException();
}
Открыть отдельно →
11 Какова роль finally?

finally выполняется всегда, независимо от того, было ли исключение, был ли return или throw в try/catch:

function readFile(string $path): string {
    $file = fopen($path, 'r');
    try {
        $content = fread($file, filesize($path));
        return $content;
    } catch (Exception $e) {
        return '';
    } finally {
        fclose($file); // выполнится даже после return!
    }
}

Используется для: закрытия файлов/соединений, освобождения блокировок, логирования. Если finally содержит return, он перезаписывает return из try/catch (избегайте этого).

Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.