Контроль ошибок php

Оператор управления ошибками

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

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

Внимание

До версии PHP 8.0.0 функция error_reporting(), вызываемая внутри пользовательского обработчика ошибок,
всегда возвращала 0, если ошибка была подавлена оператором @.
Начиная с PHP 8.0.0, она возвращает значение E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE.

Любое сообщение об ошибке, сгенерированное выражением, доступно
в элементе массива "message", возвращаемого error_get_last().
Результат этой функции будет меняться при каждой ошибке, поэтому его необходимо проверить заранее.

<?php
// Преднамеренная ошибка при работе с файлами
$my_file = @file ('non_existent_file') or
die (
"Ошибка при открытии файла: сообщение об ошибке было таким: '" . error_get_last()['message'] . "'");// работает для любых выражений, а не только для функций
$value = @$cache[$key];
// В случае если ключа $key нет, сообщение об ошибке (notice) не будет отображено?>

Замечание:

Оператор @ работает только с
выражениями.
Есть простое правило: если что-то возвращает
значение, значит вы можете использовать перед ним оператор
@. Например, вы можете использовать @ перед
именем переменной, произвольной функцией или вызовом include и так далее. В то же время вы не можете использовать этот оператор
перед определением функции или класса, условными конструкциями, такими как if,
foreach и т.д.

Внимание

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

taras dot dot dot di at gmail dot com

15 years ago

I was confused as to what the @ symbol actually does, and after a few experiments have concluded the following:

* the error handler that is set gets called regardless of what level the error reporting is set on, or whether the statement is preceeded with @

* it is up to the error handler to impart some meaning on the different error levels. You could make your custom error handler echo all errors, even if error reporting is set to NONE.

* so what does the @ operator do? It temporarily sets the error reporting level to 0 for that line. If that line triggers an error, the error handler will still be called, but it will be called with an error level of 0

Hope this helps someone

M. T.

13 years ago

Be aware of using error control operator in statements before include() like this:

<?PHP(@include("file.php"))
OR die(
"Could not find file.php!");?>

This cause, that error reporting level is set to zero also for the included file. So if there are some errors in the included file, they will be not displayed.

Anonymous

10 years ago

This operator is affectionately known by veteran phpers as the stfu operator.

anthon at piwik dot org

12 years ago

If you're wondering what the performance impact of using the @ operator is, consider this example. Here, the second script (using the @ operator) takes 1.75x as long to execute...almost double the time of the first script.

So while yes, there is some overhead, per iteration, we see that the @ operator added only .005 ms per call. Not reason enough, imho, to avoid using the @ operator.

<?php

function x() { }

for (
$i = 0; $i < 1000000; $i++) { x(); }

?>



real 0m7.617s

user 0m6.788s

sys 0m0.792s

vs

<?php

function x() { }

for (
$i = 0; $i < 1000000; $i++) { @x(); }

?>



real 0m13.333s

user 0m12.437s

sys 0m0.836s

gerrywastaken

14 years ago

Error suppression should be avoided if possible as it doesn't just suppress the error that you are trying to stop, but will also suppress errors that you didn't predict would ever occur. This will make debugging a nightmare.

It is far better to test for the condition that you know will cause an error before preceding to run the code. This way only the error that you know about will be suppressed and not all future errors associated with that piece of code.

There may be a good reason for using outright error suppression in favor of the method I have suggested, however in the many years I've spent programming web apps I've yet to come across a situation where it was a good solution. The examples given on this manual page are certainly not situations where the error control operator should be used.

jcmargentina at gmail dot com

4 years ago

Please be aware that the behaviour of this operator changed from php5 to php7.

The following code will raise a Fatal error no matter what, and you wont be able to suppress it

<?phpfunction query()
{
$myrs = null;
$tmp = @$myrs->free_result();

return

$tmp;
}
var_dump(query());

echo

"THIS IS NOT PRINT";
?>

more info at: https://bugs.php.net/bug.php?id=78532&thanks=3

dkellner

7 years ago

There is no reason to NOT use something just because "it can be misused". You could as well say "unlink is evil, you can delete files with it so don't ever use unlink".

It's a valid point that the @ operator hides all errors - so my rule of thumb is: use it only if you're aware of all possible errors your expression can throw AND you consider all of them irrelevant.

A simple example is
<?php

$x

= @$a["name"];?>
There are only 2 possible problems here: a missing variable or a missing index. If you're sure you're fine with both cases, you're good to go. And again: suppressing errors is not a crime. Not knowing when it's safe to suppress them is definitely worse.

man13or at hotmail dot fr

4 years ago

Quick debugging methods :

@print($a);
is equivalent to
if isset($a) echo $a ;

@a++;
is equivalent to
if isset($a) $a++ ;
else $a = 1;

Ryan C

2 years ago

It's still possible to detect when the @ operator is being used in the error handler in PHP8. Calling error_reporting() will no longer return 0 as documented, but using the @ operator does still change the return value when you call error_reporting().

My PHP error settings are set to use E_ALL, and when I call error_reporting() from the error handler of a non-suppressed error, it returns E_ALL as expected.

But when an error occurs on an expression where I tried to suppress the error with the @ operator, it returns: E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR (or the number 4437).

I didn't want to use 4437 in my code in case it changes with different settings or future versions of PHP, so I now use:

<?php
function my_error_handler($err_no, $err_msg, $filename, $linenum) {
if (
error_reporting() != E_ALL) {
return
false; // Silenced
}// ...
}
?>

If the code needs to work with all versions of PHP, you could check that error_reporting() doesn't equal E_ALL or 0.

And, of course, if your error_reporting settings in PHP is something other than E_ALL, you'll have to change that to whatever setting you do use.

darren at powerssa dot com

13 years ago

After some time investigating as to why I was still getting errors that were supposed to be suppressed with @ I found the following.

1. If you have set your own default error handler then the error still gets sent to the error handler regardless of the @ sign.

2. As mentioned below the @ suppression only changes the error level for that call. This is not to say that in your error handler you can check the given $errno for a value of 0 as the $errno will still refer to the TYPE(not the error level) of error e.g. E_WARNING or E_ERROR etc

3. The @ only changes the rumtime error reporting level just for that one call to 0. This means inside your custom error handler you can check the current runtime error_reporting level using error_reporting() (note that one must NOT pass any parameter to this function if you want to get the current value) and if its zero then you know that it has been suppressed.
<?php
// Custom error handler
function myErrorHandler($errno, $errstr, $errfile, $errline)
{
if (
0 == error_reporting () ) {
// Error reporting is currently turned off or suppressed with @
return;
}
// Do your normal custom error reporting here
}
?>

For more info on setting a custom error handler see: http://php.net/manual/en/function.set-error-handler.php
For more info on error_reporting see: http://www.php.net/manual/en/function.error-reporting.php

auser at anexample dot com

13 years ago

Be aware that using @ is dog-slow, as PHP incurs overhead to suppressing errors in this way. It's a trade-off between speed and convenience.

bohwaz

12 years ago

If you use the ErrorException exception to have a unified error management, I'll advise you to test against error_reporting in the error handler, not in the exception handler as you might encounter some headaches like blank pages as error_reporting might not be transmitted to exception handler.

So instead of :

<?phpfunction exception_error_handler($errno, $errstr, $errfile, $errline )
{
throw new
ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");

function

catchException($e)
{
if (
error_reporting() === 0)
{
return;
}
// Do some stuff
}set_exception_handler('catchException');?>

It would be better to do :

<?phpfunction exception_error_handler($errno, $errstr, $errfile, $errline )
{
if (
error_reporting() === 0)
{
return;
}

throw new

ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");

function

catchException($e)
{
// Do some stuff
}set_exception_handler('catchException');?>

programming at kennebel dot com

16 years ago

To suppress errors for a new class/object:

<?php
// Tested: PHP 5.1.2 ~ 2006-10-13

// Typical Example

$var = @some_function();// Class/Object Example
$var = @new some_class();// Does NOT Work!
//$var = new @some_class(); // syntax error
?>

I found this most useful when connecting to a
database, where i wanted to control the errors
and warnings displayed to the client, while still
using the class style of access.

frogger at netsurf dot de

18 years ago

Better use the function trigger_error() (http://de.php.net/manual/en/function.trigger-error.php)
to display defined notices, warnings and errors than check the error level your self. this lets you write messages to logfiles if defined in the php.ini, output
messages in dependency to the error_reporting() level and suppress output using the @-sign.

karst dot REMOVETHIS at onlinq dot nl

8 years ago

While you should definitely not be too liberal with the @ operator, I also disagree with people who claim it's the ultimate sin.

For example, a very reasonable use is to suppress the notice-level error generated by parse_ini_file() if you know the .ini file may be missing.
In my case getting the FALSE return value was enough to handle that situation, but I didn't want notice errors being output by my API.

TL;DR: Use it, but only if you know what you're suppressing and why.

ricovox

6 years ago

What is PHP's behavior for a variable that is assigned the return value of an expression protected by the Error Control Operator when the expression encounteres an error?

Based on the following code, the result is NULL (but it would be nice if this were confirmed to be true in all cases).

<?php

$var

= 3;
$arr = array(); $var = @$arr['x']; // what is the value of $var after this assignment?

// is it its previous value (3) as if the assignment never took place?
// is it FALSE or NULL?
// is it some kind of exception or error message or error number?

var_dump($var); // prints "NULL"?>

fy dot kenny at gmail dot com

3 years ago

* How to make deprecated super global variable `$php_errormsg` work

>1. modify php.ini
>track_errors = On
>error_reporting = E_ALL & ~E_NOTICE
>2. Please note,if you already using customized error handler,it will prompt `undefined variable`
>please insert code`set_error_handler(null);` before executing code, e.g:
>```php
>set_error_handler(null);
>$my_file = @file ('phpinfo.phpx') or
>die ("<br>Failed opening file: <br>\t$php_errormsg");
>```

>(c)Kenny Fang

manisha at mindfiresolutions dot com

8 years ago

Prepending @ before statement like you are doing a crime with yourself.

Joey

5 years ago

In PHP it is extremely beneficial to turn all notices into exceptions. This helps in creating bug free code through finding errors sooner rather than later. It also helps reduce the impact of bugs as code when entering an erroneous state ends sooner rather than later and at all. In the worst case without that you can have much more scenarios where code fails yet appears as though it succeeded.

However there are rare cases in which notices and warnings are produced where the above behavour might be unproductive. Worse yet your error handling will kick out that exception before the function gets to return.

They are rare cases such as socket handling where certain states are expressed through errors causing ambiguity.

Anonymous

9 years ago

I was wondering if anyone (else) might find a directive to disable/enable to error operator would be a useful addition. That is, instead of something like (which I have seen for a few places in some code):

<?phpif (defined(PRODUCTION)) {
@function();
}
else {
function();
}
?>

There could be something like this:

<?phpif (defined(PRODUCTION)) {
ini_set('error.silent',TRUE);
}
else {
ini_set('error.silent',FALSE);
}
?>

nospam at blog dot fileville dot net

16 years ago

If you want to log all the error messages for a php script from a session you can use something like this:
<?php
session_start
();
function
error($error, $return=FALSE) {
global
$php_errormsg;
if(isset(
$_SESSION['php_errors'])) {
$_SESSION['php_errors'] = array();
}
$_SESSION['php_errors'][] = $error; // Maybe use $php_errormsg
if($return == TRUE) {
$message = "";
foreach(
$_SESSION['php_errors'] as $php_error) {
$messages .= $php_error."\n";
}
return
$messages; // Or you can use use $_SESSION['php_errors']
}
}
?>
Hope this helps someone...

Anonymous

16 years ago

error_reporting()==0 for detecting the @ error suppression assumes that you did not set the error level to 0 in the first place.

However, typically if you want to set your own error handler, you would set the error_reporting to 0. Therefore, an alternative to detect the @ error suppression is required.

me at hesterc dot fsnet dot co dot uk

18 years ago

If you wish to display some text when an error occurs, echo doesn't work. Use print instead. This is explained on the following link 'What is the difference between echo and print?':

http://www.faqts.com/knowledge_base/view.phtml/aid/1/fid/40

It says "print can be used as part of a more complex expression where echo cannot".

Also, you can add multiple code to the result when an error occurs by separating each line with "and". Here is an example:

<?php
$my_file
= @file ('non_existent_file') or print 'File not found.' and $string = ' Honest!' and print $string and $fp = fopen ('error_log.txt', 'wb+') and fwrite($fp, $string) and fclose($fp);
?>

A shame you can't use curly brackets above to enclose multiple lines of code, like you can with an if statement or a loop. It could make for a single long line of code. You could always call a function instead.

tracerdx at tracerdx dot com

17 years ago

I keep seeing qualification lists for error types/error-nums as arrays; In user notes and in the manual itself. For example, in this manual entry's example, when trying to seperate behavior for the variable trace in the error report:

<?php //...

// set of errors for which a var trace will be saved

$user_errors = array(E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE);//and later...if (in_array($errno, $user_errors)) {
//...whatever
}//... ?>

I was under the impression that PHP error code values where bitwise flag values. Wouldn't bitwise masking be better? So I propose a slightly better way:
<?php //...$user_errors = E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE;//...blah...if ($errno & $user_errors) {
//...whatever
}//... ?>
Or for those of you who don't like the idea of using an integer as the condition in an if statement:

<?php
if (($errno & $user_errors) > 0) {
//...whatever
}
?>

I think that's much more efficient than using _yet another_ array() constuct and an in_array().

If I am wrong, and the E_* constants aren't supposed to be used in this fashion (ie, the constans aren't guaranteed to be bitwise, which would be odd since that's how they're setup in the php.ini file), then delete me. I just don't see why one should be using arrays when bitwise comparisons will work, considering the bitwise method should be MUCH more efficient.

shawing at gmail dot com

18 years ago

Although the root user writes to the files 'error_log' and 'access_log', the Apache user has to own the file referenced by 'error_log = filename' or no log entries will be written.

; From php.ini
; Log errors to specified file.
error_log = /usr/local/apache/logs/php.errors

[root@www logs]$ ls -l /usr/local/apache/logs/php.errors
-rw-r--r-- 1 nobody root 27K Jan 27 16:58 php.errors

ptah at se dot linux dot org

19 years ago

PHP5 only (only tested with php5.0).

If you, for some reason, prefer exceptions over errors and have your custom error handler (set_error_handler) wrap the error into an exception you have to be careful with your script.

Because if you, instead of just calling the exception handler, throws the exception, and having a custom exception handler (set_exception_handler). And an error is being triggered inside that exception handler, you will get a weird error:
"Fatal error: Exception thrown without a stack frame in Unknown on line 0"

This error is not particulary informative, is it? :)

This example below will cause this error.
<?php
class PHPErrorException extends Exception
{
private
$context = null;
public function
__construct
($code, $message, $file, $line, $context = null)
{
parent::__construct($message, $code);
$this->file = $file;
$this->line = $line;
$this->context = $context;
}
};

function

error_handler($code, $message, $file, $line) {
throw new
PHPErrorException($code, $message, $file, $line);
}

function

exception_handler(Exception $e)
{
$errors = array(
E_USER_ERROR => "User Error",
E_USER_WARNING => "User Warning",
E_USER_NOTICE => "User Notice",
);

echo

$errors[$e->getCode()].': '.$e->getMessage().' in '.$e->getFile().
' on line '.$e->getLine()."\n";
echo
$e->getTraceAsString();
}
set_error_handler('error_handler');
set_exception_handler('exception_handler');// Throw exception with an /unkown/ error code.
throw new Exception('foo', 0);
?>

There are however, easy fix for this as it's only cause is sloppy code.
Like one, directly call exception_handler from error_handler instead of throwing an exception. Not only does it remedy this problem, but it's also faster. Though this will cause a `regular` unhandled exception being printed and if only "designed" error messages are intended, this is not the ultimate solution.

So, what is there to do? Make sure the code in exception_handlers doesn't cause any errors! In this case a simple isset() would have solved it.

regards, C-A B.

Stephen

16 years ago

If you are using PHP as an Apache module, your default behavior may be to write PHP error messages to Apache's error log. This is because the error_log .ini directive may be set equal to "error_log" which is also the name of Apache's error log. I think this is intentional.

However, you can separate Apache errors from PHP errors if you wish by simply setting a different value for error_log. I write mine in the /var/log folder.

mortonda at dgrmm dot net

16 years ago

Note the example code listed here calls date() every time this is called. If you have a complex source base which calls the custom error handler often, it can end up taking quite a bit of time. I ran a profiler on som code and discovered that 50% of the time was spent in the date function in this error handler.

Anonymous

18 years ago

When configuring your error log file in php.ini, you can use an absolute path or a relative path. A relative path will be resolved based on the location of the generating script, and you'll get a log file in each directory you have scripts in. If you want all your error messages to go to the same file, use an absolute path to the file.

In some application development methodologies, there is the concept of an application root directory, indicated by "/" (even on Windows). However, PHP does not seem to have this concept, and using a "/" as the initial character in a log file path produces weird behavior on Windows.

If you are running on Windows and have set, in php.ini:

error_log = "/php_error.log"

You will get some, but not all, error messages. The file will appear at

c:\php_error.log

and contain internally generated error messages, making it appear that error logging is working. However, log messages requested by error_log() do NOT appear here, or anywhere else, making it appear that the code containing them did not get processed.

Apparently on Windows the internally generated errors will interpret "/" as "C:\" (or possibly a different drive if you have Windows installed elsewhere - I haven't tested this). However, the error_log process apparently can't find "/" - understandably enough - and the message is dropped silently.

theotek AT nowhere DOT org

17 years ago

It is totally possible to use debug_backtrace() inside an error handling function. Here, take a look:

<?php
set_error_handler
('errorHandler');

function

errorHandler( $errno, $errstr, $errfile, $errline, $errcontext)
{
echo
'Into '.__FUNCTION__.'() at line '.__LINE__.
"\n\n---ERRNO---\n". print_r( $errno, true).
"\n\n---ERRSTR---\n". print_r( $errstr, true).
"\n\n---ERRFILE---\n". print_r( $errfile, true).
"\n\n---ERRLINE---\n". print_r( $errline, true).
"\n\n---ERRCONTEXT---\n".print_r( $errcontext, true).
"\n\nBacktrace of errorHandler()\n".
print_r( debug_backtrace(), true);
}

function

a( )
{
//echo "a()'s backtrace\n".print_r( debug_backtrace(), true);
asdfasdf; // oops
}

function

b()
{
//echo "b()'s backtrace\n".print_r( debug_backtrace(), true);
a();
}
b();
?>

Outputs:

<raw>

Into errorhandler() at line 9

---ERRNO---
8

---ERRSTR---
Use of undefined constant asdfasdf - assumed 'asdfasdf'

---ERRFILE---
/home/theotek/test-1.php

---ERRLINE---
23

---ERRCONTEXT---
Array
(
)

Backtrace of errorHandler()
Array
(
[0] => Array
(
[function] => errorhandler
[args] => Array
(
[0] => 8
[1] => Use of undefined constant asdfasdf - assumed 'asdfasdf'
[2] => /home/theotek/test-1.php
[3] => 23
[4] => Array
(
)

)

)

[1] => Array
(
[file] => /home/theotek/test-1.php
[line] => 23
[function] => a
)

[2] => Array
(
[file] => /home/theotek/test-1.php
[line] => 30
[function] => a
[args] => Array
(
)

)

[3] => Array
(
[file] => /home/theotek/test-1.php
[line] => 33
[function] => b
[args] => Array
(
)

)

)

</raw>

So, the first member of the backtrace's array is not really surprising, except from the missing "file" and "line" members.

The second member of the backtrace seem the be a hook inside the zend engine that is used to trigger the error.

Other members are the normal backtrace.

jbq at caraldi dot com

15 years ago

Precision about error_log when configured with syslog: the syslog() call is done with severity NOTICE.

petrov dot michael () gmail com

16 years ago

I have found that on servers that enforce display_errors to be off it is very inconvenient to debug syntax errors since they cause fatal startup errors. I have used the following method to bypass this limitation:

The syntax error is inside the file "syntax.php", therefore I create a file "syntax.debug.php" with the following code:

<?php
error_reporting
(E_ALL);
ini_set('display_errors','On');

include(

'syntax.php');
?>

The 5 line file is guaranteed to be free of errors, allowing PHP to execute the directives within it before including the file which previously caused fatal startup errors. Now those fatal startup errors become run time fatal errors.

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

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

В статье описан функционал, который доступен в PHP (актуально для 5.3.х) для обработки ошибок всех типов, включая ошибки интерпретации кода (E_ERROR, E_PARSE, E_WARNING, etc). Эта обработка поможет вам для управляемого отображения страницы в случае возникновения таких проблем. В статье присутствует множество описаний и рабочих примеров(архитектуры) для того, что бы сразу воспользоваться в своем программном продукте. В конце концов, ну немного сломали сайт, ну надо же, об этом сообщить поисковику с заголовком 4хх или 5хх и повеселить пользователя, вместо возврата белого экрана (или что хуже экрана со священной информацией, для хакеров) с ответом 200 Ok.

Идея написать этот топик возникла, когда я на храбре задал 2 вопроса:

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

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

Если заинтересовались, то подробности под катом…

Причины использования

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

Описания функций

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

— Контроль некритических ошибок: замечания, предупреждения, пользовательские ошибки. В общем все то, что не завершает интерпретацию аварийно.
set_error_handler — Задает определенный пользователем обработчик ошибок.
Нужен для того, что бы писать в лог все такие ошибки. Если её не задать, то в лог это не пишется, а мне вот всегда хочется узнать при каких боевых ситуациях могут вызываться замечания и предупреждения. То есть позволяет автоматически тестировать продукт пользователем и он даже не будет замечать этого.
Если функция не задана, то PHP лишь пытается вывести данные на экран, а если ему и это не дают, то вообще никаких признаков жизни от этих типов ошибок не возникает.

— Контроль, исключений: является ошибкой типа E_ERROR.
set_exception_handler — Задает пользовательский обработчик исключений
Ну не знаю, зачем это вообще было придумано, когда есть то, что описано ниже и просто обработка ошибки типа Exception. Так что сообщаю что оно просто существует. Она перехватывает критическую ошибку «исключение» и позволяет что-то с ней делать. В любом случае скрипт завершается. Её работы по умолчанию лично для меня достаточно (пишет в логи, пытается вывести на экран). Я бы её вообще не переопределял, а то придется в логи о случившимся исключении самому писать.

— Функции контроля вывода: Тут я опишу 3 функции, которые следует знать по разным причинам. Например, для проблем производительности или для проблем вывода заголовков. В нашем случае требуется выводить заголовки ошибок.

ob_start — Включение буферизации вывода
ob_flush — Сброс (отправка) буфера вывода
ob_end_clean — Очищает (стирает) буфер вывода и отключает буферизацию вывода

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

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

— Функция которая запускается после того как все отработало: Барабанная дробь.
register_shutdown_function — Регистрирует функцию, которая выполнится по завершении работы скрипта. Завершение может быть штатным или аварийным, без разницы.
Много плюсов и никаких минусов:

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

Многие Объектно Ориентированные люди обрадуются

Что все вышеизложенные функции можно зарегистрировать даже на методы классов, а также, наверняка, на статические методы классов по той же схеме. Правда способ очень не очевиден для глаза обычного не PHP программиста.
Параметр handler требуется задать через массив, с элементами «название класса|объекта», «метод объекта». Устанавливаемый метод обязательно должен быть public. Пример для функции set_error_handler:

<?php
class BaseErrorCatcher
{
	public function __construct()
	{
		set_error_handler(array($this, 'ErrorCatcher'));
		
		echo "друг, я создался\n";
		
		$errorVarArray['real index'] = true;
		echo $errorVarArray['error index'];
	}
	
	public function ErrorCatcher($errno, $errstr)
	{
		echo "друг, да ты вероянто напортачил где-то в этом: $errstr\n";
	}
}

error_reporting(E_ALL | E_STRICT);	// включаем замечания у кого они выключены
ini_set('display_errors','On');		// ну уж что бы точно вы увидели результат
new BaseErrorCatcher();			// начало теста
?>

Результат:

друг, я создался
друг, да ты вероянто напортачил где-то в этом: Undefined index: error index

Рабочий пример

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

Условия

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

Код с комментариями

От себя добавлю, что код не тестировал, так как это упрощенная схема того, что у меня в коде, замечания принимаются

<?php

class ErrorSupervisor
{
	public function __construct()
	{
		// регистрация ошибок
		set_error_handler(array($this, 'OtherErrorCatcher'));
		
		// перехват критических ошибок
		register_shutdown_function(array($this, 'FatalErrorCatcher'));
		
		// создание буфера вывода
		ob_start();
	}
	
	public function OtherErrorCatcher($errno, $errstr)
	{
		// контроль ошибок:
		// - записать в лог
		return false;
	}
	
	public function FatalErrorCatcher()
	{
		$error = error_get_last();
		if (isset($error))
			if($error['type'] == E_ERROR
				|| $error['type'] == E_PARSE
				|| $error['type'] == E_COMPILE_ERROR
				|| $error['type'] == E_CORE_ERROR)
			{
				ob_end_clean();	// сбросить буфер, завершить работу буфера
			
				// контроль критических ошибок:
				// - записать в лог
				// - вернуть заголовок 500
				// - вернуть после заголовка данные для пользователя
			}
			else
			{
				ob_end_flush();	// вывод буфера, завершить работу буфера
			}
		else
		{
			ob_end_flush();	// вывод буфера, завершить работу буфера
		}
	}
}

// запуск контроллера
$errorController = new ErrorSupervisor();

// генерируем контент
// изменяем заголовки, в обещм делаем много чего
echo "генерация простейшего контента";

// тестируем систему (не тестировал, но у меня в более сложном варианте работает)
include 'null'; // запускается OtherErrorCatcher
// require 'null'; // запустится FatalErrorCatcher
// require 'foobar.php'; // а внутри этого файла вообще ошибка компиляции
?>

Ссылки

Разделы документации

  • Функции обработки ошибок
  • Функции контроля вывода
  • Управление функциями — невзрачно оформлено правда? Без помощи Aco я бы еще очень долго читал документацию и решал задачу.
Другая полезная информация

  • Функция echo в PHP может выполняться более 1 секунды
  • Предопределенные константы — они же типы ошибок

Спасибо за внимание.

UPD: По советам из комментариев, дополнил класс ErrorSupervisor новой функциональностью, исправил пару заблуждений, добавил дополнительную интересную информацию по теме, немного отладил код

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

Антон Шевчук // Web-разработчик

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

О да, в этой статье я поведу свой рассказа об ошибках в PHP, и том как их обуздать.

Ошибки

Разновидности в семействе ошибок

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

Чтобы ни одна ошибка не ушла незамеченной потребуется включить отслеживание всех ошибок с помощью функции error_reporting(), а с помощью директивы display_errors включить их отображение:

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

Фатальные ошибки

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

E_PARSE
Это ошибка появляется, когда вы допускаете грубую ошибку синтаксиса и интерпретатор PHP не понимает, что вы от него хотите, например если не закрыли фигурную или круглую скобочку:

<?php
/**
 Parse error: syntax error, unexpected end of file
 */
{

Или написали на непонятном языке:

<?php
/**
 Parse error: syntax error, unexpected '...' (T_STRING)
 */
Тут будет ошибка парсера

Лишние скобочки тоже встречаются, и не важно круглые либо фигурные:

<?php
/**
 Parse error: syntax error, unexpected '}'
 */
}

Отмечу один важный момент – код файла, в котором вы допустили parse error не будет выполнен, следовательно, если вы попытаетесь включить отображение ошибок в том же файле, где возникла ошибка парсера то это не сработает:

<?php
// этот код не сработает
error_reporting(E_ALL);
ini_set('display_errors', 1);

// т.к. вот тут
ошибка парсера

E_ERROR
Это ошибка появляется, когда PHP понял что вы хотите, но сделать сие не получилось ввиду ряда причин, так же прерывает выполнение скрипта, при этом код до появления ошибки сработает:

Не был найден подключаемый файл:

/**
 Fatal error: require_once(): Failed opening required 'not-exists.php' (include_path='.:/usr/share/php:/usr/share/pear')
 */
require_once 'not-exists.php';

Было брошено исключение (что это за зверь, расскажу немного погодя), но не было обработано:

/**
 Fatal error: Uncaught exception 'Exception'
 */
throw new Exception();

При попытке вызвать несуществующий метод класса:

/**
 Fatal error: Call to undefined method stdClass::notExists()
 */
$stdClass = new stdClass();
$stdClass->notExists();

Отсутствия свободной памяти (больше, чем прописано в директиве memory_limit) или ещё чего-нить подобного:

/**
 Fatal Error: Allowed Memory Size
 */
$arr = array();

while (true) {
    $arr[] = str_pad(' ', 1024);
}

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

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

/**
 Fatal error: Maximum function nesting level of '256' reached, aborting!
 */
function deep() {
    deep();
}
deep();

Не фатальные

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

E_WARNING
Частенько встречается, когда подключаешь файл с использованием include, а его не оказывается на сервере или ошиблись указывая путь к файлу:

/**
 Warning: include_once(): Failed opening 'not-exists.php' for inclusion
 */
include_once 'not-exists.php';

Бывает, если используешь неправильный тип аргументов при вызове функций:

/**
 Warning: join(): Invalid arguments passed
 */
join('string', 'string');

Их очень много, и перечислять все не имеет смысла…

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

Когда обращаются к неопределенной переменной:

/**
 Notice: Undefined variable: a
 */
echo $a;

Когда обращаются к несуществующему элементу массива:

<?php
/**
 Notice: Undefined index: a
 */
$b = array();
$b['a'];

Когда обращаются к несуществующей константе:

/**
 Notice: Use of undefined constant UNKNOWN_CONSTANT - assumed 'UNKNOWN_CONSTANT'
 */
echo UNKNOWN_CONSTANT;

Когда не конвертируют типы данных:

/**
 Notice: Array to string conversion
 */
echo array();

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

PHP E_NOTICE in PHPStorm

E_STRICT
Это ошибки, которые научат вас писать код правильно, чтобы не было стыдно, тем более IDE вам эти ошибки сразу показывают. Вот например, если вызвали не статический метод как статику, то код будет работать, но это как-то неправильно, и возможно появление серьёзных ошибок, если в дальнейшем метод класса будет изменён, и появится обращение к $this:

/**
 Strict standards: Non-static method Strict::test() should not be called statically
 */
class Strict { 
    public function test() { 
        echo 'Test'; 
    } 
}

Strict::test();

E_DEPRECATED
Так PHP будет ругаться, если вы используете устаревшие функции (т.е. те, что помечены как deprecated, и в следующем мажорном релизе их не будет):

/**
 Deprecated: Function split() is deprecated
 */

// популярная функция, всё никак не удалят из PHP
// deprecated since 5.3
split(',', 'a,b');

В моём редакторе подобные функции будут зачёркнуты:

PHP E_DEPRECATED in PHPStorm

Обрабатываемые

Этот вид, которые разводит сам разработчик кода, я их уже давно не встречал, не рекомендую их вам заводить:

  • E_USER_ERROR – критическая ошибка
  • E_USER_WARNING – не критическая ошибка
  • E_USER_NOTICE – сообщения которые не являются ошибками

Отдельно стоит отметить E_USER_DEPRECATED – этот вид всё ещё используется очень часто для того, чтобы напомнить программисту, что метод или функция устарели и пора переписать код без использования оной. Для создания этой и подобных ошибок используется функция trigger_error():

/**
 * @deprecated Deprecated since version 1.2, to be removed in 2.0
 */
function generateToken() {
    trigger_error('Function `generateToken` is deprecated, use class `Token` instead', E_USER_DEPRECATED);
    // ...
    // code ...
    // ...
}

Теперь, когда вы познакомились с большинством видов и типов ошибок, пора озвучить небольшое пояснение по работе директивы display_errors:

  • если display_errors = on, то в случае ошибки браузер получит html c текстом ошибки и кодом 200
  • если же display_errors = off, то для фатальных ошибок код ответа будет 500 и результат не будет возвращён пользователю, для остальных ошибок – код будет работать неправильно, но никому об этом не расскажет

Приручение

Для работы с ошибками в PHP существует 3 функции:

  • set_error_handler() — устанавливает обработчик для ошибок, которые не обрывают работу скрипта (т.е. для не фатальных ошибок)
  • error_get_last() — получает информацию о последней ошибке
  • register_shutdown_function() — регистрирует обработчик который будет запущен при завершении работы скрипта. Данная функция не относится непосредственно к обработчикам ошибок, но зачастую используется именно для этого

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

  • $errno – первый аргумент содержит тип ошибки в виде целого числа
  • $errstr – второй аргумент содержит сообщение об ошибке
  • $errfile – необязательный третий аргумент содержит имя файла, в котором произошла ошибка
  • $errline – необязательный четвертый аргумент содержит номер строки, в которой произошла ошибка
  • $errcontext – необязательный пятый аргумент содержит массив всех переменных, существующих в области видимости, где произошла ошибка

В случае если обработчик вернул true, то ошибка будет считаться обработанной и выполнение скрипта продолжится, иначе — будет вызван стандартный обработчик, который логирует ошибку и в зависимости от её типа продолжит выполнение скрипта или завершит его. Вот пример обработчика:

<?php
    // включаем отображение всех ошибок, кроме E_NOTICE
    error_reporting(E_ALL & ~E_NOTICE);
    ini_set('display_errors', 1);
    
    // наш обработчик ошибок
    function myHandler($level, $message, $file, $line, $context) {
        // в зависимости от типа ошибки формируем заголовок сообщения
        switch ($level) {
            case E_WARNING:
                $type = 'Warning';
                break;
            case E_NOTICE:
                $type = 'Notice';
                break;
            default;
                // это не E_WARNING и не E_NOTICE
                // значит мы прекращаем обработку ошибки
                // далее обработка ложится на сам PHP
                return false;
        }
        // выводим текст ошибки
        echo "<h2>$type: $message</h2>";
        echo "<p><strong>File</strong>: $file:$line</p>";
        echo "<p><strong>Context</strong>: $". join(', $', array_keys($context))."</p>";
        // сообщаем, что мы обработали ошибку, и дальнейшая обработка не требуется
        return true;
    }
    
    // регистрируем наш обработчик, он будет срабатывать на для всех типов ошибок
    set_error_handler('myHandler', E_ALL);

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

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

function shutdown() {
    echo 'Этот текст будет всегда отображаться';
}
register_shutdown_function('shutdown');

Данная функция будет срабатывать всегда!

Но вернёмся к ошибкам, для отслеживания появления в коде ошибки воспользуемся функцией error_get_last(), с её помощью можно получить информацию о последней выявленной ошибке, а поскольку фатальные ошибки прерывают выполнение кода, то они всегда будут выполнять роль “последних”:

function shutdown() {
    $error = error_get_last();
    if (
        // если в коде была допущена ошибка
        is_array($error) &&
        // и это одна из фатальных ошибок
        in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])
    ) {
        // очищаем буфер вывода (о нём мы ещё поговорим в последующих статьях)
        while (ob_get_level()) {
            ob_end_clean();
        }
        // выводим описание проблемы
        echo 'Сервер находится на техническом обслуживании, зайдите позже';
    }
}
register_shutdown_function('shutdown');

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

О прожорливости

Проведём простой тест, и выясним – сколько драгоценных ресурсов кушает самая тривиальная ошибка:

/**
 * Этот код не вызывает ошибок
 */

// сохраняем параметры памяти и времени выполнения скрипта
$memory = memory_get_usage();
$time= microtime(true);

$a = '';
$arr = [];
for ($i = 0; $i < 10000; $i++) {
    $arr[$a] = $i;
}

printf('%f seconds <br/>', microtime(true) - $time);
echo number_format(memory_get_usage() - $memory, 0, '.', ' '), ' bytes<br/>';

В результате запуска данного скрипта у меня получился вот такой результат:

0.002867 seconds 
984 bytes

Теперь добавим ошибку в цикле:

/**
 * Этот код содержит ошибку
 */

// сохраняем параметры памяти и времени выполнения скрипта
$memory = memory_get_usage();
$time= microtime(true);

$a = '';
$arr = [];
for ($i = 0; $i < 10000; $i++) {
    $arr[$b] = $i; // тут ошиблись с именем переменной
}

printf('%f seconds <br/>', microtime(true) - $time);
echo number_format(memory_get_usage() - $memory, 0, '.', ' '), ' bytes<br/>';

Результат ожидаемо хуже, и на порядок (даже на два порядка!):

0.263645 seconds 
992 bytes

Вывод однозначен – ошибки в коде приводят к лишней прожорливости скриптов – так что во время разработки и тестирования приложения включайте отображение всех ошибок!

Тестирование проводил на PHP версии 5.6, в седьмой версии результат лучше – 0.0004 секунды против 0.0050 – разница только на один порядок, но в любом случае результат стоит прикладываемых усилий по исправлению ошибок

Где собака зарыта

В PHP есть спец символ «@» – оператор подавления ошибок, его используют дабы не писать обработку ошибок, а положится на корректное поведение PHP в случае чего:

<?php
    echo @UNKNOWN_CONSTANT;

При этом обработчик ошибок указанный в set_error_handler() всё равно будет вызван, а факт того, что к ошибке было применено подавление можно отследить вызвав функцию error_reporting() внутри обработчика, в этом случае она вернёт 0.

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

Исключения

В эру PHP4 не было исключений (exceptions), всё было намного сложнее, и разработчики боролись с ошибками как могли, это было сражение не на жизнь, а на смерть… Окунуться в эту увлекательную историю противостояния можете в статье Исключительный код. Часть 1. Стоит ли её читать сейчас? Думаю да, ведь это поможет вам понять эволюцию языка, и раскроет всю прелесть исключений

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

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

Исключение – это объект который наследуется от класса Exception, содержит текст ошибки, статус, а также может содержать ссылку на другое исключение которое стало первопричиной данного. Модель исключений в PHP схожа с используемыми в других языках программирования. Исключение можно инициировать (как говорят, “бросить”) при помощи оператора throw, и можно перехватить (“поймать”) оператором catch. Код генерирующий исключение, должен быть окружен блоком try, для того чтобы можно было перехватить исключение. Каждый блок try должен иметь как минимум один соответствующий ему блок catch или finally:

try {
    // код который может выбросить исключение
    if (rand(0, 1)) {
        throw new Exception('One')
    } else {
        echo 'Zero';
    }
} catch (Exception $e) {
    // код который может обработать исключение
    echo $e->getMessage();
}

В каких случаях стоит применять исключения:

  • если в рамках одного метода/функции происходит несколько операций которые могут завершиться неудачей
  • если используемый вами фреймверк или библиотека декларируют их использование

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

$directory = __DIR__ . DIRECTORY_SEPARATOR . 'logs';

// директории может не быть
if (!is_dir($directory)) {
    throw new Exception('Directory `logs` is not exists');
}

// может не быть прав на запись в директорию
if (!is_writable($directory)) {
    throw new Exception('Directory `logs` is not writable');
}

// возможно кто-то уже создал файл, и закрыл к нему доступ
if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log', 'a+')) {
    throw new Exception('System can\'t create log file');
}

fputs($file, date('[H:i:s]') . " done\n");
fclose($file);

Соответственно ловить данные исключения будем примерно так:

try {
    // код который пишет в файл
    // ...
} catch (Exception $e) {
    // выводим текст ошибки
    echo 'Не получилось: '. $e->getMessage();
}

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

// исключения файловой системы
class FileSystemException extends Exception {}

// исключения связанные с директориями
class DirectoryException extends FileSystemException {
    // коды исключений
    const DIRECTORY_NOT_EXISTS =  1;
    const DIRECTORY_NOT_WRITABLE = 2;
}

// исключения связанные с файлами
class FileException extends FileSystemException {}

Теперь, если использовать эти исключения то можно получить следующий код:

try {
    // код который пишет в файл
    if (!is_dir($directory)) {
        throw new DirectoryException('Directory `logs` is not exists', DirectoryException::DIRECTORY_NOT_EXISTS);
    }

    if (!is_writable($directory)) {
        throw new DirectoryException('Directory `logs` is not writable', DirectoryException::DIRECTORY_NOT_WRITABLE);
    }

    if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log', 'a+')) {
        throw new FileException('System can\'t open log file');
    }

    fputs($file, date('[H:i:s]'') . " done\n");
    fclose($file);
} catch (DirectoryException $e) {
    echo 'С директорией возникла проблема: '. $e->getMessage();
} catch (FileException $e) {
    echo 'С файлом возникла проблема: '. $e->getMessage();
} catch (FileSystemException $e) {
    echo 'Ошибка файловой системы: '. $e->getMessage();
} catch (Exception $e) {
    echo 'Ошибка сервера: '. $e->getMessage();
}

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

Так, а что будет если не поймать исключение? Вы получите “Fatal Error: Uncaught exception …”. Неприятно.
Чтобы избежать подобной ситуации следует использовать функцию set_exception_handler() и установить обработчик для исключений, которые брошены вне блока try-catch и не были обработаны. После вызова такого обработчика выполнение скрипта будет остановлено:

// в качестве обработчика событий 
// будем использовать анонимную функцию
set_exception_handler(function($exception) {
    /** @var Exception $exception */
    echo $exception->getMessage(), "<br/>\n";
    echo $exception->getFile(), ':', $exception->getLine(), "<br/>\n";
    echo $exception->getTraceAsString(), "<br/>\n";
});

Ещё расскажу про конструкцию с использованием блока finally – этот блок будет выполнен вне зависимости от того, было выброшено исключение или нет:

try {
    // код который может выбросить исключение
} catch (Exception $e) {
    // код который может обработать исключение
    // если конечно оно появится
} finally {
    // код, который будет выполнен при любом раскладе
}

Для понимания того, что это нам даёт приведу следующий пример использования блока finally:

try {
    // где-то глубоко внутри кода
    // соединение с базой данных
    $handler = mysqli_connect('localhost', 'root', '', 'test');

    try {
        // при работе с БД возникла исключительная ситуация
        // ...
        throw new Exception('DB error');
    } catch (Exception $e) {
        // исключение поймали, обработали на своём уровне
        // и должны его пробросить вверх, для дальнейшей обработки
        throw new Exception('Catch exception', 0, $e);
    } finally {
        // но, соединение с БД необходимо закрыть
        // будем делать это в блоке finally
        mysqli_close($handler);
    }

    // этот код не будет выполнен, если произойдёт исключение в коде выше
    echo "Ok";
} catch (Exception $e) {
    // ловим исключение, и выводим текст
    echo $e->getMessage();
    echo "<br/>";
    // выводим информацию о первоначальном исключении
    echo $e->getPrevious()->getMessage();
}

Т.е. запомните – блок finally будет выполнен даже в том случае, если вы в блоке catch пробрасываете исключение выше (собственно именно так он и задумывался).

Для вводной статьи информации в самый раз, кто жаждет ещё подробностей, то вы их найдёте в статье Исключительный код ;)

Задание
Написать свой обработчик исключений, с выводом текста файла где произошла ошибка, и всё это с подсветкой синтаксиса, так же не забудьте вывести trace в читаемом виде. Для ориентира – посмотрите как это круто выглядит у whoops.

PHP7 – всё не так, как было раньше

Так, вот вы сейчас всю информацию выше усвоили и теперь я буду грузить вас нововведениями в PHP7, т.е. я буду рассказывать о том, с чем вы столкнётесь через год работы PHP разработчиком. Ранее я вам рассказывал и показывал на примерах какой костыль нужно соорудить, чтобы отлавливать критические ошибки, так вот – в PHP7 это решили исправить, но как обычно завязались на обратную совместимость кода, и получили хоть и универсальное решение, но оно далеко от идеала. А теперь по пунктам об изменениях:

  1. при возникновении фатальных ошибок типа E_ERROR или фатальных ошибок с возможностью обработки E_RECOVERABLE_ERROR PHP выбрасывает исключение
  2. эти исключения не наследуют класс Exception (помните я говорил об обратной совместимости, это всё ради неё)
  3. эти исключения наследуют класс Error
  4. оба класса Exception и Error реализуют интерфейс Throwable
  5. вы не можете реализовать интерфейс Throwable в своём коде

Интерфейс Throwable практически полностью повторяет нам Exception:

interface Throwable
{
    public function getMessage(): string;
    public function getCode(): int;
    public function getFile(): string;
    public function getLine(): int;
    public function getTrace(): array;
    public function getTraceAsString(): string;
    public function getPrevious(): Throwable;
    public function __toString(): string;
}

Сложно? Теперь на примерах, возьмём те, что были выше и слегка модернизируем:

try {
    // файл, который вызывает ошибку парсера
    include 'e_parse_include.php';
} catch (Error $e) {
    var_dump($e);
}

В результате ошибку поймаем и выведем:

object(ParseError)#1 (7) {
  ["message":protected] => string(48) "syntax error, unexpected 'будет' (T_STRING)"
  ["string":"Error":private] => string(0) ""
  ["code":protected] => int(0)
  ["file":protected] => string(49) "/www/education/error/e_parse_include.php"
  ["line":protected] => int(4)
  ["trace":"Error":private] => array(0) { }
  ["previous":"Error":private] => NULL
}

Как видите – поймали исключение ParseError, которое является наследником исключения Error, который реализует интерфейс Throwable, в доме который построил Джек. Ещё есть другие, но не буду мучать – для наглядности приведу иерархию исключений:

interface Throwable
  |- Exception implements Throwable
  |    |- ErrorException extends Exception
  |    |- ... extends Exception
  |    `- ... extends Exception
  `- Error implements Throwable
      |- TypeError extends Error
      |- ParseError extends Error
      |- ArithmeticError extends Error
      |  `- DivisionByZeroError extends ArithmeticError
      `- AssertionError extends Error 

TypeError – для ошибок, когда тип аргументов функции не совпадает с передаваемым типом:

try {
    (function(int $one, int $two) {
        return;
    })('one', 'two');
} catch (TypeError $e) {
    echo $e->getMessage();
}

ArithmeticError – могут возникнуть при математических операциях, к примеру когда результат вычисления превышает лимит выделенный для целого числа:

try {
    1 << -1;
} catch (ArithmeticError $e) {
    echo $e->getMessage();
}

DivisionByZeroError – ошибка деления на ноль:

try {
    1 / 0;
} catch (ArithmeticError $e) {
    echo $e->getMessage();
}

AssertionError – редкий зверь, появляется когда условие заданное в assert() не выполняется:

ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);

try {
    assert(1 === 0);
} catch (AssertionError $e) {
    echo $e->getMessage();
}

При настройках production-серверов, директивы zend.assertions и assert.exception отключают, и это правильно

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

При написании данного раздела были использованы материалы из статьи Throwable Exceptions and Errors in PHP 7

Отладка

Иногда для отладки кода нужно отследить что происходило с переменной или объектом на определённом этапе, для этих целей есть функция debug_backtrace() и debug_print_backtrace() которые вернут историю вызовов функций/методов в обратном порядке:

<?php
function example() {
    echo '<pre>';
    debug_print_backtrace();
    echo '</pre>';
}

class ExampleClass {
    public static function method () {
        example();
    }
}

ExampleClass::method();

В результате выполнения функции debug_print_backtrace() будет выведен список вызовов приведших нас к данной точке:

#0  example() called at [/www/education/error/backtrace.php:10]
#1  ExampleClass::method() called at [/www/education/error/backtrace.php:14]

Проверить код на наличие синтаксических ошибок можно с помощью функции php_check_syntax() или же команды php -l [путь к файлу], но я не встречал использования оных.

Assert

Отдельно хочу рассказать о таком экзотическом звере как assert() в PHP, собственно это кусочек контрактной методологии программирования, и дальше я расскажу вам как я никогда его не использовал :)

Первый случай – это когда вам надо написать TODO прямо в коде, да так, чтобы точно не забыть реализовать заданный функционал:

// включаем вывод ошибок
error_reporting(E_ALL);
ini_set('display_errors', 1);

// включаем asserts
ini_set('zend.assertions', 1);
ini_set('assert.active', 1);

assert(false, "Remove it!");

В результате выполнения данного кода получим E_WARNING:

Warning: assert(): Remove it! failed

PHP7 можно переключить в режим exception, и вместо ошибки будет всегда появляться исключение AssertionError:

// включаем asserts
ini_set('zend.assertions', 1);
ini_set('assert.active', 1);
// переключаем на исключения
ini_set('assert.exception', 1);

assert(false, "Remove it!");

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

assert(false, new Exception("Remove it!"));

Но я бы рекомендовал использовать метки @TODO, современные IDE отлично с ними работают, и вам не нужно будет прикладывать дополнительные усилия и ресурсы для работы с ними

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

// callback-функция для вывода информации в браузер
function backlog($script, $line, $code, $message) {
    echo "<h3>$message</h3>";
    highlight_string ($code);
}

// устанавливаем callback-функцию
assert_options(ASSERT_CALLBACK, 'backlog');
// отключаем вывод предупреждений
assert_options(ASSERT_WARNING,  false);

// пишем проверку и её описание
assert("sqr(4) == 16", "When I send integer, function should return square of it");

// функция, которую проверяем
function sqr($a) {
    return; // она не работает
}

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

/**
 * Настройки соединения должны передаваться в следующем виде
 *
 *     [
 *         'host' => 'localhost',
 *         'port' => 3306,
 *         'name' => 'dbname',
 *         'user' => 'root',
 *         'pass' => ''
 *     ]
 *
 * @param $settings
 */
function setupDb ($settings) {
    // проверяем настройки
    assert(isset($settings['host']), 'Db `host` is required');
    assert(isset($settings['port']) && is_int($settings['port']), 'Db `port` is required, should be integer');
    assert(isset($settings['name']), 'Db `name` is required, should be integer');

    // соединяем с БД
    // ...
}

setupDb(['host' => 'localhost']);

Никогда не используйте assert() для проверки входных параметров, ведь фактически assert() интерпретирует строковую переменную (ведёт себя как eval()), а это чревато PHP-инъекцией. И да, это правильное поведение, т.к. просто отключив assert’ы всё что передаётся внутрь будет проигнорировано, а если делать как в примере выше, то код будет выполняться, а внутрь отключенного assert’a будет передан булевый результат выполнения

Если у вас есть живой опыт использования assert() – поделитесь со мной, буду благодарен. И да, вот вам ещё занимательно чтива по этой теме – PHP Assertions, с таким же вопросом в конце :)

В заключение

Я за вас напишу выводы из данной статьи:

  • Ошибкам бой – их не должно быть в вашем коде
  • Используйте исключения – работу с ними нужно правильно организовать и будет счастье
  • Assert – узнали о них, и хорошо

P.S. Спасибо Максиму Слесаренко за помощь в написании статьи

PHP предлагает гибкие настройки вывода ошибок, среди которых функия error_reporting($level) – задает, какие ошибки PHP попадут в отчет, могут быть значения:

  • E_ALL – все ошибки,
  • E_ERROR – критические ошибки,
  • E_WARNING – предупреждения,
  • E_PARSE – ошибки синтаксиса,
  • E_NOTICE – замечания,
  • E_CORE_ERROR – ошибки обработчика,
  • E_CORE_WARNING – предупреждения обработчика,
  • E_COMPILE_ERROR – ошибки компилятора,
  • E_COMPILE_WARNING – предупреждения компилятора,
  • E_USER_ERROR – ошибки пользователей,
  • E_USER_WARNING – предупреждения пользователей,
  • E_USER_NOTICE – уведомления пользователей.

1

Вывод ошибок в браузере

error_reporting(E_ALL);
ini_set('display_errors', 'On'); 

PHP

В htaccess

php_value error_reporting "E_ALL"
php_flag display_errors On

htaccess

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

2

Запись ошибок в лог файл

error_reporting(E_ALL);
ini_set('display_errors', 'Off'); 
ini_set('log_errors', 'On');
ini_set('error_log', $_SERVER['DOCUMENT_ROOT'] . '/logs/php-errors.log');

PHP

Файлы логов также не должны быть доступны из браузера, храните их в закрытой директории с файлом .htaccess:

Order Allow,Deny
Deny from all

htaccess

Или запретить доступ к файлам по расширению .log (заодно и другие системные файлы и исходники):

<FilesMatch ".(htaccess|htpasswd|bak|ini|log|sh|inc|config|psd|fla|ai)$">
Order Allow,Deny
Deny from all
</FilesMatch>

htaccess

3

Отправка ошибок на e-mail

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

Первый – register_shutdown_function() регистрирует функцию, которая выполнится при завершении работы скрипта, error_get_last() получает последнюю ошибку.

register_shutdown_function('error_alert');

function error_alert() 
{ 
	$error = error_get_last();
	if (!empty($error)) {
		mail('mail@example.com', 'Ошибка на сайте example.com', print_r($error, true)); 
	}
}

PHP

Стоит учесть что оператор управления ошибками (знак @) работать в данном случаи не будет и письмо будет отправляться при каждой ошибке.

Второй метод использует «пользовательский обработчик ошибок», поэтому в браузер ошибки выводится не будут.

function error_alert($type, $message, $file, $line, $vars)
{
	$error = array(
		'type'    => $type,
		'message' => $message,
		'file'    => $file,
		'line'    => $line
	);
	error_log(print_r($error, true), 1, 'mail@example.com', 'From: mail@example.com');
}

set_error_handler('error_alert');

PHP

4

Пользовательские ошибки

PHP позволяет разработчику самому объявлять ошибки, которые выведутся в браузере или в логе. Для создания ошибки используется функция trigger_error():

trigger_error('Пользовательская ошибка', E_USER_ERROR);

PHP

Результат:

Fatal error: Пользовательская ошибка in /public_html/script.php on line 2
  • E_USER_ERROR – критическая ошибка,
  • E_USER_WARNING – не критическая,
  • E_USER_NOTICE – сообщения которые не являются ошибками,
  • E_USER_DEPRECATED – сообщения о устаревшем коде.

10.10.2019, обновлено 09.10.2021

Другие публикации

HTTP коды

Список основных кодов состояния HTTP, без WebDAV.

Автоматическое сжатие и оптимизация картинок на сайте

Изображения нужно сжимать для ускорения скорости загрузки сайта, но как это сделать? На многих хостингах нет…

Работа с JSON в PHP

JSON (JavaScript Object Notation) – текстовый формат обмена данными, основанный на JavaScript, который представляет собой набор пар {ключ: значение}. Значение может быть массивом, числом, строкой и…

Пример парсинга html-страницы на phpQuery

phpQuery – это удобный HTML парсер взявший за основу селекторы, фильтры и методы jQuery, которые позволяют…

Примеры отправки AJAX JQuery

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

Подключение к платежной системе Сбербанка

После регистрации в системе эквайринга Сбербанка и получив доступ к тестовой среде, можно приступить к интеграции с…

Понравилась статья? Поделить с друзьями:
  • Контроль ошибок modbus
  • Контроллер rittal ошибки
  • Контроль датчика кислорода ошибка или не завершен
  • Контроль незакрытых ошибок пд текущего периода что это
  • Контроллер remak коды ошибок