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

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

Что представляют собой исключения?

Исключения в PHP - это специальные условия, появляющиеся, обычно, при возникновении какой-либо ошибки в программе. Их присутствие говорит о том, что в предполагаемом ходе событий происходит что-то не так. Как правило, специалисты предпочитают использовать в подобных ситуациях соответствующие инструкции, дабы избежать бесконтрольное завершение процесса.

Как исключения PHP работают на практике?

С исключениями можно осуществить два действия: бросить (thrown) и поймать (caught). Если исключение считается брошенным, то это означает только одно: в процессе работы вашей программы произошло что-то, выходящее за рамки нормы. В данном случае вам нужно будет использовать другую функцию. Подхватить исключения у вас получится только в специальной функции, которая, в свою очередь, оповести всю программу о том, что она готова к обработке исключения.

Если вам трудно представить исключения в действии, то рекомендуем вам провести аналогию с игрой в бейсбол.

  1. Представьте, что ваш браузер – это питчер, бросающий мяч кетчеру.
  2. Игрок, отбивающий мяч, старается попасть по нему, но у него это не выходит сделать. Как результат, он получает страйк. Если же игрок выполняет свою задачу, то у полевого игрока появляется возможность поймать мяч.
  3. Полевой игрок ловит мяч, и после этого у него есть два пути: бросить его другому полевому игроку (при недостаточном расстоянии от отбивающего) или вывести отбивающего из игры, бросив в него мяч.

Бросок питчера кэтчеру в нашем случае можно сравнить с нормальным течением процесса. Однако если отбивающему игроку удается попасть по мячу, то наступает то самое исключение.

Когда же полевой игрок ловит мяч, то такое действие очень схоже с выполнением функции try-catch, способной обрабатывать исключения. Данная функция может не только сама выполнить все необходимые действия, но и перебросить выявленное исключение иной функции.

Как правильно использовать исключения?

Если вы хотите научиться обрабатывать исключения PHP, то вам следует уяснить одну вещь: исключения считаются неотъемлемой частью стандартной библиотеки PHP (SPL), а также входят в ядро PHP при условии, что они принадлежат 5 версии и выше. Вам же нужно только понять суть их применения.

Исключения можно реализовать таким же образом, как и любой другой объект:

$exception = new Exception();
throw $exception;

Исходя из этого, у исключений имеются специальные методы, которые при необходимости можно вызвать. Последние значительно облегчают процесс формирования реакции на конкретное исключение. К примеру, такая функция, как getMessage() дает возможность получить сообщение об ошибке. А его, в свою очередь, можно вывести в браузер или записать в журнал.

Ниже приведем список методов PHP исключений, которые позволяют получать сообщение исключения – getMessage();

  1. Возвратить числовой код, представленный исключением – getCode();
  2. Получить файл, внутри которого произошло исключение – getFile();
  3. Возвратить номер строки в том файле, где возникло исключение – getLine();
  4. Возвратить массив backtrace() до момента появления исключения – getTrace();
  5. Получить исключение, возникшее перед текущим, если оно, конечно, было – getPrevious();
  6. Возвратить массив backtrace() исключения в форме строки – getTraceAsString();
  7. Возвратить в виде строки все исключения – toString(). Данная функция может быть перезаписана иначе.

Для большей наглядности вышеуказанных методов, создадим класс User, через который будет осуществляться начальное управление записями пользователей:

<?php
class User
{
protected $_user_id;
protected $_user_email;
protected $_user_password;

public function __construct($user_id)
{
$user_record = self::_getUserRecord($user_id);
$this->_user_id = $user_record['id'];
$this->_user_email = $user_record['email'];
$this->_user_password = $user_record['password'];
}

//Данные функции сформируем позже
public function __get($value) {}
public function __set($name, $value) {}

private static function _getUserRecord($user_id)
{
//Для урока будем использовать модель записи для представления передачи данных в базу
$user_record = array();
switch($user_id) {
case 1:
$user_record['id'] = 1;
$user_record['email'] = 'nikko@example.com';
$user_record['password'] = 'i like croissants';
break;

case 2:
$user_record['id'] = 2;
$user_record['email'] = 'john@example.com';
$user_record['password'] = 'me too!';
break;

case 'error':
//имитируем неизвестное исключение от какой-нибудь используемой библиотеки SQL:
throw new Exception('Ошибка библиотеки SQL!');
break;
}

return $user_record;
}
}
?>

В приведенном примере присутствует сразу несколько мест, в которых может возникнуть исключение. Сперва $user_id в функции _getUserRecord() обязан иметь тип integer. Если же процесс пойдет по другому пути, то следующим шагом станет генерирование исключения:

...
...
...
private static function _getUserRecord($user_id)
{
$user_id = self::_validateUserId($user_id);
...
...
...
}

private static function _validateUserId($user_id)
{
if( !is_numeric($user_id) && $user_id != 'error' ) {
throw new Exception('Ой! Здесь что-то не так с идентификатором пользователя');
}
return $user_id;
}

После этого создаем пользователя с неправильным идентификатором:

//Сперва создаем пользователя под номером один
$user = new User(1);
//Далее намеренно делаем ошибку
$user2 = new User('not numeric');

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

try {
//Сначала создаем пользователя с номером один
$user = new User(1);

//Затем намеренно делаем ошибку
$user2 = new User('not numeric');
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: {$e->getMessage()}";
}

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

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

  1. Создается новый объект User в строке $user2 = new User('not numeric');
  2. Выполняется функция __construct() класса User;
  3. Совершается переход к функции _getUserRecord() класса User;
  4. Идет проверка $user_id посредством функции _validateUserId;
  5. В том случае, если $user_id не признан числовым значением, объект исключения выбрасывается.

Кроме того, можно выполнить функцию getTraceAsString() с целью отследить дальнейшие исключения:

...
...
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: {$e->getMessage()}";
echo '<pre>';
echo $e->getTraceAsString();
echo '</pre>';
}

Если исключение возникло глубже вызова $user2 = new User('not numeric'), то оно оказывается наверху до тех пор, пока блок try-catch не проведет его обработку.

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

Однако и это можно изменить в момент создания исключения:

...
...
...
if( !is_numeric($user_id) ) {
throw new Exception('Ой! Здесь что-то не так с идентификатором пользователя', UserErrors::INVALIDID);
}
...
...
...

Обязательно нужно создать класс UserErrors:

class UserErrors
{
const INVALIDID = 10001;
const INVALIDEMAIL = 10002;
const INVALIDPW = 10003;
const DOESNOTEXIST = 10004;
const NOTASETTING = 10005;
const UNEXPECTEDERROR = 10006;

public static function getErrorMessage($code)
{
switch($code) {
case self::INVALIDID:
return 'Ой! Здесь что-то не так с идентификатором пользователя';
break;

case self::INVALIDEMAIL:
return 'Адрес email неправильный!';
break;

case self::INVALIDPW:
return 'Пароль меньше 4 символов!';
break;

case self::DOESNOTEXIST:
return 'Пользователя не существует!';
break;

case self::NOTASETTING:
return 'Таких установок нет!';
break;

case self::UNEXPECTEDERROR:
default:
return 'Какая-то ошибка!';
break;
}
}
}

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

...
...
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: #{$e->getCode()}";
}

Также стоит применять и другие методы исключения для отслеживания. К примеру, можно создать класс Logger:

class Logger
{
public static function newMessage(
Exception $exception,
$clear = false,
$error_file = 'exceptions_log.html'
) {
$message = $exception->getMessage();
$code = $exception->getCode();
$file = $exception->getFile();
$line = $exception->getLine();
$trace = $exception->getTraceAsString();
$date = date('M d, Y h:iA');

$log_message = "<h3>Информация об исключении:</h3>
<p>
<strong>Дата:</strong> {$date}
</p>

<p>
<strong>Сообщение:</strong> {$message}
</p>

<p>
<strong>Код:</strong> {$code}
</p>

<p>
<strong>Файл:</strong> {$file}
</p>

<p>
<strong>Строка:</strong> {$line}
</p>

<h3>Stack trace:</h3>
<pre>{$trace}
</pre>
<br />
<hr /><br /><br />";

if( is_file($error_file) === false ) {
file_put_contents($error_file, '');
}

if( $clear ) {
$content = '';
} else {
$content = file_get_contents($error_file);
}

file_put_contents($error_file, $log_message . $content);
}
}

После этого проводим модификацию обработки исключения для того, чтобы можно было применить класс Logger:

...
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: #{$e->getCode()}";
Logger::newMessage($e);
}

Теперь мы добились того, чтобы во время выполнения программы выводился код исключения. В файле exceptions_log.html можно будет посмотреть всю информацию, связанную с местом расположения исключения.

Расширенные исключения

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

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

После этого следует создать пользовательское исключение UserException для вышеуказанного класса, который мы уже строили чуть выше:

class UserException extends Exception
{
public function __construct($error_code)
{
parent::__construct(UserErrors::getErrorMessage($error_code), $error_code);
Logger::newMessage($this);
}
}

В итоге мы переписали функцию __construct, которая отвечает за реализацию конструктора. Так как созданный нами класс расширяет класс Exception, то в данном случае уместно применить его конструктор посредством директивы parent.

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

Теперь обновляем код, применимый для нового класса UserException:

private static function _validateUserId($user_id)
{
if( !is_numeric($user_id) && $user_id != 'error' ) {
throw new UserException(UserErrors::INVALIDID);
}
return $user_id;
}

Так как запись в журнал производиться автоматически, операцию с комментарием «Полевой игрок поймал исключение» можно благополучно опустить внутри блока try-catch:

...
...
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: #{$e->getCode()}";
//Logger::newMessage($e);
}

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

Класс User:

...
...
...
public function __get($value) {
$value = "_{$value}";
if( !isset($this->$value) ) {
throw new UserException(UserErrors::NOTASETTING);
}
return $this->$value;
}

public function __set($name, $value) {
switch($name) {
case 'user_id':
$user_id = self::_validateUserId($value);
$this->_user_id = $user_id;
break;

case 'user_email':
$user_email = self::_validateUserEmail($value);
$this->_user_email = $user_email;
break;

case 'user_password':
$user_password = self::_validateUserPassword($value);
$this->_user_password = $user_password;
break;

default:
throw new UserException(UserErrors::NOTASETTING);
break;
}
return true;
}

private static function _getUserRecord($user_id)
{
...
...
...
switch($user_id) {
...
...
...
default:
throw new UserException(UserErrors::DOESNOTEXIST);
break;
}
}

Теперь пытаемся сгенерировать три исключения:

$user = new User(1);
try {
$user->user_email = 'invalid email';
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: #{$e->getCode()}<br />";
}

try {
echo $user->setting_that_doesnt_exist;
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: #{$e->getCode()}<br />";
}

try {
//id пользователя не существует
$user = new User(3);
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение: #{$e->getCode()}<br />";
}

Какие же выгоды ждут нас при использовании расширенного исключения?

Во-первых, мы можем создать пользовательский конструктор.

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

В-третьих, организовать исключение возможно в зависимости от конкретного типа объекта. Ну, и в четвертых, мы можем применить блок множественных перехватов во время обработки возникших исключений в зависимости от их типа.

Как использовать блок множественных перехватов?

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

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

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

try {
$user = new User(1);
$user->user_password = '123';
} catch( UserException $e ) {
echo "Полевой игрок поймал исключение UserException: #{$e->getCode()}<br />";
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение Exception:: #{$e->getCode()}<br />";
}

try {
//Использование аргумента 'error' в классе User имитирует
//ошибку в библиотеке SQL
$user = new User('error');
} catch( UserException $e ) {
echo "Полевой игрок поймал исключение UserException: #{$e->getCode()}<br />";
} catch( Exception $e ) {
echo "Полевой игрок поймал исключение Exception:: #{$e->getCode()}<br />";
}

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

В целом же исключения PHP проводят централизованную и простую обработку разнообразных ошибок во время выполнения программного кода.

--------- по материалам net.tutsplus.com ---------

Пожаловаться Подписаться
1 ответ
xduocore

Очень полезная статья. Спасибо Огромное за подробное разьяснение и наглядные примеры. Кстати ваши уроки по созданию сайта впечатляют, вы не скупитесь на время, а главное всё чётко и ясно рассказываете. Спасибо за неоценимый и при этом бесплатный вклад в развитие новичков. Успехов вам!

авторизуйтесь чтобы ответить