ООП в PHP: Исключения

Под исключительной ситуацией понимают такую ситуацию возникшею в ходе выполнения кода, когда неясно, что делать дальше или дальнейшее выполнение кода бессмысленно. К примеру, скрипту не удалось установить соединение с базой данных, тогда добавление или выборка данных из неё заведомо бессмысленна. В php в подобных ситуациях часто нативная функция просто возвращает false и выполнение заведомо неработающего кода продолжается. Чтобы избежать подобной ситуации можно проверять значение возвращаемое функцией и, в случае если оно говорит о произошедшей ошибке, выводит предупреждение. Такой подход имеет некоторые недостатки. Во-первых, для изменения действий выполняемых при ошибке пользователю вашего кода придётся редактировать ваш код. Намного лучше было бы позволить ему самому определять механизм обработки этой ошибки. Во-вторых, возвращённое функцией значение не всегда может быть верно расценено, или вовсе остаться непроверяемым, тогда в случае ошибки придётся тратить время на выяснение её причин. В-третьих, не всегда верно стремиться обработать любую проблему прямо в методе или в функции, ведь в таком случае валидация параметров и обработка ошибок может загромоздить изначально достаточно простой код. Намного лучше каким-нибудь образом сообщить пользователю вашего кода, что что-то пошло не так, позволив ему самому решать, что делать в таком случае.

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

Исключения

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

Конструктор класса Exception может принимать два необязательных параметра: строка, содержащие сообщение об ошибке и её код. Класс Exception так же содержит методы, помогающие установить причину возникшей ошибки.

  • getMessage – возвращает строку, которая была передана конструктору и содержит сообщение об ошибке.
  • getCode – возвращает код ошибки (тип int), который был передан конструктору.
  • getFile – возвращает имя файла в котором было сгенерировано исключение.
  • getLine – возвращает номер строки в которой было сгенерировано исключение.
  • getTrace – возвращает многомерный массив, содержащий последовательность методов, вызов которых привёл к генерированию исключения. Так же содержит параметры, которые были переданы этим методам и номера строк, где осуществлялись вызовы.
  • getTraceAsString – возвращает строковую версию данных, которые возвращает метод getTrace.
  • __toString – магический метод, который вызывается, когда экземпляр класса Exception приводится к строке.

Генерация исключений

Для генерации исключения используется ключевое слово throw и экземпляр класса Exception, который очень часто создаётся прямо после инструкции throw. С английского throw переводится как «бросать», что очень точно описывает поведение этого оператора, который может генерировать (бросать) исключения, предоставляя коду, который вызвал метод, в котором расположен этот оператор, самому обрабатывать исключение.

Давайте рассмотрим класс для получения некоторых конфигурационных данных из ini-файла.

class Config {
  private $data;
  private $filePath; 
 
  public function __construct($filePath) {    
    $this->data = parse_ini_file($filePath, true);
    $this->filePath = $filePath;
  }
 
  public function getHostName() {
    return $this->data['hostName'];
  }
 
  public function getUserName() {
    return $this->data['userName'];
  }
 
  public function getPassword() {
    return $this->data['password'];
  }
}

Код этого класса сильно упрощён и в нём не предусмотрена обработка ошибок. Например, при отсутствии ini-файла, неправильном его форматировании или отсутствии в файле некоторых данных, этот код не будет работать корректно.

Файл с которым будет работать этот класс может выглядеть примерно так:

userName = «true-coder»
password = «veryLongAndDifficult»
hostName = «localhost»

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

Внесём некоторые изменения в конструктор класса Config.

public function __construct($filePath) {
  if (!@file_exists($filePath)) {
    throw new Exception("File $filePath does not exists");
  }
 
  $data = parse_ini_file($filePath, true);
 
  if ($data === false) {
    throw new Exception("Incorrect file format");
  }
 
  foreach (array ('hostName', 'userName', 'password') as $el) {
    if (!isset($data[$el])) {
      throw new Exception("$el must be defined");
    }
  }
 
  $this->data = $data;
}

Теперь при создании экземпляра этого класса может быть сгенерировано исключение.

Например, при попытке обратится к несуществующему файлу.

//Uncaught exception 'Exception' with message 'File NotExistingFile.ini does not exists' in …
$conf = new Config('NotExistingFile.ini');

Или, если ini-файл содержит синтаксические ошибки (здесь в первой паре ключ-значение пропущено равно)

userName «true-coder»
password = «veryLongAndDifficult»
hostName = «localhost»

//Uncaught exception 'Exception' with message 'Incorrect file format' in …
$conf = new Config('config.ini');

Или, если в ini-файле недостаёт необходимых данных. (Здесь пропущен hostName)

userName = «true-coder»
password = «veryLongAndDifficult»

//Uncaught exception 'Exception' with message 'hostName must be defined' in …
$conf = new Config('config.ini');

Обработка исключений

Как я уже говорил, код, вызывающий метод, который может бросить исключение, должен сам его обрабатывать. Обработка исключения производится при помощи операторов try и catch. Блок кода, который может бросить исключение, располагается после try. Блок кода, который обрабатывает ошибку, располагается после ключевого слова catch. В переводе с английского try означает «пытаться», что очень точно отражает суть этого оператора, ведь мы пытаемся выполнить блок кода после него, а если не получается то выполняется блок кода после catch. Catch переводится как «ловить». Этот оператор «ловит» сгенерированное исключение.

Обработка исключений в нашем случае упрощённо будет выглядеть примерно так:

try {
  $conf = new Config('config.ini');
} catch (Exception $e) {
  echo $e->getMessage();
}

Заметьте, что оператор catch внешне напоминает объявление метода с уточнением типа его параметра. Когда генерируется исключение, управление передаётся оператору catch, при этом в качестве параметра ему передаётся объект типа Exception.

Создание подклассов класса Exception

От встроенного класса Exception можно унаследовать классы для своих собственных исключений. Делать это можно для того чтобы расширить его функциональность или создать свой собственный тип ошибок. Создание своих собственных типов ошибок нужно для того, чтобы была возможность по-разному обрабатывать разные исключения. Для этого существует возможность писать несколько операторов catch. Какой именно из них вызовется, будет зависеть от типа сгенерированного исключения, от типа, который уточнён в аргументе и от порядка, в котором расположены операторы catch.

Давайте определим простые классы-наследники класса Exception. Назовем их FileNotExistsException, ParseIniException и InvalidDataException.

class FileNotExistsException extends Exception {
  public function __construct($filePath) {
    $this->message = "File $filePath doesn not exists";
  }
}
 
class ParseIniException extends Exception {}
 
class InvalidDataException extends Exception {
  public function __construct($data) {
    $this->message = "$data must be defined";
  }
}

Немного изменим конструктор класса Config так, чтобы он бросал разные типы исключений в зависимости от возникшей исключительной ситуации.

public function __construct($filePath) {
  if (!@file_exists($filePath)) {
    throw new FileNotExistsException($filePath);
  }
 
  $data = parse_ini_file($filePath, true);
 
  if ($data === false) {
    throw new ParseIniException();
  }
 
  foreach (array ('hostName', 'userName', 'password') as $el) {
    if (!isset($data[$el])) {
      throw new InvalidDataException($el);
    }
  }
 
  $this->data = $data;
}

Теперь каждый тип исключения, брошенного при создании экземпляра класса Config можно обработать отдельно.

try {
  $conf = new Config('config.ini');
} catch (FileNotExistsException $e) {
  // Обработка исключения типа FileNotExistsException
} catch (ParseIniException $e) {
  // Обработка исключения типа ParseIniException
} catch(InvalidDataException $e) {
  // Обработка исключения типа InvalidDataException
}

Стоить заметить, что оператор catch с именем класса-предка в качестве уточнения типа параметра, сработает так же, и если в блоке try было сгенерировано исключение, которое является экземпляром класса-наследника.

try {
  $conf = new Config('config.ini');
} catch (Exception $e) {
  // Сработает, какое исключение не было бы сгенерировано
}

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

try {
  $conf = new Config('config.ini');
} catch (Exception $e) {
  // Сработает, какое исключение не было бы сгенерировано
}  catch (FileNotExistsException $e) {
  // Этот блок кода не сработает никогда
}

try {
  $conf = new Config('config.ini');
} catch (FileNotExistsException $e) {
  // Обработка исключения типа FileNotExistsException
} catch (ParseIniException $e) {
  // Обработка исключения типа ParseIniException
} catch(InvalidDataException $e) {
  // Обработка исключения типа InvalidDataException
} catch(Exception $e) {
  // Этот блок кода не сработает никогда, поскольку все типы исключений ловятся выше
}

На этом пока всё. Как всегда успехов вам!

  • Максим

    Спасибо, очень познавательно. Я где-то читал, что с версии php 5.3 работа с исключениями несколько изменилась. Верно ли это?

  • Максим, исключения появились в 5-ой версии php и после этого работа с ними значительно не менялась.

  • Отличная статья ,как раз то что я и искал. Спасибо!