Table of Contents
- Extending Exceptions
PHP has an exception model similar to that of other programming
languages. An exception can be throw
n, and caught («catch
ed») within
PHP. Code may be surrounded in a try
block, to facilitate the catching
of potential exceptions. Each try
must have at least one corresponding
catch
or finally
block.
If an exception is thrown and its current function scope has no catch
block, the exception will «bubble up» the call stack to the calling
function until it finds a matching catch
block. All finally
blocks it encounters
along the way will be executed. If the call stack is unwound all the way to the
global scope without encountering a matching catch
block, the program will
terminate with a fatal error unless a global exception handler has been set.
The thrown object must be an instanceof
Throwable.
Trying to throw an object that is not will result in a PHP Fatal Error.
As of PHP 8.0.0, the throw
keyword is an expression and may be used in any expression
context. In prior versions it was a statement and was required to be on its own line.
catch
A catch
block defines how to respond to a thrown exception. A catch
block defines one or more types of exception or error it can handle, and
optionally a variable to which to assign the exception. (The variable was
required prior to PHP 8.0.0.) The first catch
block a thrown exception
or error encounters that matches the type of the thrown object will handle
the object.
Multiple catch
blocks can be used to catch different classes of
exceptions. Normal execution (when no exception is thrown within the try
block) will continue after that last catch
block defined in sequence.
Exceptions can be throw
n (or re-thrown) within a catch
block. If not,
execution will continue after the catch
block that was triggered.
When an exception is thrown, code following the statement will not be
executed, and PHP will attempt to find the first matching catch
block.
If an exception is not caught, a PHP Fatal Error will be issued with an
«Uncaught Exception ...
» message, unless a handler has
been defined with set_exception_handler().
As of PHP 7.1.0, a catch
block may specify multiple exceptions
using the pipe (|
) character. This is useful for when
different exceptions from different class hierarchies are handled the
same.
As of PHP 8.0.0, the variable name for a caught exception is optional.
If not specified, the catch
block will still execute but will not
have access to the thrown object.
finally
A finally
block may also be specified after or
instead of catch
blocks. Code within the finally
block will always be
executed after the try
and catch
blocks, regardless of whether an
exception has been thrown, and before normal execution resumes.
One notable interaction is between the finally
block and a return
statement.
If a return
statement is encountered inside either the try
or the catch
blocks,
the finally
block will still be executed. Moreover, the return
statement is
evaluated when encountered, but the result will be returned after the finally
block
is executed. Additionally, if the finally
block also contains a return
statement,
the value from the finally
block is returned.
Global exception handler
If an exception is allowed to bubble up to the global scope, it may be caught
by a global exception handler if set. The set_exception_handler()
function can set a function that will be called in place of a catch
block if no
other block is invoked. The effect is essentially the same as if the entire program
were wrapped in a try
—catch
block with that function as the catch
.
Notes
Note:
Internal PHP functions mainly use
Error reporting, only modern
Object-oriented
extensions use exceptions. However, errors can be easily translated to
exceptions with ErrorException.
This technique only works with non-fatal errors, however.Example #1 Converting error reporting to exceptions
<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
throw new ErrorException($message, 0, $severity, $filename, $lineno);
}set_error_handler('exceptions_error_handler');
?>
Examples
Example #2 Throwing an Exception
<?php
function inverse($x) {
if (!$x) {
throw new Exception('Division by zero.');
}
return 1/$x;
}
try {
echo
inverse(5) . "n";
echo inverse(0) . "n";
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "n";
}// Continue execution
echo "Hello Worldn";
?>
The above example will output:
0.2 Caught exception: Division by zero. Hello World
Example #3 Exception handling with a finally
block
<?php
function inverse($x) {
if (!$x) {
throw new Exception('Division by zero.');
}
return 1/$x;
}
try {
echo
inverse(5) . "n";
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "n";
} finally {
echo "First finally.n";
}
try {
echo
inverse(0) . "n";
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "n";
} finally {
echo "Second finally.n";
}// Continue execution
echo "Hello Worldn";
?>
The above example will output:
0.2 First finally. Caught exception: Division by zero. Second finally. Hello World
Example #4 Interaction between the finally
block and return
<?phpfunction test() {
try {
throw new Exception('foo');
} catch (Exception $e) {
return 'catch';
} finally {
return 'finally';
}
}
echo
test();
?>
The above example will output:
Example #5 Nested Exception
<?phpclass MyException extends Exception { }
class
Test {
public function testing() {
try {
try {
throw new MyException('foo!');
} catch (MyException $e) {
// rethrow it
throw $e;
}
} catch (Exception $e) {
var_dump($e->getMessage());
}
}
}$foo = new Test;
$foo->testing();?>
The above example will output:
Example #6 Multi catch exception handling
<?phpclass MyException extends Exception { }
class
MyOtherException extends Exception { }
class
Test {
public function testing() {
try {
throw new MyException();
} catch (MyException | MyOtherException $e) {
var_dump(get_class($e));
}
}
}$foo = new Test;
$foo->testing();?>
The above example will output:
Example #7 Omitting the caught variable
Only permitted in PHP 8.0.0 and later.
<?phpclass SpecificException extends Exception {}
function
test() {
throw new SpecificException('Oopsie');
}
try {
test();
} catch (SpecificException) {
print "A SpecificException was thrown, but we don't care about the details.";
}
?>
Example #8 Throw as an expression
Only permitted in PHP 8.0.0 and later.
<?phpfunction test() {
do_something_risky() or throw new Exception('It did not work');
}
try {
test();
} catch (Exception $e) {
print $e->getMessage();
}
?>
ask at nilpo dot com ¶
14 years ago
If you intend on creating a lot of custom exceptions, you may find this code useful. I've created an interface and an abstract exception class that ensures that all parts of the built-in Exception class are preserved in child classes. It also properly pushes all information back to the parent constructor ensuring that nothing is lost. This allows you to quickly create new exceptions on the fly. It also overrides the default __toString method with a more thorough one.
<?php
interface IException
{
/* Protected methods inherited from Exception class */
public function getMessage(); // Exception message
public function getCode(); // User-defined Exception code
public function getFile(); // Source filename
public function getLine(); // Source line
public function getTrace(); // An array of the backtrace()
public function getTraceAsString(); // Formated string of trace
/* Overrideable methods inherited from Exception class */
public function __toString(); // formated string for display
public function __construct($message = null, $code = 0);
}
abstract class
CustomException extends Exception implements IException
{
protected $message = 'Unknown exception'; // Exception message
private $string; // Unknown
protected $code = 0; // User-defined exception code
protected $file; // Source filename of exception
protected $line; // Source line of exception
private $trace; // Unknownpublic function __construct($message = null, $code = 0)
{
if (!$message) {
throw new $this('Unknown '. get_class($this));
}
parent::__construct($message, $code);
}
public function
__toString()
{
return get_class($this) . " '{$this->message}' in {$this->file}({$this->line})n"
. "{$this->getTraceAsString()}";
}
}
?>
Now you can create new exceptions in one line:
<?php
class TestException extends CustomException {}
?>
Here's a test that shows that all information is properly preserved throughout the backtrace.
<?php
function exceptionTest()
{
try {
throw new TestException();
}
catch (TestException $e) {
echo "Caught TestException ('{$e->getMessage()}')n{$e}n";
}
catch (Exception $e) {
echo "Caught Exception ('{$e->getMessage()}')n{$e}n";
}
}
echo
'<pre>' . exceptionTest() . '</pre>';
?>
Here's a sample output:
Caught TestException ('Unknown TestException')
TestException 'Unknown TestException' in C:xampphtdocsCustomExceptionCustomException.php(31)
#0 C:xampphtdocsCustomExceptionExceptionTest.php(19): CustomException->__construct()
#1 C:xampphtdocsCustomExceptionExceptionTest.php(43): exceptionTest()
#2 {main}
Johan ¶
12 years ago
Custom error handling on entire pages can avoid half rendered pages for the users:
<?php
ob_start();
try {
/*contains all page logic
and throws error if needed*/
...
} catch (Exception $e) {
ob_end_clean();
displayErrorPage($e->getMessage());
}
?>
christof+php[AT]insypro.com ¶
5 years ago
In case your E_WARNING type of errors aren't catchable with try/catch you can change them to another type of error like this:
<?php
set_error_handler(function($errno, $errstr, $errfile, $errline){
if($errno === E_WARNING){
// make it more serious than a warning so it can be caught
trigger_error($errstr, E_ERROR);
return true;
} else {
// fallback to default php error handler
return false;
}
});
try {
// code that might result in a E_WARNING
} catch(Exception $e){
// code to handle the E_WARNING (it's actually changed to E_ERROR at this point)
} finally {
restore_error_handler();
}
?>
lscorionjs at gmail dot com ¶
4 months ago
<?phptry {
$str = 'hi';
throw new Exception();
} catch (Exception) {
var_dump($str);
} finally {
var_dump($str);
}?>
Output:
string(2) "hi"
string(2) "hi"
Shot (Piotr Szotkowski) ¶
14 years ago
‘Normal execution (when no exception is thrown within the try block, *or when a catch matching the thrown exception’s class is not present*) will continue after that last catch block defined in sequence.’
‘If an exception is not caught, a PHP Fatal Error will be issued with an “Uncaught Exception …” message, unless a handler has been defined with set_exception_handler().’
These two sentences seem a bit contradicting about what happens ‘when a catch matching the thrown exception’s class is not present’ (and the second sentence is actually correct).
Simo ¶
8 years ago
#3 is not a good example. inverse("0a") would not be caught since (bool) "0a" returns true, yet 1/"0a" casts the string to integer zero and attempts to perform the calculation.
daviddlowe dot flimm at gmail dot com ¶
5 years ago
Starting in PHP 7, the classes Exception and Error both implement the Throwable interface. This means, if you want to catch both Error instances and Exception instances, you should catch Throwable objects, like this:
<?phptry {
throw new Error( "foobar" );
// or:
// throw new Exception( "foobar" );
}
catch (Throwable $e) {
var_export( $e );
}?>
Edu ¶
9 years ago
The "finally" block can change the exception that has been throw by the catch block.
<?php
try{
try {
throw new Exception("Hello");
} catch(Exception $e) {
echo $e->getMessage()." catch inn";
throw $e;
} finally {
echo $e->getMessage()." finally n";
throw new Exception("Bye");
}
} catch (Exception $e) {
echo $e->getMessage()." catch outn";
}
?>
The output is:
Hello catch in
Hello finally
Bye catch out
mlaopane at gmail dot com ¶
5 years ago
<?php/**
* You can catch exceptions thrown in a deep level function
*/function employee()
{
throw new Exception("I am just an employee !");
}
function
manager()
{
employee();
}
function
boss()
{
try {
manager();
} catch (Exception $e) {
echo $e->getMessage();
}
}boss(); // output: "I am just an employee !"
telefoontoestel at nospam dot org ¶
8 years ago
When using finally keep in mind that when a exit/die statement is used in the catch block it will NOT go through the finally block.
<?php
try {
echo "try block<br />";
throw new Exception("test");
} catch (Exception $ex) {
echo "catch block<br />";
} finally {
echo "finally block<br />";
}// try block
// catch block
// finally block
?>
<?php
try {
echo "try block<br />";
throw new Exception("test");
} catch (Exception $ex) {
echo "catch block<br />";
exit(1);
} finally {
echo "finally block<br />";
}// try block
// catch block
?>
Tom Polomsk ¶
8 years ago
Contrary to the documentation it is possible in PHP 5.5 and higher use only try-finally blocks without any catch block.
Sawsan ¶
11 years ago
the following is an example of a re-thrown exception and the using of getPrevious function:
<?php
$name
= "Name";//check if the name contains only letters, and does not contain the word nametry
{
try
{
if (preg_match('/[^a-z]/i', $name))
{
throw new Exception("$name contains character other than a-z A-Z");
}
if(strpos(strtolower($name), 'name') !== FALSE)
{
throw new Exception("$name contains the word name");
}
echo "The Name is valid";
}
catch(Exception $e)
{
throw new Exception("insert name again",0,$e);
}
}
catch (
Exception $e)
{
if ($e->getPrevious())
{
echo "The Previous Exception is: ".$e->getPrevious()->getMessage()."<br/>";
}
echo "The Exception is: ".$e->getMessage()."<br/>";
}?>
ilia-yats at ukr dot net ¶
5 months ago
Note some undocumented details about exceptions thrown from 'finally' blocks.
When exception is thrown from 'finally' block, it overrides the original not-caught (or re-thrown) exception. So the behavior is similar to 'return': value returned from 'finally' overrides the one returned earlier. And the original exception is automatically appended to the exceptions chain, i.e. becomes 'previous' for the new one. Example:
<?php
try {
try {
throw new Exception('thrown from try');
} finally {
throw new Exception('thrown from finally');
}
} catch(Exception $e) {
echo $e->getMessage();
echo PHP_EOL;
echo $e->getPrevious()->getMessage();
} // will output:
// thrown from finally
// thrown from try
?>
Example with re-throwing:
<?php
try {
try {
throw new Exception('thrown from try');
} catch (Exception $e) {
throw new Exception('thrown from catch');
} finally {
throw new Exception('thrown from finally');
}
} catch(Exception $e) {
echo $e->getMessage();
echo PHP_EOL;
echo $e->getPrevious()->getMessage();
} // will output:
// thrown from finally
// thrown from catch
?>
The same happens even if explicitly pass null as previous exception:
<?php
try {
try {
throw new Exception('thrown from try');
} finally {
throw new Exception('thrown from finally', null, null);
}
} catch(Exception $e) {
echo $e->getMessage();
echo PHP_EOL;
echo $e->getPrevious()->getMessage();
} // will output:
// thrown from finally
// thrown from try
?>
Also it is possible to pass previous exception explicitly, the 'original' one will be still appended to the chain, e.g.:
<?php
try {
try {
throw new Exception('thrown from try');
} finally {
throw new Exception(
'thrown from finally',
null,
new Exception('Explicitly set previous!')
);
}
} catch(Exception $e) {
echo $e->getMessage();
echo PHP_EOL;
echo $e->getPrevious()->getMessage();
echo PHP_EOL;
echo $e->getPrevious()->getPrevious()->getMessage();
} // will output:
// thrown from finally
// Explicitly set previous!
// thrown from try
?>
This seems to be true for versions 5.6-8.2.
Daan ¶
1 year ago
I would like to emphasise that you can not rethrow an Exception inside a catch-block and expect that the next catch-block will handle it.
<?php try {
throw new RuntimeException('error');
} catch (RuntimeException $e) {
throw $e;
} catch (Exception $e) {
// this will not be executed[
}
?>
PHP5 Обработка ошибок
Обработка ошибок по умолчанию в PHP очень проста. Сообщение об ошибке с именем файла, строка число и сообщение, описывающее ошибку, отправляется в браузер.
При создании скриптов и веб-приложений, обработка ошибок, является важной
частью. Если коду не хватает кода проверки ошибок, программа может выглядеть
непрофессионально и Вы можете быть открыты для рисков безопасности.
Учебник содержит несколько из наиболее распространенных методов проверки ошибок в PHP.
Вы узнаете различные методы обработки ошибок:
- Простое заявление
это()
- Пользовательские ошибки и триггеры ошибок
- Отчеты об ошибках
PHP Основная обработка ошибок
В первом примере показан простой скрипт, открывающий текстовый файл: использование функции это()
Пример
<?php
$file=fopen(«welcome.txt»,»r»);
?>
Если файл не существует, Вы можете получить ошибку, как эта:
Внимание: fopen(welcome.txt) [function.fopen]: не удалось открыть поток:
Нет такого файла или каталога в C:webfoldertest.php на линии 2
Чтобы запретить пользователю получать сообщение об ошибке, подобное приведенному примеру выше, мы проверяем
файл, существует ли он до того, как мы попытаемся получить к нему доступ:
Пример
<?php
if(!file_exists(«welcome.txt»)) {
die(«Файл не найден»);
}
else {
$file=fopen(«welcome.txt»,»r»);
}
?>
Теперь, если файл не существует вы получите ошибку, как эта:
Файл не найден
Приведенный ниже код более эффективен, чем предыдущий код, поскольку он использует простой механизм обработки ошибок для остановки сценария после ошибки.
Тем не менее, остановить просто сценарий не всегда правильный путь. Рассмотрим альтернативные функции PHP для обработки ошибок.
PHP Создание пользовательского обработчика ошибок
Создать пользовательский обработчик ошибок довольно просто. Создаем специальную функцию, которая может быть вызвана при возникновении ошибки в PHP.
Эта функция должна быть способна обрабатывать, как минимум два параметра (уровень ошибки и сообщение об ошибке),
но можно принимать до пяти параметров (дополнительно: файл, номер строки и контекст ошибки):
Синтаксис
error_function(error_level,error_message,
error_file,error_line,error_context)
Параметр | Описание |
---|---|
error_level | Необходимо. Указывает уровень отчета об ошибках для пользовательской ошибки. Должно быть числовое значение. См. таблицу ниже для возможных уровней отчета об ошибках |
error_message | Необходимо. Указывает сообщение об ошибке определяемая пользователем |
error_file | Необязательно. Задает имя файла, в котором произошла ошибка |
error_line | Необязательно. Указывает номер строки, в которой произошла ошибка |
error_context | Необязательно. Задает массив, содержащий все переменные и их значения, используемые при возникновении ошибки |
PHP Уровни отчетов об ошибках
Эти уровни отчетов об ошибках, являются различными типами ошибок, для которых может использоваться определяемый пользователем обработчик ошибок:
Значение | Констант | Описание |
---|---|---|
2 | E_WARNING | Неустранимые ошибки выполнения. Выполнение скрипта не останавливается |
8 | E_NOTICE | Уведомления среды выполнения. Сценарий нашел что-то, что могло бы быть ошибкой, но могло бы также произойти при запуске сценария, как обычно |
256 | E_USER_ERROR | Неустранимая ошибка пользователя. Это похоже на набор E_ERROR установленный программистом с помощью функции PHP trigger_error() |
512 | E_USER_WARNING | Неустранимое пользовательское предупреждение. Это похоже на набор E_WARNING установленный программистом с помощью функции PHP trigger_error() |
1024 | E_USER_NOTICE | Автоматическое уведомление пользователя. Это похоже на набор E_NOTICE устанавливается программистом с помощью функции PHP trigger_error() |
4096 | E_RECOVERABLE_ERROR | Перехватываемая неустранимая ошибка. Это похоже на набор E_ERROR но может быть перехватана пользователем, определенной обработкой (смотреть также set_error_handler()) |
8191 | E_ALL | Все ошибки и предупреждение (E_STRICT становится частью E_ALL в PHP 5.4) |
Теперь давайте создадим функцию для обработки ошибок:
Пример
function customError($errno, $errstr) {
echo «<b>Ошибка:</b> [$errno] $errstr<br>»;
echo «Конечный Script»;
die();
}
Приведенный выше код, является простой функцией обработки ошибок. Когда он срабатывает, он получает код ошибки и сообщение об ошибке.
Затем выводится уровень ошибки и сообщение и завершается сценарий.
Теперь, когда Вы создали функцию обработки ошибок, Вы должны решить, когда она должно сработать.
PHP Установить обработчик ошибок
Обработчик ошибок по умолчанию для PHP является встроенным обработчиком ошибок.
Мы собираемся сделать функцию над обработчиком ошибок по умолчанию на время скрипта.
Можно изменить обработчик ошибок для применения только к некоторым ошибкам, таким образом,
сценарий может обрабатывать различные ошибки по-разному.
Однако, в этом примере мы будем использовать наш пользовательский обработчик ошибок для всех ошибок:
set_error_handler(«customError»);
Поскольку мы хотим, чтобы наша пользовательская функция обрабатывала все ошибки, set_error_handler()
требуется только один параметр, второй параметр может быть добавлен, чтобы указать уровень ошибки.
Тестирование обработчика ошибок при попытке вывести несуществующую переменную:
Пример
<?php
//функция обработчика ошибок
function customError($errno, $errstr) {
echo «<b>Ошибка:</b> [$errno] $errstr»;
}
//установить обработчик ошибок
set_error_handler(«customError»);
//Вызов ошибки
echo($test);
?>
Выходные данные приведенного выше кода должны быть примерно такими:
Ошибка: [8] Неопределенна переменная: test
PHP Вызвать ошибку
В скрипте, где пользователи могут вводить данные, полезно инициировать ошибки, когда происходит незаконный ввод.
В PHP это делается с помощью функции trigger_error()
.
В этом примере возникает ошибка, если $test
переменная больше, чем 1
:
Пример
<?php
$test=2;
if ($test>=1)
{
trigger_error(«Значение должно быть 1 или ниже»);
}
?>
Выходные данные приведенного выше кода должны быть примерно такими:
Заметьте: Значение должно быть 1 или ниже
в C:webfoldertest.php на линии 6
Ошибка может быть вызвана в любом месте сценария и путем добавления
второй параметр, Вы можете указать, какой уровень ошибки срабатывает.
Возможные типы ошибок:
- E_USER_ERROR — Неустранимая пользовательская ошибка выполнения. Ошибки, из которых невозможно восстановить. Выполнение скрипта прекращается
- E_USER_WARNING — Непоправимое пользовательское предупреждение во время выполнения. Выполнение скрипта не останавливается
- E_USER_NOTICE — Невыполнение. Уведомление о времени выполнения, созданное пользователем. Сценарий нашел что-то, что могло бы быть ошибкой, но могло бы также произойти при запуске сценария
В этом примере E_USER_WARNING происходит, если переменная $test
больше, чем 1
. Если происходит E_USER_WARNING мы будем использовать наш пользовательский обработчик ошибок и закончить сценарий:
Пример
<?php
//функция обработчика ошибок
function customError($errno, $errstr) {
echo «<b>Ошибка:</b> [$errno] $errstr<br>»;
echo «Закончить Script»;
die();
}
//установить обработчик ошибок
set_error_handler(«customError»,E_USER_WARNING);
//вызов ошибки
$test=2;
if ($test>=1) {
trigger_error(«Значение должно быть 1 или ниже»,E_USER_WARNING);
}
?>
Выходные данные приведенного выше кода должны быть примерно такими:
Ошибка: [512] Значение должно быть 1 или ниже
Конец скрипта
Теперь, когда мы научились создавать собственные ошибки и как их вызвать,
давайте посмотрим на ошибки.
PHP Регистрация ошибок
По умолчанию, PHP отправляет отчет об ошибке в систему регистрации на сервер или файл,
в зависимости от того, как конфигурация error_log установлена в php.ini-файл. По
с помощью функции error_log()
можно отправлять журнал ошибок в указанный файл или в удаленное место назначения.
Отправка сообщений об ошибках по электронной почте, может быть хорошим способом получения уведомления о конкретных ошибках.
PHP Отправка сообщение об ошибке по электронной почте
В приведенном ниже примере мы отправим электронное письмо с сообщением об ошибке и
сценарий, если возникает ошибка:
Пример
<?php
//функция обработчика ошибок
function customError($errno, $errstr) {
echo «<b>Ошибка:</b> [$errno] $errstr<br>»;
echo «Веб-мастер был уведомлен»;
error_log(«Ошибка: [$errno] $errstr»,1,
«someone@example.com»,»От: webmaster@example.com»);
}
//установить обработчик ошибок
set_error_handler(«customError»,E_USER_WARNING);
//вызов ошибки
$test=2;
if ($test>=1) {
trigger_error(«Значение должно быть 1 или ниже»,E_USER_WARNING);
}
?>
Выходные данные приведенного выше кода должны быть примерно такими:
Ошибка: [512] Значение должно быть 1 или ниже
Веб-мастер был уведомлен
И почта, полученная из кода выше, выглядит так:
Ошибка: [512] начение должно быть 1 или ниже
Не должно использоваться со всеми ошибками. Регулярные ошибки должны быть зарегистрированы на
сервере, использующий систему регистрации PHP по умолчанию.
Содержание
- Наследование исключений
Модель исключений (exceptions) в PHP 5 схожа с используемыми в других языках программирования.
Исключение можно сгенерировать (как говорят, «выбросить») при помощи оператора
throw, и можно перехватить (или, как говорят, «поймать»)
оператором catch. Код генерирующий исключение, должен
быть окружен блоком try, для того чтобы можно было
перехватить исключение. Каждый блок try
должен иметь как минимум один соответствующий ему блок catch или finally.
Генерируемый объект должен принадлежать классу Exception
или наследоваться от Exception. Попытка сгенерировать
исключение другого класса приведет к неисправимой ошибке.
catch
Можно использовать несколько блоков catch,
перехватывающих различные классы исключений.
Нормальное выполнение (когда не генерируются исключения в блоках
try или когда класс сгенерированного исключения не
совпадает с классами, объявленными в соответствующих блоках
catch) будет продолжено за последним блоком
catch. Исключения так же могут быть сгенерированы (или
вызваны еще раз) оператором throw
внутри блока catch.
При генерации исключения код следующий после описываемого выражения
исполнен не будет, а PHP предпримет попытку найти
первый блок catch, перехватывающий исключение данного
класса. Если исключение не будет перехвачено, PHP выдаст сообщение об
ошибке: «Uncaught Exception …» (Неперехваченное
исключение), если не был определен обработчик ошибок при помощи
функции set_exception_handler().
finally
В PHP 5.5 и более поздних версиях также можно использовать блок finally
после или вместо блока catch. Код в блоке
finally всегда будет выполняться после кода в блоках
try и catch, вне зависимости было ли
брошено исключение или нет, перед тем как продолжится нормальное выполнение кода.
whether an exception has been thrown, and before normal execution resumes.
Примеры
Пример #3 Выброс исключений
<?php
function inverse($x) {
if (!$x) {
throw new Exception('Деление на ноль.');
}
return 1/$x;
}
try {
echo
inverse(5) . "n";
echo inverse(0) . "n";
} catch (Exception $e) {
echo 'Выброшено исключение: ', $e->getMessage(), "n";
}// Продолжение выполнения
echo "Hello Worldn";
?>
Результат выполнения данного примера:
0.2 Выброшено исключение: Деление на ноль. Hello World
Пример #4 Вложенные исключения
<?php
function inverse($x) {
if (!$x) {
throw new Exception('Деление на ноль.');
}
return 1/$x;
}
try {
echo
inverse(5) . "n";
} catch (Exception $e) {
echo 'Поймано исключение: ', $e->getMessage(), "n";
} finally {
echo "Первое finally.n";
}
try {
echo
inverse(0) . "n";
} catch (Exception $e) {
echo 'Поймано исключение: ', $e->getMessage(), "n";
} finally {
echo "Второе finally.n";
}// Продолжение нормального выполнения
echo "Hello Worldn";
?>
Результат выполнения данного примера:
0.2 Первое finally. Поймано исключение: Деление на ноль. Второе finally. Hello World
Пример #5 Вложенные исключения
<?phpclass MyException extends Exception { }
class
Test {
public function testing() {
try {
try {
throw new MyException('foo!');
} catch (MyException $e) {
// повторный выброс исключения
throw $e;
}
} catch (Exception $e) {
var_dump($e->getMessage());
}
}
}$foo = new Test;
$foo->testing();?>
Результат выполнения данного примера:
Вернуться к: Справочник языка
Иногда ваше приложение не запускается должным образом, что приводит к ошибкам. Есть ряд причин, которые могут вызвать ошибки, например:
- Веб-серверу может не хватить места на диске;
- Пользователь мог ввести недопустимое значение в поле формы;
- Файл или запись базы данных, к которой вы пытались получить доступ, возможно, не существует;
- Приложение может не иметь разрешения на запись в файл на диске;
- Служба, к которой приложение должно получить доступ, может быть временно недоступна.
Эти типы ошибок известны как ошибки времени выполнения, потому что они возникают во время выполнения скрипта. Они отличаются от синтаксических ошибок, которые необходимо исправлять перед запуском скриптов.
Профессиональное приложение должно иметь возможность изящно обрабатывать такие ошибки времени выполнения. Обычно это означает более четкое и точное информирование пользователя о проблеме.
Понимание уровней ошибок
Обычно, когда возникает проблема, препятствующая правильной работе скрипта, механизм PHP выдает ошибку. Каждая ошибка представлена целым числом и соответствующей константой. В следующей таблице перечислены некоторые из распространенных уровней ошибок:
Название | Значение | Описание |
---|---|---|
E_ERROR |
1 |
Неустранимая ошибка времени выполнения от которой невозможно избавиться. Выполнение скрипта немедленно прекращается. |
E_WARNING |
2 |
Предупреждение во время выполнения. Она несущественна, и большинство ошибок попадают в эту категорию. Выполнение скрипта не останавливается. |
E_NOTICE |
8 |
Уведомление во время выполнения. Указывает, что скрипт обнаружил что-то, что могло быть ошибкой, хотя такая ситуация также может возникнуть при обычном запуске скрипта. |
E_USER_ERROR |
256 |
Сообщение о фатальной пользовательской ошибке. Она похожа на E_ERROR , за исключением того, что она генерируется PHP-скриптом с использованием функции trigger_error() . |
E_USER_WARNING |
512 |
Предупреждающее сообщение, созданное пользователем без фатального исхода. Она похожа на E_WARNING , за исключением того, что она генерируется PHP-скриптом с использованием функции trigger_error() . |
E_USER_NOTICE |
1024 |
Сообщение с уведомлением, созданное пользователем. Она похожа на E_NOTICE за исключением того, что она генерируется PHP-скриптом с использованием функции trigger_error() . |
E_STRICT |
2048 |
Не совсем ошибка, но срабатывает всякий раз, когда PHP встречает код, который может привести к проблемам или несовместимости пересылки. |
E_ALL |
8191 |
Все ошибки и предупреждения, кроме E_STRICT до PHP 5.4.0. |
Дополнительные сведения об уровнях ошибок см. в справочнике по уровням ошибок PHP.
Механизм PHP вызывает ошибку всякий раз, когда он сталкивается с проблемой в вашем скрипте, но вы также можете инициировать ошибки самостоятельно, чтобы генерировать более удобные сообщения об ошибках. Таким образом вы можете сделать свое приложение более сложным. В следующем разделе описаны некоторые из распространенных методов, используемых для обработки ошибок в PHP:
Базовая обработка ошибок с помощью функции die()
Рассмотрим следующий пример, в котором просто попытаемся открыть текстовый файл только для чтения.
<?php
// Пробуем открыть несуществующий файл
$file = fopen("sample.txt", "r"); // Выводит: Warning: fopen(sample.txt) [function.fopen]: failed to open stream: No such file or directory in C:wampwwwprojecttest.php on line 2
?>
Если мы выполним несколько простых шагов, мы сможем предотвратить получение пользователями такого сообщения об ошибке.
<?php
if(file_exists("sample.txt")){
$file = fopen("sample.txt", "r");
} else{
die("Error: The file you are trying to access doesn't exist.");
}
?>
Как вы можете видеть, реализовав простую проверку, существует ли файл перед попыткой доступа к нему, мы можем сгенерировать сообщение об ошибке, которое будет более понятным для пользователя.
Используемая выше функция die()
просто отображает пользовательское сообщение об ошибке и завершает текущий скрипт, если файл sample.txt
не найден.
Создание собственного обработчика ошибок
Вы можете создать свою собственную функцию обработчика ошибок, чтобы справляться с ошибкой времени выполнения, генерируемой механизмом PHP. Пользовательский обработчик ошибок обеспечивает большую гибкость и лучший контроль над ошибками; он может проверять ошибку и решать, что с ней делать — отображать сообщение пользователю, регистрировать ошибку в файле или базе данных или отправлять по электронной почте, попытаться исправить проблему и продолжить, выйти из выполнения скрипта или вообще игнорировать ошибку.
Функция пользовательского обработчика ошибок должна иметь возможность обрабатывать как минимум два параметра (errno и errstr), однако она может дополнительно принимать три дополнительных параметра (errfile, errline и errcontext), как описано ниже:
Параметр | Описание |
---|---|
Обязательно — следующие параметры обязательны | |
errno. | Задает уровень ошибки в виде целого числа. Это соответствует соответствующей константе уровня ошибки (E_ERROR , E_WARNING и т. д.). |
errstr. | Задает сообщение об ошибке в виде строки. |
Опционально — следующие параметры являются необязательными | |
errfile. | Задает имя файла скрипта, в котором произошла ошибка. |
errline. | Задает номер строки, в которой произошла ошибка. |
errcontext. | Задает массив, содержащий все переменные и их значения, которые существовали на момент возникновения ошибки. Полезно для отладки. |
Вот пример простой пользовательской функции обработки ошибок. Этот обработчик customError()
запускается всякий раз, когда возникает ошибка, какой бы тривиальной она ни была. Затем он выводит сведения об ошибке в браузер и останавливает выполнение скрипта.
<?php
// Функция обработчика ошибок
function customError($errno, $errstr){
echo "<b>Error:</b> [$errno] $errstr";
}
?>
Вам нужно указать PHP, чтобы он использовал вашу пользовательскую функцию обработчика ошибок — просто вызовите встроенную функцию set_error_handler()
, передав имя функции.
<?php
// Функция обработчика ошибок
function customError($errno, $errstr){
echo "<b>Error:</b> [$errno] $errstr";
}
// Устанавливаем обработчик ошибок
set_error_handler("customError");
// Вызываем ошибку
echo($test);
?>
Регистрация ошибок
Журнал сообщений об ошибках в текстовом файле
Вы также можете записать подробную информацию об ошибке в файл журнала, например:
<?php
function calcDivision($dividend, $divisor){
if($divisor == 0){
trigger_error("calcDivision(): The divisor cannot be zero", E_USER_WARNING);
return false;
} else{
return($dividend / $divisor);
}
}
function customError($errno, $errstr, $errfile, $errline, $errcontext){
$message = date("Y-m-d H:i:s - ");
$message .= "Error: [" . $errno ."], " . "$errstr in $errfile on line $errline, ";
$message .= "Variables:" . print_r($errcontext, true) . "rn";
error_log($message, 3, "logs/app_errors.log");
die("There was a problem, please try again.");
}
set_error_handler("customError");
echo calcDivision(10, 0);
echo "This will never be printed.";
?>
Отправка сообщений об ошибках по электронной почте
Вы также можете отправить электронное письмо с подробностями об ошибке, используя ту же функцию error_log()
.
<?php
function calcDivision($dividend, $divisor){
if ($divisor == 0){
trigger_error("calcDivision(): The divisor cannot be zero", E_USER_WARNING);
return false;
} else{
return($dividend / $divisor);
}
}
function customError($errno, $errstr, $errfile, $errline, $errcontext){
$message = date("Y-m-d H:i:s - ");
$message .= "Error: [" . $errno ."], " . "$errstr in $errfile on line $errline, ";
$message .= "Variables:" . print_r($errcontext, true) . "rn";
error_log($message, 1, "webmaster@example.com");
die("There was a problem, please try again. Error report submitted to webmaster.");
}
set_error_handler("customError");
echo calcDivision(10, 0);
echo "This will never be printed.";
?>
Вызов ошибок
Хотя движок PHP выдает ошибку всякий раз, когда он сталкивается с проблемой в вашем скрипте, вы также можете вызвать ошибки самостоятельно. Это может помочь сделать ваше приложение более надежным, поскольку оно может выявлять потенциальные проблемы до того, как они перерастут в серьезные ошибки.
Чтобы вызвать ошибку в скрипте, вызовите функцию trigger_error()
, передав сообщение об ошибке, которое вы хотите сгенерировать:
trigger_error("There was a problem.");
Рассмотрим следующую функцию, которая вычисляет деление двух чисел.
<?php
function calcDivision($dividend, $divisor){
return($dividend / $divisor);
}
// Вызываем функцию
echo calcDivision(10, 0); // Выводит: Warning: Division by zero in C:wampwwwprojecttest.php on line 3
?>
Это сообщение выглядит не очень информативным. Рассмотрим следующий пример, в котором для генерации ошибки используется функция trigger_error()
.
<?php
function calcDivision($dividend, $divisor){
if($divisor == 0){
trigger_error("Делитель не может быть нулевым", E_USER_WARNING);
return false;
} else{
return($dividend / $divisor);
}
}
// Вызываем функцию
echo calcDivision(10, 0); // Выводит: Warning: Делитель не может быть нулевым C:wampwwwprojecterror.php on line 4
?>
Как видите, сообщение об ошибке, созданное во втором примере, более четко объясняет проблему по сравнению с предыдущим.
Время на прочтение
16 мин
Количество просмотров 113K
Я рад бы написать что “эта статья предназначена для новичков”, но это не так. Большинство php-разработчиков, имея опыт 3, 5 и даже 7 лет, абсолютно не понимают как правильно использовать эксепшены. Нет, они прекрасно знают о их существовании, о том что их можно создавать, обрабатывать, и т.п., но они не осознают их удобность, логичность, и не воспринимают их как абсолютно нормальный элемент разработки.
В этой статье не будет мануала по эксепшенам — это все отлично описано в документации php. Здесь я я расскажу о преимуществах использования эксепшенов, и о том, где их, собственно говоря, надо использовать. Все примеры будут для Yii
, но это не особо важно.
Почему мы не умеем пользоваться эксепшенами:
Я люблю PHP
. Это прекрасный язык, как бы его не ругали. Но для начинающих разработчиков он несет определенную опасность: он слишком многое прощает.
PHP
— это чрезмерно любящая мать. И при отсутствии строгого отца (например, Java
) или самодисциплины, разработчик вырастет эгоистом, которому плевать на все правила, стандарты и лучшие практики. И вроде бы E_NOTICE
пора включать, а он все на мать надеется. Которая, между прочим, стареет — ей уже E_STRICT
c E_DEPRICATED
нужны, а сынуля все на шее висит.
Виноват ли PHP
— предмет дискуссий, но то, что с самого начала PHP
не приучает нас к эксепшенам — это факт: его стандартные функции не создают эксепшены. Они либо возвращают false
, намекая что что-то не так, или записывают куда-то код ошибки, который не всегда додумаешься проверить. Или впадают в другую крайность — Fatal Error
.
И пока наш начинающий разработчик пытается написать свою первую быдло-cms, он ни разу не встретиться с механизмом эксепшенов. Вместо этого, он придумает несколько способов обработки ошибок. Я думаю, все понимают о чем я — эти методы, возвращающие разные типы (например, объект при успешном выполнении, а при неудаче — строка с ошибкой), или запись ошибки в какую-либо переменную/свойство, и всегда — куча проверок чтоб передать ошибку вверх по стеку вызовов.
Затем он начнет использовать сторонние библиотеки: попробует, например, Yii
, и впервые столкнется с эксепшенами. И вот тогда…
И тогда ничего не произойдет. Ровном счетом ничего. У него уже сформировались отточенные месяцами/годами способы обработки ошибок — он продолжит использовать их. Вызванный кем-то (сторонней библиотекой) эксепшн будет восприниматься как определенный вид Fatal Error
. Да, гораздо более детальный, да подробно логируется, да Yii покажет красивую страничку, но не более.
Затем он научиться отлавливать и обрабатывать их. И на этом его знакомство c эксепшенами закончиться. Ведь надо работать, а не учиться: знаний ему и так хватает (сарказм)!
Но самое ужасное — вырабатывается отношение к эксепшенам как к чему-то плохому, нежелательному, опасному, чего быть не должно, и чего нужно всеми способами избежать. Это абсолютно не правильный подход.
Преимущества эксепшенов
На самом деле использование эксепшенов — крайне лаконичное и удобное решение создания и обработки ошибок. Приведу наиболее значимые преимущества:
Контекстная логика
Прежде всего, хотелось бы показать что эксепшн — это не всегда только ошибка (как обычно ее воспринимают разработчики). Иногда он может быть частью логики.
Например, есть у нас функция чтения JSON
объекта из файла:
/**
* Читает объект из JSON файла
* @param string $file
* @throws FileNotFoundException файл не найден
* @throws JsonParseException не правильный формат json
* @return mixed
*/
public function readJsonFile($file)
{
...
}
Допустим мы пытаемся прочитать какие-то ранее загруженные данные. При такой операции эксепшн FileNotFoundException
не является ошибкой и вполне допустим: возможно, мы ни разу не загружали данные, поэтому файла и нет. А вот JsonParseException
— это уже признак ошибки, ибо данные были заргужены, обработаны, сохранены в файл, но почему-то сохранились не правильно.
Совсем другое дело, когда мы пытаемся прочитать файл, который должен быть всегда: при такой операции FileNotFoundException
так же является сигналом ошибки.
Таким образом, эксепшены позволяют нам определять логику их обработки в зависимости от контекста, что очень удобно.
Упрощение логики и архитектуры приложения
Попробуйте использовать эксепшены, и вы увидите как ваш код станет более лаконичным и понятным. Исчезнут все костыльные механизмы, возможно, уберутся куча вложенных if’ов, различные механизмы передачи ошибки вверх по стеку вызова, логика станет более простой и прямолинейной.
Места вызовы эксепшенов помогут вашему коллеге лучше понять бизнес логику и предметную область, ибо копаясь в вашем коде он сразу увидит что допустимо, а что нет.
И, если рассматривать какой-либо самодостаточный кусок кода, например, компонента, то список бросаемых им эксепшенов выполняет еще одну важную вещь: дополняет интерфейс этого компонента.
Пользуясь сторонними компонентами мы привыкли обращать внимание только на положительную сторону — на то, что он умеет. При этом обычно не думаем о исключениях, которые он может создать в процессе работы. Список эксепшенов сразу предупреждает где, когда, и какие проблемы могут возникнуть. А предупрежден — значит вооружен.
Вот пример информативного интерфейса, который дополнен знаниями об эксепшенах:
interface KladrService
{
/**
* Определяет код КЛАДР по адресу
* @param Address $address
* @return string код для адреса
* @throws AddressNotFoundException адрес не найден в базе адресов
* @throws UnresoledAddressException адрес найден, но для него не существует код КЛАДР
*/
public function resolveCode(Address $address);
/**
* Определяет адрес по коду КЛАДР
* @param string $code
* @return Address
* @throws CodeNotFoundException не найлен код КЛАДР
*/
public function resolveAddress($code);
}
Следует упомянуть что разрабатывая классы эксепшенов мы должны следовать принципу информативного интерфейса. Грубо говоря — учитывать их логический смысл, а не физический. Например, если адреса у нас храняться в файлах, то отсутствие файла адреса вызовет FileNotFoundException
. Мы же должны перехватить его и вызвать более осмысленный AddressNotFoundException
.
Использование объектов
Использование определенного класса в качестве ошибки — очень удобное решение. Во первых, класс невозможно перепутать: взгляните на 2 способа обработки ошибки:
if(Yii::app()->kladr->getLastError() == ‘Не найден адрес’){
….
}
try{
...
}
catch(AddressNotFoundException $e){
...
}
В первом варианте элементарная опечатке сломает вам всю логику, во втором же ошибку сделать просто невозможно.
Второе преимущество — класс эксепшена инкапсулирует все необходимые данные для его обработки. Например, AddressNotFoundException
мог бы выглядеть следующим образом:
/**
* Адрес не найден в базе адресов
*/
class AddressNotFoundException extends Exception
{
/**
* Не найденный адрес
* @var Address
*/
private $address;
/**
* @param Address $address
*/
public function __construct(Address $address)
{
Exception::__construct('Не найден адрес '.$address->oneLine);
$this->address = $address;
}
/**
* @return Address
*/
public function getAddress()
{
return $this->address;
}
}
Как видим — эксепшн содержит адрес, который не удалось найти. Обработчик может его получить и выполнить на основании его какую-то свою логику.
Третье преимущество — это, собственно, все преимущества ООП. Хотя эксепшены, как правило, простые объекты, поэтому возможности ООП мало используются, но используются.
Например, у меня в приложении порядка 70 классов эксепшенов. Из них несколько — базовых — по одному классу на модуль. Все остальные — наследуются от базового класса своего модуля. Сделано это для удобства анализа логов.
Так же я использую несколько ИНТЕРФЕЙС-МАРКЕРОВ:
- UnloggedInterface: По умолчанию у меня логируются все необработанные ошибки. Этим интерфейсом я помечаю эксепшены, которые не надо логировать вообще.
- PreloggedInterface: Этим интерфейсом я помечаю эксепшены, которые необходимо логировать в любом случае: неважно, обработаны они или нет.
- OutableInterface: Этот интерфейс помечает эксепшены, текст которых можно выдавать пользователю: далеко не каждый эксепшн можно вывести пользователю. Например, можно вывести эксепшн с текстом “Страница не найдена” — это нормально. Но нельзя выводить эксепшн с текстом “Не удалось подключиться к Mysql используя логин root и пароль 123”.
OutableInterface
помечает эксепшены которые выводить можно (таких у меня меньшинство). В остальных ситуация выводиться что то типа “Сервис не доступен”.
Обработчик по умолчанию, логирование
Обработчик по умолчанию — чрезвычайно полезная штука. Кто не знает: он выполняется когда эксепшн не удалось обработать ни одним блоком try catch
.
Этот обработчик позволяет нам выполнить различные действия перед остановкой скрипта. Самое главное что нужно сделать, это:
Откат изменений: так как операция не выполнена до конца, необходимо откатить все сделанные изменения. В противном случае мы испортим данные. Например, можно в CController::beforeAction()
открыть транзакцию, в CController::afterAction()
коммитить, а в случае ошибки сделать роллбэк в обработчике по умолчанию.
Это довольно грубый способ отката, плюс зачастую откат подразумевает не только откат транзакций, и знания о том, как правильно откатывать должны находиться в коде бизнес логики. В таких ситуациях следует воспользоваться вот таким приемом:
public function addPosition(Position $position)
{
try
{
... выполнение операции ...
}
catch(Exception $e)
{
... откат изменений ...
throw $e; // Заново бросаем тот же эксепшн
}
}
Получается что мы откатили изменения и бросили тот же эксепшн, что продолжить его обработку.
Логирование: так же обработчик по умолчанию позволяет нам выполнить какое-то кастомное логирование. Например, в моем приложении я все складываю в базу и использую собственное средство для анализа. На работе мы используем getsentry.com/welcome. В любом случае, эксепшн, дошедший до обработчика по умолчанию — скорее всего непредусмотренный эксепшн, и его необходимо логировать. Следует отметить, что в класс эксепшена можно добавить различную информацию, которую необходимо логировать для большего понимания причины возникновения ошибки.
Невозможность не заметить и перепутать
Огромным плюсом эксепшена является его однозначность: его не возможно не заметить и не возможно с чем-то спутать.
Из первого следует что мы всегда будем осведомлены о возникшей ошибке. И это замечательно — всегда лучше знать о проблеме, чем не знать.
Второй плюс становится очевиден в сравнении с кастомными методами обработки ошибки, например когда метод возвращает null
если не нашла нужный объект и false в случае ошибки. В таком случае элементарно не заметить ошибку:
$result = $this->doAnything(); // null если не нашла нужный объект и false в случае ошибки
// Не заметит ошибки
if($result){ ... }
// Не заметит ошибки
if($result == null){ ... }
// Не заметит ошибки
if(empty($result)){ ... }
// Не заметит ошибки
if($result = null){ ... }
Эксепшн же невозможно пропустить.
Прекращение ошибочной операции
Но самое главное, и самое важное, что делает эксепшн — это прекращает дальнейшее выполнение операции. Операции, которая уже пошла не так. И, следовательно, результат которой непредсказуем.
Огромный минус самопальных механизмов обработки ошибок — необходимость самостоятельно проверять возникновение ошибки. Например, после каждой операции нам нужно писать что то типа:
$this->doOperation();
if($this->getLastError() !== null)
{
echo $this->getLastError();
die;
}
Это требует определенной дисциплины от разработчиков. А далеко не все дисциплинированны. Далеко не все вообще знают что у вашего объекта есть метод getLastError()
. Далеко не все понимают, почему вообще так важно проверит что все идет как надо, а если нет — откатить изменения и прекратить выполнение.
В итоге, проверки не делаются, и выполнение операции приводит к совершенно неожиданным результатам: вместо одного пользователя удаляются все, деньги пересылаются не тому человеку, голосования в госдуме выдает ложный результат — подобное я видел десятки раз.
Эксепшн защищает нас от подобных проблем: он либо ищет соответствующий обработчик ( его наличие означает что разработчик предусмотрел данную ситуацию, и все нормально), либо доходит до обработчика по умолчанию, который может откатить все изменения, залогировать ошибку, и выдать соответствующее предупреждение пользователю.
Когда следует вызывать эксепшены:
С преимуществами вроде разобрались. Надеюсь, я сумел показать что эксепшены являются крайне удобным механизмом.
Встает вопрос: в каких ситуациях стоит вызывать эксепшн?
Если кратко — всегда! Если подробно: всегда, когда ты уверен что операция должна выполниться нормально, но что-то пошло не так, и ты не знаешь что с этим делать.
Посмотрим на простейший экшн добавления записи:
/**
* Создает пост
*/
public function actionCreate()
{
$post = Yii::app()->request->loadModel(new Post());
if($post->save())
{
$this->outSuccess($post);
}
else
{
$this->outErrors($post);
}
}
Когда мы введем некорректные данные поста эксепшн не вызывается. И это вполне соответствует формуле:
- На данным шаге мы не уверенны, что операция должна пройти успешно, ибо нельзя доверять данным, введенным пользователем.
- Мы знаем что с этим делать. Мы знаем что в случае некорректных данных, мы должны отобразить пользователю список ошибок. Тут следует отметить что знание о том, “что делать” находиться в пределах текущего метода.
Поэтому в данном случае нет необходимости использовать эксепшены. Но давайте посмотрим на другой пример: Есть страница заказа, на которой есть кнопка, отменяющая заказ. Код отмены заказа выглядит следующим образом:
/**
* Отменяет заказа.
* Отмена производиться путем смены статуса на STATUS_CANCEL.
* @throws Exception
*/
public function cancel()
{
// Проверим, находиться ли STATUS_CANCEL в разрешенных
if(!$this->isAllowedStatus(self::STATUS_CANCEL))
{
throw new Exception('Cancel status not allowed');
}
// Сообственно смена статуса
$this->status = self::STATUS_CANCEL;
$isSaved = $this->save();
// Проверка на то что все успешно сохранилось и что после сохранения статус остался STATUS_CANCEL
if(!$isSaved|| $this->status !== self::STATUS_CANCEL)
{
throw new Exception('Bad logic in order cancel');
}
}
Сама кнопка отмены показывается только тогда, когда заказ возможно отменить. Таким образом, когда вызывается этот метод, я уверен, что операция должна пройти успешно (в противном случае кнопка бы не отобразилась, и пользователь не смог бы нажать на нее для вызова этого метода).
Первым делом идет предвалидация — мы проверяем действительно ли мы можем выполнить операцию. В теории все должно пройти успешно, но если isAllowedStatus
вернет false
— значит что-то пошло не так. Плюс, в пределах текущего метода, мы абсолютно не знаем как обработать эту ситуацию. Понятно, что нужно залогировать ошибку, вывести ее пользователю, и т.п… Но в контексте именно этого метода мы не знаем что с ней делать. Поэтому бросаем эксепшн.
Далее идет выполнение операции и сохранение изменений.
Затем идет поствалидация — мы проверяем, действительно ли все сохранилось, и действительно ли статус изменился. На первый взгляд это может показаться бессмысленным, но: заказ вполне мог не сохранится (например, не прошел валидацию), а статус вполне мог быть изменен (например, кто-то набыдлокодил в CActiveRecord::beforeSave
). Поэтому эти действия необходимы, и, опять-таки, если что-то пошло не так — бросаем эксепшн, так как в пределах данного метода мы не знаем как обрабатывать эти ошибки.
Эксепшн vs возврат null
Следует отметить, что эксепшн следует бросать только в случае ошибки. Я видел как некоторые разработчики злоупотребляют ими, бросая их там, где не следовало бы. Особенно часто — когда метод возвращает объект: если не получается вернуть объект — бросается эксепшн.
Тут следует обратить внимание на обязанности метода. Например, СActiveRecord::find()
не бросает эксепшн, и это логично — уровень его “знаний” не содержит информации о том, является ли ошибкой отсутствие результата. Другое дело, например, метод KladrService::resolveAddress()
который в любом случае обязан вернуть объект адреса (иначе либо код неправильный, либо база не актуальная). В таком случае нужно бросать эксепшн, ибо отсутствие результата — это ошибка.
В целом же, описанная формула идеально определяет места, где необходимо бросать эксепшены. Но особо хотелось бы выделить 2 категории эксепшенов, которых нужно делать как можно больше:
Технические эксепшены
Это эксепшены, которые абсолютно не связанны с предметной областью, и необходимы чтобы чисто технически предотвратить выполнение неверной логики.
Вот несколько примеров:
// В нескольких if
if($condition1)
{
$this->do1();
}
elseif($condition2)
{
$this->do2();
}
...
else
{
// Когда должен сработать один из блоков if, но не сработал - бросаем эксепшн
throw new BadLogicException;
}
// То же самое в swith
switch($c)
{
case 'one':
return 1;
case 'two'
return 2;
...
default:
// Когда должен сработать один из блоков case, но не сработал - бросаем эксепшн
throw new BadLogicException;
}
// При сохранении связанных моделей
if($model1->isNewRecord)
{
// Если первая модель не сохранена, у нее нет id, то строка $model2->parent_id = $model1->id
// сделает битые данные, поэтому необходимо проверять
throw new BadLogicException;
}
$model2->parent_id = $model1->id;
// Просто сохранении - очень часто разраотчики используют save и не проверяют результат
if(!$model->save())
{
throw new BadLogicException;
}
/**
* Cкоуп по id пользователя
* @param int $userId
* @return $this
*/
public function byUserId($userId)
{
if(!$userId)
{
// Если не вызывать этот эксепшн, то при пустом userId скоуп вообще не будет применен
throw new InvalidArgumentException;
}
$this->dbCriteria->compare('userId', $userId);
return $this;
}
Технические эксепшены помогут не допустить или отловить, имхо, большую часть багов в любом проекте. И неоспоримым плюсом их использования является отсутствие необходимости понимать предметную область: единственное что требуется — это дисциплина разработчика. Я призываю не лениться и вставлять такие проверки повсеместно.
Эксепшены утверждений
Эксепшены утверждений (по мотивам DDD
) вызываются когда мы обнаруживаем что нарушается какая-либо бизнес-логика. Безусловно, они тесно связанна с знаниями предметной области.
Они бросаются когда мы проверяем какое-либо утверждение, и видим что результат проверки не соответствует ожидаемому.
Например, есть метод добавления позиции в заказ:
/**
* Добовляет позицию в заказ
* @param Position $position
* @throws Exception
*/
public function addPosition(Position $position)
{
$this->positions[] = $position;
... перерасчет стоимость позиций, доставки, скидок, итоговой стоимсоти ...
// проверям корректность рассчета
if($this->totalCost != $this->positionsCost + $this->deliveryCost - $this->totalDiscounts)
{
throw new Exception('Cost recalculation error');
}
... Обновление параметров доставки ...
// проверям можем ли мы доставить заказа с новой позицеей
if(!Yii::app()->deliveryService->canDelivery($this))
{
throw new Exception('Cant delivery with new position')
}
… прочие действия ...
}
В процессе добавления позиции происходит куча различных действий. И при этом периодически проверяются различные утверждения: что все суммы сходятся, что заказ может быть доставить — это и есть эксепшены утверждений.
Здесь можно подискутировать на тему необходимости подобных эксепшенов:
Например, можно написать тесты на методы перерасчета стоимости заказа, и проверка в теле метода — не более чем дублирование теста. Можно проверять возможность доставки заказа с новой позицией до добавления позиции (чтоб предупредить об этом пользователя, как минимум)
Но практика показывает, что далеко не всегда удается написать тесты для всех инвариантов объекта. И невозможно защититься от, например, нового разработчика, который может накодить все что угодно.
Поэтому в критичных местах такие эксепшены нужны однозначно.
Изменение логики для избегания эксепшна
Как я уже говорил, PHP
разработчики боятся эксепшенов. Они боятся их появления, и боятся бросать их самостоятельно.
И в этой борьбе с эксепшенами многие допускают ошибку: отступают от изначально четкой, понятной, прямолинейной логики в сторону каких-либо допущений, чтобы хоть как-то выполнить операцию.
Вот пример: необходимо просто отобразить страницу по id (чтоб вы понимали — это реальный код из известного проекта)
/**
* Отображает страницу по id
* @param int $id
*/
public function actionView($id = 1)
{
$page = Page::model()->findByPk($id) ?: Page::model()->find();
$this->render('view', ['page' => $page]);
}
Несмотря на простейшую и понятную задачу — здесь совершенно дикая логика.
Мало того, что она может показать пользователю совершенно не то что надо, так она еще и маскирует наши баги:
- если
id
не задан — беретсяid = 1
. Проблема в том, что когда id не задан — это уже баг, ибо где-то у нас не правильно формируются ссылки. - Если страница не найдена — значит где-то у нас ссылка на несуществующую страницу. Это тоже, скорее всего, баг.
Такое поведение не приносит пользы ни пользователю, ни разработчикам. Мотивация такой реализации — показать хоть что-то, ибо 404
эксепшн — плохо.
Еще один пример:
/**
* Выдает код кладра города
* @param mixed $region
* @param mixed $city
* @return string
*/
public function getCityKladrCode($region, $city)
{
if($сode = ... получение кода для города... )
{
return $сode;
}
return ... получение кода для региона ...
}
Тоже из реального проекта, и мотивация такая-же: вернуть хоть что-то, но не вызывать эксепшн, несмотря на то, что метод явно должен возвращать код города, а не региона.
И таких изменений логики в среднестатистическом проекте огромное кол-во. Пока ты помнишь об этом — это кажется безобидным. Но как только забываешь, или подключается другой разработчик — баг обеспечен. Причем неявный баг, плавающий.
Мое мнение — это недопустимо. Просто когда ты работаешь с большими деньгами (а я с ними работал довольно долго), вырабатывается определенные правила, и одно из них — прерывать операцию в случае любого подозрения на ошибку. Транзакция на 10 млн баксов: согласитесь, ее лучше отменить, чем перечислить деньги не тому человеку.
Конечно, обычно мы имеем дело с менее рискованными операциями. И в случае бага, например, инвалид не сможет оставить заявку на установку пандуса в подъезде. И разработчик на раслабоне (его ведь даже не оштрафуют) пренебрегает этими элементарными правилами, мол, подумаешь, мелочь какая. Проблема в том, что когда ему доверят что-то критически важное — вряд ли его подход измениться. Ибо проблема не в знаниях, и не в риске, а в дисциплине и в отношении к делу. И получается, что после таких программистов у нас где-то трубы зимой лопаются, где-то нефть разливается тоннами, где-то люди умирают десятками, где-то деньги воруются миллионами. Подумаешь, мелочь какая!
Собачки
Я почему-то думал что собачками уже никто не пользуется. Но недавно столкнулся с коллективом разработчиков, которые используют их повсеместно вместо проверки isset
, поэтому решил написать и про них.
Собачки вместо isset
используют для лаконичности кода:
@$policy->owner->address->locality;
против
isset($policy->owner->address) ? $policy->owner->address->locality : null;
Действительно, выглядит намного короче, и на первый взгляд результат такой же. Но! опасно забывать что собачка — оператор игнорирования сообщений об ошибках. И @$policy->owner->address->locality
вернет null
не потому-что проверит существование цепочки объектов, а потому-что просто проигнорирует возникшую ошибку. А это совершенно разные вещи.
Проблем в том, что помимо игнорирования ошибки Trying to get property of non-object
(которое и делает поведение собачки похожим на isset
), игнорируются все другие возможные ошибки.
PHP
— это магический язык! При наличии всех этих магических методов (__get, __set, __call, __callStatic, __invoke
и пр.) мы не всегда можем сразу понять что происходит на самом деле.
Например, еще раз взглянем на строку $policy->owner->address->locality
. На первый взгляд — цепочка объектов, если присмотреться пристально — вполне может быть и так:
policy
— модельCActiveRecord
owner
— релейшенaddress
— геттер, который, например, обращается к какому-либо стороннему сервисуlocality
— аттрибут у
То есть простой строкой $policy->owner->address->locality
мы на самом деле запускаем выполнение тысяч строк кода. И собачка перед это строкой скрывает ошибки в любой из этих строк.
Таким образом, столь необдуманное использование собачки потенциально создает огромное кол-во проблем.
Послесловие
Программирование — потрясающее занятие. На мой взгляд, оно похоже на сборку огромного конструктора LEGO. В самом начале перед тобой инструкция и россыпь мелких деталей. И вот, ты берешь инструкцию, по которой методично собираешь их в небольшие блоки, затем объеденяешь их в нечто большее, еще больше… И ты ловишь кайф от этого чертовски увлекательного процесса, ты ловишь кайф от того, насколько все логично и продуманно устроенно, насколько все эти детальки подходят друг к другу. И вот — перед тобой уже целый трактор, или самосвал. И это потрясающе!
В программировании тоже самое, только роль инструкций выполняют знания паттернов, принципов проектирования классов, лучших практик программирования и построения архитектур. И когда ты все это впитаешь и научишься применять на практике — ты начинаешь ловить кайф от работы, такой же, как при сборке LEGO.
Но попробуйте собрать конструктор без инструкции… Эта мысль похожа на бред. Тем не менее, программисты без всех этих знаний прекрасно работают. Годами. И им это не кажется бредом — они даже не понимают, что что-то делают не так. Вместо этого они жалуются что им дали слишком мало времени.
И если в послесловии предыдущего поста я предлагал задуматься, надеясь что кто-то изменит свой код к лучшему, то теперь я потерял эту надежду. Видимо, люди действительно ценят только тот опыт, за который заплатили.
Так что всем, кто прочел этот пост и подумал “что за бред”, “я все это знаю, но применять лень”, или “будет сосунок мне указывать” — я желаю совершить баг. Баг, за который оштрафуют или уволят. И тогда вы, возможно, вспомните этот пост, и задумаетесь: “возможно я и в правду что-то делаю не так”?
Совершите его как можно скорее. Ибо лучше один раз ошибиться, но прозреть, чем всю жизнь прожить быдлокоддером. Аминь.
Всем добра )