Skip to content

Исключения и ошибки

Исключение — это специальный объект, который является экземпляром встроенного класса Exceptionили производного от него класса. Объекты типа Exception предназначены для хранения информации об ошибках и выдачи сообщений о них.

Screenshot 2024-11-24 at 02.51.02.png

Вместе с объектом типа Exception используется инструкция с ключевым словом throw. Эта инструкция останавливает выполнение текущего метода и передает ответственность за обработку ошибок обратно вызывающему коду.

Пример

Screenshot 2024-11-24 at 02.53.28.png

Пример обработки Exceptions в коде

Screenshot 2024-11-24 at 02.55.34.png

Ошибки

Когда исключения только внедрялись, механизмы try-catch применялись главным образом в коде сценариев, но не в самом ядре РНР , где возникавшие ошибки обрабатывались собственной логикой. Это могло привести к серьезным осложнениям, если требовалось обрабатывать ошибки, возникающие в ядре РНР , таким же образом, как и исключения в прикладном коде. В качестве первоначальной меры борьбы с подобными осложнениями в версии РНР 7 был создан внутренний класс Error, в котором реализован тот же встроенный интерфейс Throwable, что и в классе Exception. Поэтому с классом Error можно обращаться так же, как и с классом Exception.

Практика

1. Создание пользовательского исключения для валидации данных

Создайте пользовательское исключение для валидации email-адресов. Например, если email-адрес не соответствует стандартному формату, выбрасывается исключение с соответствующим сообщением. Также добавьте обработку этого исключения, чтобы выводить на экран сообщение о недействительном email.

Задание:

  • Создайте класс InvalidEmailException, который наследуется от встроенного класса Exception.
  • Напишите функцию validateEmail($email), которая будет проверять формат email-адреса с помощью регулярного выражения. Если формат некорректен — выбрасывайте исключение InvalidEmailException.
  • Обработайте исключение в основном коде и выведите сообщение об ошибке.

2. Логирование ошибок в файл

Реализуйте систему логирования ошибок в файл. При возникновении исключения, создайте лог-файл, куда будет записываться информация о возникшей ошибке, включая дату, время, сообщение и стек вызовов.

Задание:

  • Создайте класс ErrorLogger, который будет логировать ошибки в файл (например, error_log.txt).
  • Используйте метод set_exception_handler для обработки необработанных исключений, и записывайте информацию об ошибке в файл.
  • Реализуйте логику, при которой в лог-файл будет записываться ошибка с полным стеком вызовов и временной меткой.

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

Разработайте кастомный тип исключений для работы с ограничениями по возрасту пользователя. Например, если возраст пользователя меньше 18 лет, выбрасывайте исключение с определенным сообщением.

Задание:

  • Создайте класс UnderageException, наследующий от Exception.
  • Напишите функцию checkAge($age), которая будет проверять, что возраст пользователя больше или равен 18. Если возраст меньше 18 — выбрасывайте исключение UnderageException.
  • Обработайте исключение и выведите сообщение об ошибке.

4. Обработка нескольких типов исключений

Реализуйте код, в котором можно обработать несколько типов исключений, например, исключения на основе типа ошибки базы данных и ошибки валидации данных.

Задание:

  • Создайте два класса исключений: DatabaseException и ValidationException.
  • Напишите функцию, которая может выбрасывать одно из этих исключений в зависимости от типа ошибки.
  • В коде обработчика используйте конструкцию try-catch для обработки обоих типов исключений.

5. Реализация обработки исключений с использованием логирования и ретраев

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

Задание:

  • Создайте класс RetryDatabaseException, который будет выбрасываться в случае неудачных попыток подключения к базе данных.
  • Напишите функцию, которая будет пытаться подключиться к базе данных, но в случае ошибки будет пытаться подключиться несколько раз (до 3 попыток).
  • Если все попытки неудачны, выбросите исключение RetryDatabaseException и запишите в лог информацию о неудачных попытках.

// Создание пользовательского исключения 
// для валидации данных

// 1. Создайте класс `InvalidEmailException`, 
// который наследуется от встроенного класса 
// `Exception`.

class InvalidEmailException extends Exception {

}

function validateEmail(string $email) {
    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
    $result = preg_match($pattern, $email);

    if (!$result) {
        throw new InvalidEmailException('Почта: ' . $email . ', не является валидной');
    }

    return true;
}

validateEmail('sdfsdfsdf');

// Результат: 

/**
 * 
 * Fatal error: Uncaught InvalidEmailException: Почта: sdfsdfsdf, 
 * не является валидной in /var/www/www/php8-book/exceptions.php:19 
 * Stack trace: #0 /var/www/www/php8-book/exceptions.php(25): 
 * validateEmail('sdfsdfsdf') #1 {main} thrown in 
 * /var/www/www/php8-book/exceptions.php on line 19
 * 
 */


// Базовый класс для пользовательских исключений
class CustomException extends Exception {
    // Дополнительные свойства
    protected $context;
    protected $logFile;

    // Конструктор с возможностью передачи дополнительных параметров
    public function __construct($message = "", $code = 0, Exception $previous = null, $context = [], $logFile = 'exception.log') {
        $this->context = $context;
        $this->logFile = $logFile;

        // Вызываем конструктор родительского класса
        parent::__construct($message, $code, $previous);
    }

    // Переопределение метода __toString()
    public function __toString() {
        // Формируем строковое представление ошибки с контекстом
        $contextInfo = json_encode($this->context);
        return "Exception: [{$this->code}] {$this->message} in {$this->file} on line {$this->line}\nContext: {$contextInfo}\n";
    }

    // Метод для записи ошибки в лог-файл
    public function logError() {
        $errorDetails = "Exception: [{$this->code}] {$this->message} in {$this->file} on line {$this->line}\n";
        $errorDetails .= "Context: " . json_encode($this->context) . "\n";
        $errorDetails .= "Stack Trace: " . $this->getTraceAsString() . "\n\n";

        // Логирование ошибки в файл
        file_put_contents($this->logFile, $errorDetails, FILE_APPEND);
    }

    // Переопределение метода для отображения пользовательского интерфейса ошибки
    public function displayUserFriendlyMessage() {
        return "An error occurred. Please contact support with the error code: {$this->code}.";
    }
}

// Исключение для ошибок валидации
class ValidationException extends CustomException {
    // Дополнительное поле для полей, которые не прошли валидацию
    protected $invalidFields;

    public function __construct($message = "", $code = 0, $invalidFields = [], $context = [], $logFile = 'validation_errors.log') {
        $this->invalidFields = $invalidFields;
        parent::__construct($message, $code, null, $context, $logFile);
    }

    // Переопределение __toString() для отображения деталей полей
    public function __toString() {
        $invalidFields = implode(', ', $this->invalidFields);
        return parent::__toString() . "Invalid Fields: {$invalidFields}\n";
    }

    public function getInvalidFields() {
        return $this->invalidFields;
    }
}

// Исключение для ошибок работы с БД
class DatabaseException extends CustomException {
    protected $query;
    protected $dbError;

    public function __construct($message = "", $code = 0, $query = "", $dbError = "", $context = [], $logFile = 'db_errors.log') {
        $this->query = $query;
        $this->dbError = $dbError;
        parent::__construct($message, $code, null, $context, $logFile);
    }

    public function __toString() {
        return parent::__toString() . "SQL Query: {$this->query}\nDB Error: {$this->dbError}\n";
    }
}

// Пример использования исключений
try {
    // Симулируем ошибку валидации
    $data = ['username' => '', 'email' => 'invalidemail'];
    $invalidFields = [];

    // Проверка на пустое поле username
    if (empty($data['username'])) {
        $invalidFields[] = 'username';
    }

    // Проверка на неправильный email
    if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
        $invalidFields[] = 'email';
    }

    if (!empty($invalidFields)) {
        throw new ValidationException(
            "Validation failed for some fields.",
            1001,
            $invalidFields,
            ['data' => $data]
        );
    }

    // Симулируем ошибку работы с БД
    throw new DatabaseException(
        "Database connection failed.",
        2001,
        "SELECT * FROM users WHERE id = 1",
        "SQLSTATE[HY000] [1049] Unknown database 'nonexistent_db'",
        ['server' => 'localhost', 'db' => 'nonexistent_db']
    );

} catch (ValidationException $e) {
    // Логируем ошибку
    $e->logError();

    // Показываем пользовательское сообщение
    echo $e->displayUserFriendlyMessage();

    // Отображаем стек вызова
    echo $e->__toString();
} catch (DatabaseException $e) {
    // Логируем ошибку
    $e->logError();

    // Показываем пользовательское сообщение
    echo $e->displayUserFriendlyMessage();

    // Отображаем стек вызова
    echo $e->__toString();
} catch (CustomException $e) {
    // Логируем ошибку
    $e->logError();

    // Показываем сообщение об ошибке
    echo $e->displayUserFriendlyMessage();

    // Отображаем стек вызова
    echo $e->__toString();
}