Статьи

Исключения PHP

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


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

Допустим, вы хотите вычислить площадь круга по заданному радиусу. Эта функция сделает это:

1
2
3
4
5
function circle_area($radius) {
 
    return pi() * $radius * $radius;
 
}

Это очень просто, однако он не проверяет, является ли радиус допустимым числом. Теперь мы собираемся сделать это и сгенерировать исключение, если радиус является отрицательным числом:

01
02
03
04
05
06
07
08
09
10
function circle_area($radius) {
 
    // radius can’t be negative
    if ($radius < 0) {
        throw new Exception(‘Invalid Radius: ‘ . $radius);
    } else {
        return pi() * $radius * $radius;
    }
 
}

Давайте посмотрим, что произойдет, когда мы позвоним с отрицательным числом:

1
2
3
4
5
6
7
$radius = -2;
 
echo «Circle Radius: $radius => Circle Area: «.
            circle_area($radius) .
 
 
echo «Another line»;

Сценарий вылетает со следующим сообщением:

1
2
3
4
5
6
<br />
<b>Fatal error</b>: Uncaught exception ‘Exception’ with message ‘Invalid Radius: -2’ in C:\wamp\www\test\test.php:19
Stack trace:
#0 C:\wamp\www\test\test.php(7): circle_area(-2)
#1 {main}
  thrown in <b>C:\wamp\www\test\test.php</b> on line <b>19</b><br />

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

На этот раз давайте сделаем массив значений радиуса:

01
02
03
04
05
06
07
08
09
10
11
12
$radius_array = array(2,-2,5,-3);
 
foreach ($radius_array as $radius) {
 
    try {
        echo «Circle Radius: $radius => Circle Area: «.
            circle_area($radius) .
    } catch (Exception $e) {
        echo ‘Caught Exception: ‘, $e->getMessage(), «\n»;
    }
 
}

Теперь мы получаем этот вывод:

1
2
3
4
Circle Radius: 2 => Circle Area: 12.566370614359
Caught Exception: Invalid Radius: -2
Circle Radius: 5 => Circle Area: 78.539816339745
Caught Exception: Invalid Radius: -3

Больше нет ошибок, и скрипт продолжает работать. Вот как вы ловите исключения.



Исключения были в других объектно-ориентированных языках программирования в течение достаточно долгого времени. Впервые он был принят в PHP с версией 5.

По определению исключение «выбрасывается», когда происходит исключительное событие. Это может быть так же просто, как «деление на ноль» или любая другая недопустимая ситуация.

1
throw new Exception(‘Some error message.’);

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

Исключения на самом деле являются объектами, и у вас есть возможность «поймать» их и выполнить определенный код. Это делается с помощью блоков try-catch:

01
02
03
04
05
06
07
08
09
10
11
try {
 
    // some code goes here
    // which might throw an exception
 
} catch (Exception $e) {
 
    // the code here only gets executed
    // if an exception happened in the try block above
 
}

Мы можем заключить любой код в блок try. Следующий блок catch используется для перехвата любого исключения, которое могло быть выброшено из блока try. Блок catch никогда не выполняется, если не было исключений. Кроме того, как только возникает исключение, скрипт немедленно переходит к блоку catch, не выполняя никакого дополнительного кода.

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


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

Например, здесь у нас есть функция, которая выдает исключение. Мы вызываем эту функцию из второй функции. И, наконец, мы вызываем вторую функцию из основного кода, чтобы продемонстрировать этот пузырящийся эффект:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
function bar() {
 
    throw new Exception(‘Message from bar().’);
 
}
 
function foo() {
 
    bar();
 
}
 
 
try {
 
    foo();
 
} catch (Exception $e) {
    echo ‘Caught exception: ‘, $e->getMessage(), «\n»;
}

Поэтому, когда мы вызываем foo (), мы пытаемся перехватить любые возможные исключения. Несмотря на то, что foo () не выдает ни одного, а bar (), он все равно всплывает и попадает вверху, поэтому мы получаем вывод, говорящий: «Поймано исключение: сообщение от bar ()».


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

Давайте посмотрим на пример, который включает в себя несколько файлов и несколько классов.

Во-первых, у нас есть класс User, и мы сохраняем его как user.php:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
class User {
 
    public $name;
    public $email;
 
    public function save() {
 
        $v = new Validator();
 
        $v->validate_email($this->email);
 
        // … save
        echo «User saved.»;
 
        return true;
    }
 
}

Он использует другой класс с именем Validator, который мы поместили в validator.php:

01
02
03
04
05
06
07
08
09
10
11
class Validator {
 
    public function validate_email($email) {
 
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception(‘Email is invalid’);
        }
 
    }
 
}

Из нашего основного кода мы собираемся создать новый объект User, установить имя и значения электронной почты. Как только мы вызовем метод save (), он будет использовать класс Validator для проверки формата электронной почты, который может вернуть исключение:

1
2
3
4
5
6
7
8
9
include(‘user.php’);
include(‘validator.php’);
 
 
$u = new User();
$u->name = ‘foo’;
$u->email = ‘$!%#$%#*’;
 
$u->save();

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
include(‘user.php’);
include(‘validator.php’);
 
try {
 
    $u = new User();
    $u->name = ‘foo’;
    $u->email = ‘$!%#$%#*’;
 
    $u->save();
 
} catch (Exception $e) {
 
    echo «Message: » .
    echo «File: » .
    echo «Line: » .
    echo «Trace: \n» .
 
}

Код выше производит этот вывод:

01
02
03
04
05
06
07
08
09
10
Message: Email is invalid
 
File: C:\wamp\www\test\validator.php
 
Line: 7
 
Trace:
#0 C:\wamp\www\test\user.php(11): Validator->validate_email(‘$!%#$%#*’)
#1 C:\wamp\www\test\test.php(12): User->save()
#2 {main}

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

Структура класса Exception по умолчанию показана в руководстве по PHP , где вы можете увидеть все методы и данные, с которыми он поставляется:



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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// to be used for database issues
class DatabaseException extends Exception {
 
    // you may add any custom methods
    public function log() {
 
        // log this error somewhere
        // …
    }
}
 
// to be used for file system issues
class FileException extends Exception {
 
    // …
 
}

Мы только что создали два новых типа исключений. И они могут иметь собственные методы.

Когда мы ловим исключение, мы можем отобразить фиксированное сообщение и вызвать пользовательские методы внутри:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function foo() {
 
    // …
    // something wrong happened with the database
    throw new DatabaseException();
 
}
 
 
try {
 
    // put all your code here
    // …
 
    foo();
 
} catch (FileException $e) {
 
    die («We seem to be having file system issues.
        We are sorry for the inconvenience.»);
 
} catch (DatabaseException $e) {
 
    // calling our new method
    $e->log();
 
    // exit with a message
    die («We seem to be having database issues.
        We are sorry for the inconvenience.»);
 
} catch (Exception $e) {
 
    echo ‘Caught exception: ‘.
 
}

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

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

Обратите внимание, что блок catch с классом Exception по умолчанию должен стоять последним, поскольку наши новые дочерние классы также по-прежнему считаются этим классом. Например, «DatabaseException» также считается «Exception», поэтому его можно поймать там, если порядок обратный.


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

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

Для этого мы будем использовать функцию set_exception_handler () :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
set_exception_handler(‘exception_handler’);
 
function exception_handler($e) {
 
    // public message
    echo «Something went wrong.\n»;
 
    // semi-hidden message
    echo «<!— Uncaught exception: » .
 
}
 
 
throw new Exception(‘Hello.’);
throw new Exception(‘World.’);

Первая строка инструктирует PHP вызывать данную функцию, когда происходит исключение и оно не перехватывается. Это вывод:

1
2
Something went wrong.
<!— Uncaught exception: Hello.

Как видите, скрипт прервался после первого исключения и не выполнил второе. Это ожидаемое поведение неисследованных исключений.

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


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MysqlException extends Exception {
 
    // path to the log file
    private $log_file = ‘mysql_errors.txt’;
 
 
    public function __construct() {
 
        $code = mysql_errno();
        $message = mysql_error();
 
        // open the log file for appending
        if ($fp = fopen($this->log_file,’a’)) {
 
            // construct the log message
            $log_msg = date(«[Ymd H:i:s]») .
                » Code: $code — » .
                » Message: $message\n»;
 
            fwrite($fp, $log_msg);
 
            fclose($fp);
        }
 
        // call parent constructor
        parent::__construct($message, $code);
    }
 
}

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

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

Например, давайте попробуем подключиться к MySQL без предоставления информации о пользователе / ​​пароле:

01
02
03
04
05
06
07
08
09
10
11
12
13
try {
 
    // attempt to connect
    if (!@mysql_connect()) {
        throw new MysqlException;
    }
 
} catch (MysqlException $e) {
 
    die («We seem to be having database issues.
        We are sorry for the inconvenience.»);
 
}

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

Класс MysqlException автоматически регистрирует ошибки. Когда вы откроете файл журнала, вы найдете следующую строку:

1
[2010-05-05 21:41:23] Code: 1045 — Message: Access denied for user ‘SYSTEM’@’localhost’ (using password: NO)

Давайте добавим больше кода в наш пример, а также предоставим правильную информацию для входа:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
 
    // connection should work fine
    if (!@mysql_connect(‘localhost’,’root’,»)) {
        throw new MysqlException;
    }
 
    // select a database (which may not exist)
    if (!mysql_select_db(‘my_db’)) {
        throw new MysqlException;
    }
 
    // attempt a query (which may have a syntax error)
    if (!$result = mysql_query(«INSERT INTO foo SET bar = ’42 «)) {
        throw new MysqlException;
    }
 
 
} catch (MysqlException $e) {
 
    die («We seem to be having database issues.
        We are sorry for the inconvenience.»);
 
}

Если соединение с базой данных успешно, но база данных с именем ‘my_db’ отсутствует, вы найдете это в журналах:

1
[2010-05-05 21:55:44] Code: 1049 — Message: Unknown database ‘my_db’

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

1
[2010-05-05 21:58:26] Code: 1064 — Message: You have an error in your SQL syntax;

Мы можем сделать приведенный выше пример кода еще чище, написав собственный класс базы данных, который обрабатывает выброс исключений. На этот раз я собираюсь использовать некоторые «волшебные» возможности PHP для создания этого класса.

В конце мы хотим, чтобы наш основной код выглядел так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
try {
 
    Database::connect(‘localhost’,’root’,»);
 
    Database::select_db(‘test’);
 
    $result = Database::query(«INSERT INTO foo SET bar = ’42 «);
 
} catch (MysqlException $e) {
 
    die («We seem to be having database issues.
        We are sorry for the inconvenience.»);
 
}

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

И вот магический класс базы данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Database {
 
    // any static function call invokes this
    public static function __callStatic($name, $args) {
 
        // function to be called
        $function = ‘mysql_’ .
 
        // does the function exist?
        if (!function_exists($function)) {
            // throw a regular exception
            throw new Exception(«Invalid mysql function: $function.»);
        }
 
        // call the mysql function
        $ret = @call_user_func_array($function , $args);
 
        // they return FALSE on errors
        if ($ret === FALSE) {
            // throw db exception
            throw new MysqlException;
        }
 
        // return the returned value
        return $ret;
    }
 
}

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

Таким образом, когда мы вызываем Database :: connect (), этот код автоматически вызывает mysql_connect (), передает аргументы, проверяет ошибки, при необходимости выдает исключения и возвращает возвращаемое значение из вызова функции. Это в основном действует как посредник, и обрабатывает грязную работу.


Надеюсь, вам понравился этот урок и выучили его. Теперь вы должны лучше понять эту тему. Попробуйте и посмотрите, сможете ли вы использовать исключения PHP в вашем следующем проекте. Увидимся в следующий раз!