Php класс ошибок

Исключения

Содержание

  • Наследование исключений

В PHP реализована модель исключений, аналогичная тем, что используются в других языках программирования.
Исключение в PHP может быть выброшено (throw) и поймано (catch).
Код может быть заключён в блок try, чтобы облегчить обработку потенциальных исключений.
У каждого блока try должен быть как минимум один соответствующий блок catch или finally.

Если выброшено исключение, а в текущей области видимости функции нет блока catch,
исключение будет «подниматься» по стеку вызовов к вызывающей функции, пока не найдёт подходящий блок catch.
Все блоки finally, которые встретятся на этом пути, будут выполнены.
Если стек вызовов разворачивается до глобальной области видимости, не встречая подходящего блока catch,
программа завершается с неисправимой ошибкой, если не был установлен глобальный обработчик исключений.

Выброшенный объект должен наследовать (instanceof) интерфейс Throwable.
Попытка выбросить объект, который таковым не является, приведёт к неисправимой ошибке PHP.

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

catch

Блок catch определяет, как реагировать на выброшенное исключение.
Блок catch определяет один или несколько типов исключений или ошибок, которые он может обработать,
и, по желанию, переменную, которой можно присвоить исключение
(указание переменной было обязательно до версии PHP 8.0.0).
Первый блок catch, с которым столкнётся выброшенное исключение или ошибка
и соответствует типу выброшенного объекта, обработает объект.

Несколько блоков catch могут быть использованы для перехвата различных классов исключений.
Нормальное выполнение (когда исключение не выброшено в блоке try)
будет продолжаться после последнего блока catch, определённого в последовательности.
Исключения могут быть выброшены (throw) (или повторно выброшены) внутри блока catch.
В противном случае выполнение будет продолжено после блока catch, который был вызван.

При возникновении исключения, код, следующий за утверждением, не будет выполнен,
а PHP попытается найти первый подходящий блок catch.
Если исключение не поймано, будет выдана неисправимая ошибка PHP
с сообщением «Uncaught Exception ...«,
если только обработчик не был определён с помощью функции set_exception_handler().

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

Начиная с версии PHP 8.0.0, имя переменной для пойманного исключения является необязательным.
Если оно не указано, блок catch будет выполнен,
но не будет иметь доступа к выброшенному объекту.

finally

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

Одно из заметных взаимодействий происходит между блоком finally и оператором return.
Если оператор return встречается внутри блоков try или catch, блок finally
всё равно будет выполнен. Более того, оператор return выполнится, когда встретится,
но результат будет возвращён после выполнения блока finally.
Кроме того, если блок finally также содержит оператор return,
возвращается значение из блока finally.

Глобальный обработчик исключений

Если исключению разрешено распространяться на глобальную область видимости,
оно может быть перехвачено глобальным обработчиком исключений, если он установлен.
Функция set_exception_handler() может задать функцию,
которая будет вызвана вместо блока catch, если не будет вызван никакой другой блок.
Эффект по сути такой же, как если бы вся программа была обёрнута в блок trycatch
с этой функцией в качестве catch.

Примечания

Замечание:

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

Пример #1 Преобразование отчётов об ошибках в исключения

<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
throw new
ErrorException($message, 0, $severity, $filename, $lineno);
}
set_error_handler('exceptions_error_handler');
?>

Примеры

Пример #2 Выбрасывание исключения

<?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 "Привет, мир\n";
?>

Результат выполнения данного примера:

0.2
Выброшено исключение: Деление на ноль.
Привет, мир

Пример #3 Обработка исключений с помощью блока finally

<?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 "Привет, мир\n";
?>

Результат выполнения данного примера:

0.2
Первый блок finally.
Поймано исключение: Деление на ноль.
Второй блок finally.
Привет, мир

Пример #4 Взаимодействие между блоками finally и return

<?phpfunction test() {
try {
throw new
Exception('foo');
} catch (
Exception $e) {
return
'catch';
} finally {
return
'finally';
}
}

echo

test();
?>

Результат выполнения данного примера:

Пример #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();?>

Результат выполнения данного примера:

Пример #6 Обработка нескольких исключений в одном блоке catch

<?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();?>

Результат выполнения данного примера:

Пример #7 Пример блока catch без указания переменной

Допустимо начиная с PHP 8.0.0

<?phpclass SpecificException extends Exception {}

function

test() {
throw new
SpecificException('Ой!');
}

try {

test();
} catch (
SpecificException) {
print
"Было поймано исключение SpecificException, но нам безразлично, что у него внутри.";
}
?>

Пример #8 Throw как выражение

Допустимо начиная с PHP 8.0.0

<?phpfunction test() {
do_something_risky() or throw new Exception('Всё сломалось');
}

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:\xampp\htdocs\CustomException\CustomException.php(31)
#0 C:\xampp\htdocs\CustomException\ExceptionTest.php(19): CustomException->__construct()
#1 C:\xampp\htdocs\CustomException\ExceptionTest.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

6 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();
}
?>

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).

Edu

10 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 in\n";
throw
$e;
} finally {
echo
$e->getMessage()." finally \n";
throw new \
Exception("Bye");
}
} catch (\
Exception $e) {
echo
$e->getMessage()." catch out\n";
}
?>

The output is:

Hello catch in
Hello finally
Bye catch out

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 );
}
?>

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.

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

9 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.

tianyiw at vip dot qq dot com

13 days ago

Easy to understand `finally`.
<?php
try {
try {
echo
"before\n";
1 / 0;
echo
"after\n";
} finally {
echo
"finally\n";
}
} catch (\
Throwable) {
echo
"exception\n";
}
?>
# Print:
before
finally
exception

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

8 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.

lscorionjs at gmail dot com

8 months ago

<?phptry {
$str = 'hi';
throw new
Exception();
} catch (
Exception) {
var_dump($str);
} finally {
var_dump($str);
}
?>

Output:
string(2) "hi"
string(2) "hi"

Время на прочтение
16 мин

Количество просмотров 115K

Я рад бы написать что “эта статья предназначена для новичков”, но это не так. Большинство 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.

Но попробуйте собрать конструктор без инструкции… Эта мысль похожа на бред. Тем не менее, программисты без всех этих знаний прекрасно работают. Годами. И им это не кажется бредом — они даже не понимают, что что-то делают не так. Вместо этого они жалуются что им дали слишком мало времени.

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

Так что всем, кто прочел этот пост и подумал “что за бред”, “я все это знаю, но применять лень”, или “будет сосунок мне указывать” — я желаю совершить баг. Баг, за который оштрафуют или уволят. И тогда вы, возможно, вспомните этот пост, и задумаетесь: “возможно я и в правду что-то делаю не так”?

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

Всем добра )

Reg.ru: домены и хостинг

Крупнейший регистратор и хостинг-провайдер в России.

Более 2 миллионов доменных имен на обслуживании.

Продвижение, почта для домена, решения для бизнеса.

Более 700 тыс. клиентов по всему миру уже сделали свой выбор.

Перейти на сайт->

Бесплатный Курс «Практика HTML5 и CSS3»

Освойте бесплатно пошаговый видеокурс

по основам адаптивной верстки

на HTML5 и CSS3 с полного нуля.

Начать->

Фреймворк Bootstrap: быстрая адаптивная вёрстка

Пошаговый видеокурс по основам адаптивной верстки в фреймворке Bootstrap.

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

Верстайте на заказ и получайте деньги.

Получить в подарок->

Бесплатный курс «Сайт на WordPress»

Хотите освоить CMS WordPress?

Получите уроки по дизайну и верстке сайта на WordPress.

Научитесь работать с темами и нарезать макет.

Бесплатный видеокурс по рисованию дизайна сайта, его верстке и установке на CMS WordPress!

Получить в подарок->

*Наведите курсор мыши для приостановки прокрутки.

Перед изучением данной статьи вы можете прочитать предыдущую статью из этой серии — «Позднее статическое связывание: ключевое слово static».

Иногда всё идет не так, как надо. Файлы где-то потерялись, серверы баз данных остались не инициализированы, URL-адреса изменились, XML-файлы повреждены, права доступа настроены неправильно, лимиты на дисковую память превышены и бог знает что еще…

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

Ниже приведено определение простого класса Conf, который сохраняет, извлекает и определяет данные в XML-файле конфигурации.

class Conf {
    private $file;
    private $xml;
    private $lastmatch;

    function __construct( $file ) {
        $this->file = $file;
        $this->xml = simplexml_load_file($file);
    }

    function write() {
        file_put_contents( $this->file, $this->xml->asXML() );
    }

    function get( $str ) {
        $matches = $this->xml->xpath("/conf/item[@name=\"$str\"]");
        if ( count( $matches ) ) {
            $this->lastmatch = $matches[0];
            return (string)$matches[0];
        }
        return null;
    }

    function set( $key, $value ) {
        if ( ! is_null( $this->get( $key ) ) ) {
            $this->lastmatch[0]=$value;
            return;
        }
        $conf = $this->xml->conf;
        $this->xml->addChild('item', $value)->addAttribute( 'name', $key );
    }
}

В классе Conf для доступа к парам «имя — значение» используется расширение PHP SimpleXml. Ниже приведен фрагмент файла конфигурации в формате XML, с которым работает наш класс.

<?xml version="1.0"?>
<conf>
    <item name="user">bob</item>
    <item name="pass">newpass</item>
    <item name="host">localhost</item>
</conf>

Конструктору класса Conf передается имя файла конфигурации, которое далее передается функции simplexml_load_file(). Полученный от функции объект типа SimpleXmlElement сохраняется в свойстве $xml.

В методе get() для нахождения элемента item с заданные атрибутом name используется метод xpath объекта SimpleXmlElement. Значение найденного элемента возвращается в вызывающий код. Метод set() либо меняет значение существующего элемента, либо создает новый. И, наконец, метод write() сохраняет данные о новой конфигурации в исходном файле на диске.

Как и многие коды, приведенные в качестве примеров, код класса Conf крайне упрощен. В частности, в нем не предусмотрена обработка ситуаций, когда файл конфигурации не существует или в него нельзя записать данные. Этот код также слишком «оптимистичен». В нем предполагается, что XML-документ должен быть правильно отформатирован и содержать ожидаемые элементы.

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

1. Мы можем завершить выполнение программы. Это простой, но радикальный выход. В результате наш скромный класс будет виноват в том, что из-за него потерпел неудачу весь сценарий. Хотя такие методы, как __construct() и write(), удачно расположены в коде с целью обнаружения ошибок, у них нет информации, позволяющей решить, как обрабатывать эти ошибки.

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

Во многих PEAR-пакетах сочетаются эти два подхода и возвращается объект ошибок (экземпляр класса PEAR_Error). Наличие этого объекта говорит о том, что произошла ошибка, а подробная информация о ней содержится в самом объекте.

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

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

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

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

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

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

Исключения

С PHP 5 было введено понятие исключений, представляющих собой совершенно другой способ обработки ошибок. Я хочу сказать — совершенно другой для PHP. Но если у вас есть опыт работы с Java или C++, то исключения покажутся вам знакомыми и близкими. Использование исключений позволяет решить все проблемы, о которых мы говорили ранее.

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

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

Общедоступные методы класса Exception

getMessage() — получить строку сообщения, переданную конструктору;
getCode() — получить код ошибки (целое число), который был передан конструктору;
getFile() — получить имя файла, в котором было сгенерировано исключение;
getLine() — получить номер строки, в которой было сгенерировано исключение;
getPrevious() — получить вложенный объект типа Exception;
getTrace() — получить многомерный массив, отслеживающий вызовы метода, которые привели к исключению, включая имя метода, класса, файла и значение аргумента;
getTraceAsString() — получить строковую версию данных, возвращенных методом getTrace();
__toString() — вызывается автоматически, когда объект Exception используется в контексте строки. Возвращает строку, описывающую подробности исключения.

Класс Exception крайне полезен для поиска сообщения об ошибке и информации для отладки (в этом отношении особенно полезны методы getTrace() и getTraceAsString()). На самом деле класс Exception почти идентичен классу PEAR_Error, который мы обсуждали выше. Но в нем сохраняется меньше информации об исключениях, чем есть на самом деле.

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

Совместно с объектом Exception используется ключевое слово throw. Оно останавливает выполнение текущего метода и передает ответственность за обработку ошибок назад в вызывающий код. Давайте подкорректируем метод __construct(), чтобы использовать оператор throw.

function __construct( $file ) {
        $this->file = $file;
        if ( ! file_exists( $file ) ) {
            throw new Exception( "Файл '$file' не существует" );
        }
        $this->xml = simplexml_load_file($file);
    }

Аналогичная конструкция может использоваться и в методе write().

function write() {
        if ( ! is_writeable( $this->file ) ) {
            throw new Exception("Файл '{$this->file}' недоступен для записи");
        }
        file_put_contents( $this->file, $this->xml->asXML() );
    }

Теперь наши методы __construct() и write() могут тщательно проверять ошибки в файле по мере выполнения своей работы. Однако при этом решение о том, как реагировать на любые ошибки, будет приниматься в клиентском коде.

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

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

try {
    $conf = new Conf( dirname(__FILE__)."/conf01.xml" );
    print "user: ".$conf->get('user')."\n";
    print "host: ".$conf->get('host')."\n";
    $conf->set("pass", "newpass");
    $conf->write();
} catch ( Exception $e ) {
    die( $e->__toString() );
}

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

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

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

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

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

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

class XmlException extends Exception {
    private $error;

    function __construct( LibXmlError $error ) {
        $shortfile = basename( $error->file );
        $msg = "[{$shortfile}, строка line {$error->line}, колонка {$error->column}] {$error->message}";
        $this->error = $error;
        parent::__construct( $msg, $error->code );
    }

    function getLibXmlError() {
        return $this->error;
    }
}

class FileException extends Exception { }
class ConfException extends Exception { }

Объект типа LibXmlError создается автоматически, когда средства SimpleXml обнаруживают поврежденный XML-файл. У него есть свойства message и code, и он напоминает класс Exception. Мы пользуется преимуществом этого подобия и используем объект LibXmlError в классе XmlException.

У классов FileException и ConfException не больше функциональных возможностей, чем у родительского класса Exception. Теперь мы можем использовать эти классы в коде и подкорректировать оба метода __construct() и write().

// класс Conf...

    function __construct( $file ) {
        $this->file = $file;
        if ( ! file_exists( $file ) ) {
            throw new FileException( "Файл '$file' не существует" );
        }
        $this->xml = simplexml_load_file($file, null, LIBXML_NOERROR );
        if ( ! is_object( $this->xml ) ) {
            throw new XmlException( libxml_get_last_error() );
        }
		print gettype( $this->xml );
        $matches = $this->xml->xpath("/conf");
        if ( ! count( $matches ) ) {
            throw new ConfException( "Корневой элемент conf не найден" );
        }
    }

    function write() {
        if ( ! is_writeable( $this->file ) ) {
            throw new Exception("Файл '{$this->file}' недоступен для записи");
        }
        file_put_contents( $this->file, $this->xml->asXML() );
    }

Метод __construct() генерирует исключение типа XmlException, FileException или ConfException, в зависимости от вида ошибки, которую он обнаружит.

Обратите внимание на то, что методу simplexml_load_file() передается флаг LIBXML_NOERROR. Это блокирует выдачу предупреждений внутри класса и оставляет программисту свободу действий для их последующей обработки с помощью класса XmlException.

Если обнаружится поврежденный XML-файл, то метод simplexml_load_file() уже не возвращает объект типа SimpleXmlElement. Благодаря классу XmlException в клиентском коде можно будет легко узнать причину ошибки, а с помощью метода libxml_get_last_error() — все подробности этой ошибки.

Метод write() генерирует исключение типа FileException, если свойство $file указывает на файл, недоступный для записи.

Итак, мы установили, что метод __construct() может генерировать одно из трех возможных исключений. Как мы можем этим воспользоваться? Ниже приведен пример кода, в котором создается экземпляр объекта Conf().

class Runner {
    static function init() {
        try {
            $conf = new Conf( dirname(__FILE__)."/conf01.xml" );
            print "user: ".$conf->get('user')."\n";
            print "host: ".$conf->get('host')."\n";
            $conf->set("pass", "newpass");
            $conf->write();
        } catch ( FileException $e ) {
            // Файл не существует, либо недоступен для записи

        } catch ( XmlException $e ) {
            // Поврежденный XML-файл

        } catch ( ConfException $e ) {
            // Некорректный формат XML-файла

        } catch ( Exception $e ) {
            // Ограничитель: этот код не должен никогда вызываться
        }
    }
}

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

Например, если бы вы разместили оператор catch для обработки исключения типа Exception перед операторами для обработки исключений типа XmlException и ConfException, ни один из них никогда бы не был вызван. Причина в том, что оба исключения относятся к типу Exception и поэтому будут соответствовать первому оператору.

Первый оператор catch (FileException) вызывается, если есть проблема с файлом конфигурации (если этот файл не существует или в него нельзя ничего записать).

Второй оператор catch (XmlException) вызвается, если происходит ошибка при синтаксическом анализе XML-файла (например, если какой-то элемент не закрыт).

Третий оператор catch (ConfException) вызывается, если корректный в плане формата файл XML не содержит ожидаемый корневой элемент conf.

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

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

try {
	// ...
} catch ( FileException $e ) {
	throw $e;
}

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

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

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

PHP Fatal error: Uncaught exception 'FileException' with message 'file 'nonexistent/not_there.xml' does not exist' in ...

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

Метод write() в нашем примере «знает», когда попытка сделать запись заканчивается неудачей и почему, но «не знает», что с этим делать. Именно так и должно быть.

Если бы мы сделали класс Conf более «сведущим», чем он есть в настоящее время, он бы потерял свою универсальность и перестал бы быть повторно используемым.

На этом пока всё. В следующем материале мы поговорим о ключевом слове final и о том, как предотвратить наследование в PHP (скоро будет доступен).

Понравился материал и хотите отблагодарить?
Просто поделитесь с друзьями и коллегами!

Смотрите также:

PHP: Получение информации об объекте или классе, методах, свойствах и наследовании

PHP: Получение информации об объекте или классе, методах, свойствах и наследовании

CodeIgniter: жив или мертв?

CodeIgniter: жив или мертв?

Функции обратного вызова, анонимные функции и механизм замыканий

Функции обратного вызова, анонимные функции и механизм замыканий

Применение функции к каждому элементу массива

Применение функции к каждому элементу массива

Слияние массивов. Преобразование массива в строку

Слияние массивов. Преобразование массива в строку

Деструктор и копирование объектов с помощью метода __clone()

Деструктор и копирование объектов с помощью метода __clone()

Эволюция веб-разработчика или Почему фреймворк - это хорошо?

Эволюция веб-разработчика или Почему фреймворк — это хорошо?

Магические методы в PHP или методы-перехватчики (сеттеры, геттеры и др.)

Магические методы в PHP или методы-перехватчики (сеттеры, геттеры и др.)

PHP: Удаление элементов массива

PHP: Удаление элементов массива

Ключевое слово final (завершенные классы и методы в PHP)

Ключевое слово final (завершенные классы и методы в PHP)

50 классных сервисов, программ и сайтов для веб-разработчиков

50 классных сервисов, программ и сайтов для веб-разработчиков

Наверх

PHP is the language used to build websites on the internet for over ten years. Although, there are a lot of people who think that it’s time to move into something else, PHP is a dynamic programming language, which means that it can be adapted to the current needs. And the PHP Core team has been excellent in bringing out new features that make PHP an attractive language in this time and age.

The flexibility in the PHP language makes it easy to handle things like exceptions in code, which are the out of the ordinary scenarios that can occur. They can be caused by some unexpected input, a bug, or some other problem. PHP 8 is a new version of this language that was released on 26 November 2020. The new version has been adapted to be more secure and handle exceptions better than the previous versions.

Potential exceptions/errors are enclosed inside a try block if exception is encountered, will be thrown to catch or finally block. PHP usually handles exceptions in a separate catch block for each different type of exception.

In this post, you can gain knowledge about what exactly is exception handling, and how it works.

Below are the topics that shall be covered in this blog:

  1. When, Where, and How to use Exceptions and Errors in PHP?
  2. Error Class

  3. Exception Class

  4. Custom Exception

  5. Multiple Exception

  6. Global Exception Handler

  7. Non-Capturing Catches

#1 When, Where, and How to use Exceptions and Errors in PHP?

PHP 7 introduced the new Throwable interface to unite the exception branches Exception and Error. The entire PHP exception hierarchy is as follows:

interface Throwable
    |- Error implements Throwable
        |- CompileError extends Error
            |- ParseError extends CompileError
        |- TypeError extends Error
            |- ArgumentCountError extends TypeError
        |- ArithmeticError extends Error
            |- DivisionByZeroError extends ArithmeticError
        |- AssertionError extends Error
    |- Exception implements Throwable
        |- ClosedGeneratorException
        |- DOMException
        |- ErrorException
        |- IntlException
        |- LogicException
            |- BadFunctionCallException
                |- BadMethodCallException
            |- DomainException
            |- InvalidArgumentException
            |- LengthException
            |- OutOfRangeException
        |- PharExceptionaddition
        |- ReflectionException
        |- RuntimeException
            |- mysqli_sql_exception
            |- OutOfBoundsException
            |- OverflowException
            |- PDOException
            |- RangeException
            |- UnderflowException
            |- UnexpectedValueException
        |- Custom Exception

To catch both exceptions and errors in PHP 8, add a catch block for Exception after catching Throwable first.

try 
{ 
    // Code that may throw an Exception or Error. 
} catch (Throwable $t) 
{ 
   // Executed only in PHP 7 and more
}

#2 Error Class

Error Class is the base class for all internal PHP errors. Errors can be caught in  try/catch block as explained above. Few errors will throw a specific subclass of Error such as Parse Error, Type Error, and so on.

Here are the list of various types of errors (we have covered only the most common ones):

  1. Parse/Syntax Error
  2. Type Error

  3. Arithmetic Error
  4. Assertion Error

  5. Value Error

a. Parse/Syntax Error

A syntax/parse error in the code while compilation, a Parse error is thrown. If a code contains an error, the PHP parser cannot interpret the code and it stops working.

Let’s look into a simple example for understanding Parse error.

Code:

<?php
    $x = "Exception";
    y = "Handling";
    echo $x . ' ' . y;
?>

Output:

syntax error, unexpected '=' in line 3

b. Type Error

When data type mismatch happens in PHP while doing an operation, a Type error is thrown. There are three scenarios where this type of error is thrown:

  • Invalid number of arguments passed to a built-in function.
  • Value returned from a function doesn’t match the declared function return type.
  • Argument type passed to a function doesn’t match the declared parameter type.

Let’s look into a simple example for understanding Type error.

Code:

<?php
function add(int $x, int $y)
{
    return $x + $y;
}
try {
    $value = add('Type', 10);
}
catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}
?>

Output:

Argument 1 passed to add() must be of the type integer, string given.

c. Arithmetic Error

Occurrence of error while performing a mathematical operation, bit shifting by a negative number or calling an intdiv() function, the Arithmetic error is thrown.

Example With Division Operator:

<?php
try {
    intdiv(PHP_INT_MIN, -1);
}
catch (ArithmeticError $e) {
    echo $e->getMessage();
}
?>

Output:

Division of PHP_INT_MIN by -1 is not an integer

Example With Modulo Operator:

<?php
try {
    $x = 4;
    $y = 0;
    $result = $x%$y;
}
catch (DivisionByZeroError $e) {
   echo $e->getMessage();
}
?>

Output:

Modulo by zero error

Example With Division Operator Which Returns INF:

<?php
try {
    $x      = 4;
    $y      = 0;
    $result = $x / $y;
}
catch (DivisionByZeroError $e) {
    echo $e->getMessage();
}
?>

Output:

INF

Explanation:

There is a very minute difference in the above two examples. The first one contains the Modulo operator and the second one has the  Division operator. If any variable divided by zero will return an error, Division by zero error. When any variable divided by zero with modulo operator returns Modulo by zero error and the variable divided by zero with the division operator also returns anyone the following- INF, NAN, -INF

d. Assertion Error

When an assert() call fails or let’s say when the condition inside the assert() call doesn’t meet, the Assertion error is thrown. String description emits E_DEPRECATED message from PHP 7.2 version. The Assertion Error thrown by assert() will be sent to catch block only if assert.exception=on is enabled in php.ini.

Let’s look into a simple example for understanding assertion error.

Code:

<?php
try {
    $x      = 1;
    $y      = 2;
    $result = assert($x === $y);
    if (!$result) {
        throw new DivisionByZeroError('Assertion error');
    }
}
catch (AssertionError $e) {
    echo $e->getMessage();
}
?>

Output:

Assertion error

e. Value Error

When the type of the argument is correct and the value of it is incorrect, a value error is thrown. These type of errors occurs when:

  • Passing a negative value when the function expects a positive value.
  • Passing an empty string or array when function expects a non-empty string/array.

Let’s look into a simple examples for understanding value error.

Code:

<?php
    $x = strpos("u", "austin", 24);
    var_dump($x);
?>

Output:

[Mon Feb 22 20:59:04 2021] 
PHP Warning:  strpos(): Offset not contained in string in /home/ubuntu/value_error.php on line 2

Code:

<?php
    $x = array_rand(array(), 0);
    var_dump($x);
?>
[Mon Feb 22 21:04:14 2021] PHP Warning:  array_rand(): Array is empty in /home/ubuntu/index.php on line 2

#3 Exception Class

Exception Class occurs when a specified/exceptional error condition changes the normal flow of the code execution.

Exception handling comprises five components i.e, try block, exception, throw, catch block, and finally block.

Let’s look into a simple example for understanding the above-mentioned components

Code:

<?php
function add($x,$y) {
    if (is_numeric($x) == False) {
        throw new Exception('Num1  is not a number');
    }
    if (is_numeric($y) == False) {
        throw new RuntimeException('Num2 is not a number');
    }
    return $x + $y;
}

try {
    echo add(5,10). "\n";
    echo add(5,k). "\n";
} 

catch (Exception $e) {
    echo 'Exception caught: ', $e->getMessage(), "\n";
} 

finally {
    echo "Finally Block.\n";
}

// Continue execution
echo "Hello World\n";
?>

Output:

15
Exception caught: Num2 is not a number
Finally Block.
Hello World

Explanation:

Our example is about adding two numbers and we assumed that we might get non-numeric value as input which would raise an error.

  • We created a function called addition with Exception for non-numeric values and If encountered with the exception, will throw it with the exception message.

  • We called the addition function inside a Try block so that non-numeric value error won’t affect/stop the whole execution. All potential exceptions should be enclosed inside a try block.

  • The Catch block will receive any exceptions thrown from the try block and execute the code inside the block. In our case will print the error message ‘Caught exception: Num2 is not a number’.

  • The Finally block will be executed irrespective of whether we received exception or not.

#4 Custom Exception

We use custom exception to make it clear what is being caught in the catch block and to understand the exception in a better way. The custom exception class inherits properties from the PHP exception’s class where you can add your custom functions too. To easily understand the exceptions we can use custom exceptions and can log it for the future use.

If you just want to capture a message, you can do it at follows:

try {
    throw new Exception("This is an error message");
}
catch(Exception $e) {
    print $e->getMessage();
}

If you want to capture specific error messages which could be easy to understand you can use:

try {
    throw new MyException("Error message");
}
catch(MyException $e) {
    print "Exception caught: ".$e->getMessage();
}
catch(Exception $e) {
    print "Error: ".$e->getMessage();
}

Code:

<?php

class customStringException extends Exception
{
    public function myerrorMessage()
    {
        //error message
        $errorMsg = 'Error on line ' . $this->getLine() . ': <b>' . $this->getMessage() . '</b> is not a String';
        return $errorMsg;
    }
}

class customNumericException extends Exception
{
    public function myerrorMessage()
    {
        //error message
        $errorMsg = 'Error on line ' . $this->getLine() . ': <b>' . $this->getMessage() . '</b> is not an Integer';
        return $errorMsg;
    }
}

function typeCheck($name, $age)
{
    if (!is_string($name)) {
        throw new customStringException($name);
    }
    if (!is_numeric($age)) {
        throw new customNumericException($age);
    } else {
        echo $name . " is of age " . $age;
    }
}

try {
    echo typeCheck("Sara", 25) . "\n";
    echo typeCheck(5, 10) . "\n";
}
catch (customStringException $e) {
    echo $e->myerrorMessage();
}
catch (customNumericException $e) {
    echo $e->myerrorMessage();
}

?>

Output:

Sara is of age 25
Error on line 21: 5 is not a String

Explanation:

The above example is on a type check, we have two variables name and age . Let’s assume $name is of type string and $age is of type integer and we assumed that we might get any type of value as input which would raise an error.

  • We created a function called typeCheck to check the type of the variable with exception. If the condition fails it will throw an exception with an Exception message that we have customized.

  • We created a class customStringException to create a custom exception handler with a function called errorMessage which would be called when an exception occurs.

  • Here we called the typeCheck function inside a try block so that if any error is encountered it could be caught in the catch block.

#5 Multiple Exception

You can also handle multiple exception in a single catch block using the pipe ‘|’ symbol like this:

try {
    $error = "Foo / Bar / Baz Exception"; throw new MyBazException($error); 
} 
catch(MyFooException | MyBarException | MyBazException $e) { 
    //Do something here 
}

#6 Global Exception Handler

In Global Exception Handler, it sets the default exception handler if an exception is not caught within a try/catch block. If no other block is invoked the set_exception_handler function can set a function which will be called in the place of catch. Execution will stop after the exception_handler is called.

set_exception_handler Syntax:

set_exception_handler ( callable $exception_handler ) : callable

<?php

function exception_handler($exception)
{
    echo "Uncaught exception: ", $exception->getMessage(), "\n";
}

set_exception_handler('exception_handler');

throw new Exception('Uncaught Exception');

echo "Not Executed\n";
?>

#7 Non-Capturing Catches

Before PHP version 8, if you wanna catch an exception you will need to store it in a variable irrespective of its usage. You usually must specify the type whenever you use a catch exception. With this Non-Capturing Catch exception, you can ignore the variable.

Example:

try {
    // Something goes wrong
} 
catch (MyException $exception) {
    Log::error("Something went wrong");
}

You can put it this way in PHP 8:

try {
    // Something goes wrong
} 
catch (MyException) {
    Log::error("Something went wrong");
}

Summary:

Here we have explained the basic usage of exceptions and how to implement it in detail.You can quickly track the errors and fix the exceptions that have been thrown in your code. I hope this blog might be useful for you to learn what exception is and the correct usage of it.

If you would like to monitor your PHP code, you can try Atatus here.

Если ты изучаешь ООП, то наверняка натыкался на понятие исключения, а может даже уже видел его где-то в коде. В этом уроке я постараюсь объяснить, что такое исключение в PHP и зачем они нужны. Расскажу когда, какое и как правильно применять то или иное исключение при разработке.

Что такое исключение в PHP

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

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

// Где-то (модель или сервис)
public function delete($id)
{
 $category = Category::find($id);
 // Если категория не найдена - кидаем исключение
 if (!$category) throw new Exception('Page Not Found!');
 // Если в категории есть посты - кидаем исключение
 if (count($category->posts) > 0) throw new Exception('Cannot delete category with posts!');
 // Если всё хорошо - продолжаем выполнение кода
 // Удаляем категорию
}


// В контроллере
public function deleteAction($id)
{
  try {
  // Если метод delete() из модели возвращает true 
    $model->delete($id);
  } catch (Exception $e) {
  	// Если false - ловим брошенное из модели исключение 
    echo $e->getMessage();
    // Или вывести в уведомление через сессию, например
    // Session::set('error', $e->getMessage());
  }
}

Согласитесь, удобно. Вместо того, чтобы просто возвращать false в случае, когда срабатывает условие if (count($category->products) > 0), лучше кинуть исключение и как-то оповестить пользователя о каких-либо возникших исключительных ситуациях. Если же мы просто вернём false, то мы сами со временем не сможем понять, что именно там случилось и почему этот метод не работает. Поэтому, я советую всегда пользоваться исключениями, но слишком не увлекаясь этим делом.

Класс Exception

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

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

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

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

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

// Класс User
class User
{
  private $name;

  public function setName($name)
  {
    if (!$name) throw new InvalidArgumentException('Имя не задано!');
    if (strlen($name) < 3) throw new LengthException('Имя должно быть больше 3-х символов!');
    $this->name = $name;
  }

  public function getName(): string
  {
    return $this->name;
  }

	// ...
}

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

И так, метод, который вызывает метод, в котором в свою очередь может быть брошено исключение, должен сам его обрабатывать. Обработка исключения производится при помощи операторов try - catch. Блок кода, который может поймать исключение, располагается после try. Блок кода, который обрабатывает исключение, располагается после оператора catch. В переводе с английского try означает «пытаться», что очень точно отражает суть этого оператора, ведь мы пытаемся выполнить блок кода после него, а если не получается то выполняется блок кода после catch. Catch переводится как «ловить». Он фактически «ловит» сгенерированное исключение. В примере ниже мы ловим исключение из метода setName() класса User из примера выше:

// Где-то ловим исключение и обрабатываем его
try {
  $user = new User();
  $user->setName('John');
  // Случится исключение InvalidArgumentException
	$user->setName('');
  // Случится исключение LengthException
  $user->setName('Jo');
  echo $user->getName();
} catch (Exception $e) {
    echo "Message: {$e->getMessage()}<br>
    Code: {$e->getCode()}<br>
    File: {$e->getFile()}<br>
    Line: {$e->getLine()}";
}

Ловим исключение в блоке try:

try {
  // ...

  $config = "config.php";
  if (!file_exists($config)) {
    throw new Exception("Configuration file not found.");
  }

  // ...
} catch (Exception $e) {
  echo $e->getMessage();
  die();
}

Но так делать не рекомендуется (try/catch и throw на одном уровне). В этом случае проще написать if!

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

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

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

Пример собственного класса исключения:

<?php
/**
 * Определим свой класс исключения
 */
class MyException extends Exception
{
  // Переопределим исключение так, что параметр message станет обязательным
  public function __construct($message, $code = 0, Exception $previous = null) {
    // некоторый код 

    // убедитесь, что все передаваемые параметры верны
    parent::__construct($message, $code, $previous);
  }

  // Переопределим строковое представление объекта.
  public function __toString() {
    return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
  }

  public function customFunction() {
    echo "Мы можем определять новые методы в наследуемом классе\n";
  }
}

Полная иерархия исключений в PHP

Throwable (интерфейс)
├── Exception (реализует Throwable)
│   ├── LogicException (расширяет Exception)
│   │   ├── BadFunctionCallException (расширяет LogicException)
│   │   │   └── BadMethodCallException (расширяет BadFunctionCallException)
│   │   ├── DomainException (расширяет LogicException)
│   │   ├── InvalidArgumentException (расширяет LogicException)
│   │   ├── LengthException (расширяет LogicException)
│   │   └── OutOfRangeException (расширяет LogicException)
│   └── RuntimeException (расширяет Exception)
│       ├── OutOfBoundsException (расширяет RuntimeException)
│       ├── OverflowException (расширяет RuntimeException)
│       ├── RangeException (расширяет RuntimeException)
│       ├── UnderflowException (расширяет RuntimeException)
│       └── UnexpectedValueException (расширяет RuntimeException)
└── Error (реализует Throwable)
    ├── AssertionError (расширяет Error)
    ├── ParseError (расширяет Error)
    └── TypeError (расширяет Error)	

Throwable

Throwable — это даже не исключение, а интерфейс, который реализуют все остальные рассматриваемые классы. Добавлен в PHP7.

Exception

Базовый класс для исключений. Стандартная библиотека SPL вводит две группы исключений, два надкласса: для исключений в логике: LogicException и исключений времени исполнении RuntimeException.

LogicException

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

BadFunctionCallException

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

BadMethodCallException

Подкласс BadFunctionCallException. Аналогично ему используется для методов, которые не существуют или которым передано неверное число параметров. Всегда используйте внутри __call(), в основном для этого оно и применяется.

Пример использования этих двух исключений:

// Для метода в __call 
class Foo 
{     
  public function __call($method, $args)     
  {         
   switch ($method) {
        case 'someExistentClass': /* do something positive... */ break;
        default:
        	throw new BadMethodCallException('Метод ' . $method . ' не может быть вызван');
        }
    }   
}
   
// процедурный подход function 
foo($arg1, $arg2) 
{
   $func = 'do' . $arg2;
   if (!is_callable($func)) {         
    throw new BadFunctionCallException('Функция ' . $func . ' не может быть вызвана');     
  } 
}

DomainException

Если в коде подразумеваются некие ограничения для значений, то это исключение можно вызывать, когда значение выходит за эти ограничения. Например, у вас дни недели обозначаются числами от 1 до 7, а ваш метод получает внезапно на вход 0 или 9, или, скажем, вы ожидаете число, обозначающее количество зрителей в зале, а получаете отрицательное значени. В таких случаях и вызывается DomainException. Также можно использовать для разных проверок параметров, когда параметры нужных типов, но при этом не проходят проверку на значение. Например:

if ($a > 5)
    throw new DomainException ("Переменная a должна быть меньше 5");

InvalidArgumentException

Вызываем, когда ожидаемые аргументы в функции/методе некорректно сформированы. Например, ожидается целое число, а на входе строка или ожидается GET, а пришел POST и т.п.

public function foo($number) {
  if(!is_numeric($number)) {
    throw new InvalidArgumentException('На входе ожидалось целое число!');
  }
}

LengthException

Вызываем, если длина чего-то слишком велика или мала. Например, имя файла слишком короткое или длина строки слишком большая.

RuntimeException

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

OutOfBoundsException

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

Пример использования в реализации ArrayAccess:

public function offsetGet($offset) {
  if(!isset($this->objects[$offset])) {
    throw new OutOfBoundsException("Смещение '$offset' вышло из заданного диапазона");
  }

  return $this->objects[$offset];
}

OutOfRangeException

Используется, когда встречаем некорректный индекс, но на этот раз ошибка должна быть обнаружена ещё до прогона кода, например, если мы пытаемся адресовать элемент массива, который в принципе не поддерживается. То есть если функция, возвращающая день недели по его индексу от 1 до 7, получает внезапно 9, то это DomainException — ошибка логики, а если у нас есть массив с днями недели с индексами от 1 до 7, а мы пытаемся обратиться к элементу с индексом 9, то это уже OutOfRangeException.

OverflowException

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

UnderflowException

Обратная OverflowException ситуация, когда, например, класс-контейнер имеет недостаточно элементов для осуществления операции. Например, когда он пуст, а вы пытаетесь удалить элемент.

RangeException

Вызывается, когда значение выходит за границы некоего диапазона. Похоже на DomainException, но используется при возврате из функции, а не при входе. Если мы не можем вернуть легитимное значение, мы выбрасываем это исключение. То есть, к примеру, функция у вас принимает целочисленный индекс и использует другую функцию, чтоб получить некое значение по этой сущности. Та функция вернула null, но ваша функция не имеет права возвращать Null. В таком случае можно применить это исключение. То есть между ними примерно такая же разница, как между OutOfBoundsException и OutOfRangeException.

UnexpectedValueException

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

Важно, что, в отличие от InvalidArgumentException, здесь мы имеем дело, в основном, с возвращаемыми значениями. Часто мы заранее не можем быть уверены в том, что придет в ответе от функции (особенно сторонней). Скажем, мы используем некую стороннюю функцию, использующую API ВКонтакте, и возвращющую количество постов для пользователя. Она всегда возвращала целое неотрицательное число, и вдруг неожиданно возвращает отрицательное число. Это не соответствует документации. Соответственно, чтобы подстраховаться от таких ситуаций, мы можем проверять результат такого вызова и, если он отличается от ожидаемого, выбрасывать UnexpectedValueException.

Вот пример, когда у нас есть список констант, и функция getValueOfX должна гарантированно возвращать значение одной из них.

const TYPE_FOO = 'foo';
const TYPE_BAR = 'bar';

public function doSomething($y) {
  $x = ModuleUsingSomeExternalAPI::getValueOfX($y);
  if($x != self::TYPE_FOO || $x != self::TYPE_BAR) {
    throw new UnexpectedValueException('Параметр должен быть в виде TYPE_* констант');
  }
}

Error

Добавлено в PHP7 для обработки фатальных ошибок. То есть многие из ошибок, которые раньше приводили к Fatal Error, сейчас могут обрабатываться в блоках try/catch. Эти ошибки вызываются самим PHP, нет нужды их вызывать, как Exception. Класс Error имеет три подкласса:

AssertionError

Вызывается, когда условие, заданное методом assert(), не выполняется.

ParseError

Для ошибок парсинга, когда подключаемый по include/require код вызывает ошибку синтаксиса, ошибок функции eval() и т.п.

try {
   require 'file-with-syntax-error.php';
} catch (ParseError $e) {
   // обработка ошибки
}

TypeError

Используется для ошибок несоответствия типов данных. В PHP7 введена опциональная строгая типизация. Вот для поддержки ошибок, связанных с ней, и служит этот класс. Например, если функция ожидает на входе аргумент типа int, а вы ее вызываете со строковым аргументом.

if (!is_string($name)) throw new TypeError('Имя должно быть стройкой!');

Учебный пример, в котором есть примеры использования всех классов исключений:

class Example
{
  protected $author;  
  protected $month;  
  protected $goals = [];
  
  public function exceptions(int $a, int $b): int
  {
    $valid_a = [7, 8, 9];
    if (!is_int($a)) {
      throw new InvalidArgumentException("a должно быть целочисленным!");
    }
    if ($a > 5 || !in_array($a, $valid_a, true)) {
      throw new DomainException("a не может быть больше 5");
    }
    
    $c = $this->getByIndex($a);
    if (!is_int($c)) {
      throw new RangeException("c посчитался неправильно!");
    } else {
      return $c;
    }      
  }
  
  private function getByIndex($a)
  {
    return ($a < 100) ? $a + 1 : null;
  }
  
  public function deleteNextGoal()
  {
    if (empty($this->goals)) {
      throw new UnderflowException("Нет цели, чтобы удалить!");
    } elseif (count($this->goals) > 100000) {
      throw new OverflowException("Система не может оперировать больше, чем 100000 целями одновременно!");
    } else {
      array_pop($this->goals);
    }
  }
  
  public function getGoalByIndex($i)
  {
    if (!isset ($this->goals[$i])) {
      throw new OutOfBoundsException("Нет цели с индексом $i"); // легитимные значения известны только во время выполнения
    } else {
      return $this->goals[$i];
    }
  }
  
  public function setPublicationMonth(int $month)
  {
    if ($month < 1 || $month > 12) {
      throw new OutOfRangeException("Месяц должен быть от 1 до 12!"); // легитимные значения известны заранее
    }
    $this->month = $month;
  }
  
  public function setAuthor($author)
  {
    if (mb_convert_case($author, MB_CASE_UPPER) !== $author) {
      throw new InvalidArgumentException("Все буквы имени автора должны быть заглавными");
    } else {
      if (mb_strlen($author) > 255) {
        throw new LengthException("Поле автор не должно быть больше 255 сиволов!");
      } else {
        $this->author = $author;
      }
    }
  }
  
  public function __call(string $name, array $args)
  {
    throw new BadMethodCallException("Метод Example>$name() не существует");
  }
}

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

Документация по исключениям в PHP здесь .

Ещё почитать на гитхабе .

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Peugeot 408 ошибка давления масла
  • Php запретить вывод ошибок
  • Phf ошибка упп шнайдер
  • Peugeot 408 ошибка p1337
  • Php как убрать сообщения об ошибках

  • Добавить комментарий

    ;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: