Отслеживание ошибок php

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.

Антон Шевчук // 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. Спасибо Максиму Слесаренко за помощь в написании статьи

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

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

Что я понимаю под правильной обработкой:

  • Универсальное решение, которое можно вставить в любой существующий код;
  • Легко расширяемое решение;
  • В PHP аж три «механизма ошибок»: собственно ошибки (error), исключения (exception) и утверждения (assertion). Свести три механизма к одному — exception. В комментариях к предыдущей статье на эту тему выражалось мнение, что exception это плохой и/или сложный метод обработки ошибок. Я так не считаю и готов это обсудить в комментариях;
  • Опциональное логирование;
  • Общий обработчик exception, который будет поддерживать разные форматы вывода и debug/production режимы;
  • В debug режиме должен выводится trace. Требования к trace: компактный, понятный и по возможности ссылки на открытие файлов в IDE.

Универсальное решение

Для этого сделаем класс exceptionHandlerClass. В exceptionHandlerClass будут храниться настройки и статические методы — обработчики error, exception и assertion. Еще нам нужны методы setupHandlers и restoreHandlers. Первый метод настроит перехват ошибок. Error и assertion обработчики будут бросать ErrorException. Exception обработчик будет обрабатывать необработанные Exception и в зависимости от настроек выводить соответствующий ответ. restoreHandlers вернет все обработчики в изначальное состояние — это поможет при встраивании класса в код с существующим механизмом обработки ошибок. Подключение выглядит так:

  1. require ‘exceptionHandler/exceptionHandlerClass.php’;
  2. exceptionHandlerClass::setupHandlers();

включение debug режима (по умолчанию выключен) :

  1. exceptionHandlerClass::$debug = true;

Форматы вывода

Проще объяснить на примере: для вывода trace на веб странице я оберну его в таги pre и применю htmlspecialchars(), с другой стороны этот же trace при выводе в консоль будет не удобно читать, было бы проще, если бы это был plainText. Если нужно вывести ошибку как ответ SоapServer, то это должен быть правильно сформированный XML документ (SoapFault). Если скрипт выводит бинарные данные, например изображение, то удобней выводить ошибки через WildFire. Во всех этих ситуация нужно просто применить разные форматы вывода.
Для разных форматов будем создавать разные классы. Я для начала реализую два формата вывода exceptionHandlerOutputWeb(для веба) и exceptionHandlerOutputCli(для командной строки). Так же нам понадобиться класс фабрика (exceptionHandlerOutputFactory), в котором будет инкапсулирована логика, когда какой формат вывода применить.

  1. public function getExceptionHandlerOutput(){
  2.     if(php_sapi_name() == ‘cli’){
  3.         return new exceptionHandlerOutputCli();
  4.     }
  5.     return new exceptionHandlerOutputWeb();
  6. }

При вызове setupHandlers можно установить формат вывода, передав экземпляр класса exceptionHandlerOutput* или exceptionHandlerOutputFactory*.

  1. exceptionHandlerClass::setupHandlers(new exceptionHandlerOutputAjax());

Благодаря такой архитектуре можно легко расширять форматы. Для создания нового формата достаточно создать класс, который будет наследоваться от абстрактного класса exceptionHandlerOutput и реализовать один метод (output).

  1. class exceptionHandlerOutputAjax extends exceptionHandlerOutput{
  2.     public function output($exception, $debug){
  3.         header(‘HTTP/1.0 500 Internal Server Error’, true, 500);
  4.         header(‘Status: 500 Internal Server Error’, true, 500);
  5.         $response = array(
  6.             ‘error’ => true,
  7.             ‘message’ => »,
  8.         );
  9.         if($debug){
  10.             $response[‘message’] = $exception->getMessage();
  11.         } else {
  12.             $response[‘message’] = self::$productionMessage;
  13.         }
  14.         exit(json_encode($response));
  15.     }
  16. }

Если нужна более сложная логика для автоматического выбора формата вывода, нужно создать класс, наследуемый от exceptionHandlerOutputFactory и реализовать метод getExceptionHandlerOutput.

  1. class exceptionHandlerOutputAjaxFactory extends exceptionHandlerOutputDefaultFactory{
  2.     public function getExceptionHandlerOutput() {
  3.         if( self::detect() ){
  4.             return new exceptionHandlerOutputAjax();
  5.         }
  6.         parent::getExceptionHandlerOutput();
  7.     }
  8.     public static function detect(){
  9.         return (!empty($_SERVER[‘HTTP_X_REQUESTED_WITH’]) 
  10.             && strtolower($_SERVER[‘HTTP_X_REQUESTED_WITH’]) == ‘xmlhttprequest’);
  11.     }
  12. }
  13. exceptionHandlerClass::setupHandlers(new exceptionHandlerOutputAjaxFactory());

Логирование

Как я и сказал выше логирование можно включать по желанию. Для этого в exceptionHandlerClass создан метод exceptionLog

  1. public static function exceptionLog($exception, $logPriority = null){
  2.     if(!is_null(self::$exceptionHandlerLog)){
  3.         self::$exceptionHandlerLog->log($exception, $logPriority);
  4.     }
  5. }

если нужно включить логирование, то достаточно сделать следующее:

  1. exceptionHandlerClass::$exceptionHandlerLog = new exceptionHandlerSimpleLog();

Класс для логирования должен наследоваться от абстрактного exceptionHandlerLog и реализовывать метод log

  1. class exceptionHandlerSimpleLog extends exceptionHandlerLog{
  2.     public function log($exception, $logType){
  3.         switch ($logType){
  4.             case self::uncaughtException:
  5.                 error_log($exception->getMessage());
  6.                 break;
  7.         }
  8.     }
  9. }

logType это одна из констант объявленных exceptionHandlerLog

  1. const uncaughtException = 0; //необработанные исключения
  2. const caughtException = 1; //вызов метода логирования вне обработчиков ошибок
  3. const ignoredError = 2; //ошибки маскированные @, логируются если выключена опция scream
  4. const lowPriorityError = 3; //ошибки которые не превращаются exception
  5. const assertion = 4; //assertion 

Имея logType и exception разработчик может сам решить какие искллючения и как логировать. Например, uncaughtException можно высылать по почте, ignoredError с severity E_ERROR логировать в файл итп.

Trace

При выводе trace я хочу видеть тип исключения, сообщение и собственно сам trace. В trace для каждого вызова должно выводится, какая функция вызвалась, список «кратких» параметров, файл и строка где произошел вызов. Что такое «краткие» параметры объясню на примерах: если функцию вызвали со строкой длиной в 1000 символов, то наличие этой строки в trace ничем не поможет при решении проблемы, а только затруднит чтение trace, это же касается массивов с большой вложенностью. Вывод trace на экран просто должен подсказать, где искать. Чтобы разобраться, что именно происходит нужно дебажить с помощью xdebug или примитивных var_dump() и die(), кому как больше нравится.
Пример trace:

	[ErrorException]: E_WARNING - mysql_connect(): Can't connect to MySQL server on 'localhost' (10061)
	#0: mysql_connect()
	    D:\projects1\d\index.php:19
	#1: testClass::test1("длиная строка…eeeeeery long string"(56))
	    D:\projects1\d\index.php:22
	#2: testClass->test2(testClass(), -∞, i:iTest, c:testClass, fa:testClass::test2)
	    D:\projects1\d\index.php:27
	#3: testAll(r:stream, fs:testClass::test1)
	    D:\projects1\d\index.php:30

Легенда

  • r: — resource
  • fs: — function (callable string)
  • fa: — function (callable array)
  • i: — interface (string)
  • c: — class (string)
  • ∞/INF — infinity
  • testClass() — object of type testClass
  • «»(n) — string, в скобках указана длина, … — место где вырезана часть строки
  • array(n) — array, в скобках указана длина

И самое полезное… ссылки на открытие файлов в IDE прямо из trace.

При нажатии на ссылку в IDE откроется соответствующий файл на соответствующей строке.
Для консольного режима (консоль NetBeans):
NetBeans Console

  1. exceptionHandlerOutputCli::setFileLinkFormat(‘: in %f on line %l’);

Для веб режима (TextMate):

  1. exceptionHandlerOutputWeb::setFileLinkFormat(‘txmt://open/?file://%f&line=%l’);

Можно реализовать для NetbBeans (или другого IDE). Для этого нужно: зарегистрировать протокол; сделать обработчик этого протокола (самое простое — bat файл). В обработчике через командную строку вызвать NetBeans с соответствующим файлом и строкой. Но это тема для следующей статьи.
Код писался за два дня так, что возможны мелкие недочеты. Скачать (не было времени, чтобы выложить в репозиторий).

UPD: перенесено в блог PHP
UPD2: в продолжение темы работа с исключениями в PHP

 on
May 06, 2021

PHP has been one of the top (if not best) server-side scripting languages in the world for decades. However, let’s be honest – error logging in PHP is not the most straightforward or intuitive. It involves tweaking a few configuration options plus some playing around to get used to. Once you have everything set up and figured out (like you will after reading this post), things seem much easier, and you realize how helpful error logging can turn out to be for your application – from debugging and troubleshooting to monitoring and maintenance. 

And this is why we are covering error logging in PHP in this post. We will start by revisiting the importance of logging errors in your application. We will then explore errors in PHP – their different types, and how they can be output. Next, we will look at all the error logging configurations in PHP, and understand how we can tweak these to our liking, before we see some error logging examples, and explore functions in PHP that allow us to write errors to log files. This post is a complete guide to error logging in PHP.

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the guide: 

  • Importance of Logging Errors 
  • PHP Error Types
  • Where Can PHP Errors be Output
  • Enabling and Configuring Error Reporting in PHP 
  • Logging Errors in PHP 
  • PHP’s Error Logging Functions 
    • error_log()
    • trigger_error()
    • syslog()
    • set_error_handler()
  • Popular PHP Logging Libraries 

Importance of Logging Errors 

Errors in software systems have this terrible reputation of being associated with failing things and breaking functionality. As a result, many of us often fail to recognize the importance of these loud red strings that bring our attention to faults, inconsistencies, and inaccuracies in our code – mistakes that can cost us dearly if allowed to fall through the cracks. Therefore, it is worthwhile for some of us to change our outlook towards error messages – to track, log, and organize them – and embrace their importance. 

There’s a reason developers and organizations build and leverage dedicated logging systems that keep track of errors that arise throughout an application’s lifecycle. These logs provide useful information about what went wrong, when, where, and how it can be fixed. 

For small-scale personal projects, it is common for developers not to feel the need to spend time setting up an effective logging system. This seems plausible because your code and end-user interactions are much more manageable for smaller projects. However, the requirement for effectively logging and maintaining errors and other information grows exponentially as your application scales. For larger applications, catering to thousands of users, it becomes unwieldy to track errors and updates across hundreds of components in real-time. Putting in place a system that can record the status of the very many events that an application’s operation entails allows organizations to maintain a clear record of their performance. This allows for more transparency and therefore ensures that no issues go unnoticed. As a result, this makes your application more reliable, easy to maintain, monitor, and debug.

Now that we are hopefully convinced that error logging is a worthwhile expedition, let us look at the different types of errors in PHP.

PHP Error Types

Broadly, there are five types of errors in PHP:

1. Fatal run-time Errors (E_ERROR) 

These errors typically happen when an operation in your code cannot be performed. This leads to your code exiting. An example of a fatal error would be when you call a function that hasn’t been defined in your code, shown below:

<?php
function foo() {
  echo "Function foo called.";
}
boo(); // undefined function 'boo'
?>

Error output –>

Fatal error: Uncaught Error: Call to undefined function boo() in code/my-php/index.php:5 Stack trace: #0 {main} thrown in code/my-php/index.php on line 5.

2. Warning Errors (E_WARNING)

A warning error is more gentle and less obtrusive in that it does not halt the execution. It presents a friendly reminder of something amiss in your code – a mistake that might not fail things immediately or fail anything at all but suggests a more accurate way of doing things that make your code more foolproof. These warnings can also save developers from issues that might pose a much bigger threat in the future. An example of a warning error would be when you try to include a file in PHP using an incorrect file path, as shown below:

<?php
include('filename.txt'); // arbitrary file that is not present
echo "Hello world";
?>

Error output ->

Warning: include(filename.txt): failed to open stream: No such file or directory in code/my-php/index.php on line 2

3. Parse Errors (E_PARSE)

Parse errors are also known as syntax errors as they arise from syntactical mistakes in your code. These errors are raised during the compilation of your code, making it exit before it runs. A common example of a parse error is missing a semicolon at the end of a code statement, shown below:

<?php
echo Hello world // no quotes or semicolon used
?>

Error output ->

Parse error: syntax error, unexpected 'world' (T_STRING), expecting ',' or ';' in code/my-php/index.php on line 2.

4. Notice Errors (E_NOTICE)

Notice errors are minor errors that are encountered during run-time, and just like warning errors, do not halt the execution. They usually occur when the script is attempting to access an undefined variable, for example, as shown below:

<?php
$a = 1;
$c = $a + $b; // undefined variable $b
?>

Error output ->

Notice: Undefined variable: b in code/my-php/index.php on line 3

5. User Errors (E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE) 

User errors are user-defined, i.e., present a custom user-generated message raised explicitly from the code to capture a specific condition. These errors are manually raised by developers using the trigger_error function instead of the PHP engine. They are further classified as fatal, warning, and notice errors, but we’ll group them all as user errors for simplicity.

Where can PHP Errors be Output?

There are two primary places where we can have our errors presented in PHP – through inline errors and dedicated error log files.

Inline Errors 

Inline errors are those that show up on your webpage in the browser or your terminal via STDOUT in a command-line environment. These errors prove to be quite useful during development – for developers to debug their code, fix issues, and get information about the overall execution. Below is an example of what these errors usually look like in the browser:

undefined

Though this proves to be super helpful for developers, you should be very careful in ensuring that these errors are not output when your application goes into production – for two reasons – end-user experience and security. You can toggle the displaying of these errors using the display_error directive in your system’s configuration. We’ll dive deeper into this in the next section.

Error Log Files 

Inline errors are not persistent in memory, i.e., they are not saved anywhere and are only viewed as long as the browser or terminal session is alive. Additionally, you’ll only want to have them in a development environment. Conversely, as the theme of this post suggests, logging your errors is the more intelligent and more systematic approach towards maintaining large-scale applications. These are persistent in memory and provide information about the operation of your application across multiple components in one place, making it easier for monitoring and troubleshooting.

PHP, therefore, allows you to direct all your errors to specific log files; these files store timestamps, error stack traces, custom messages, and other helpful information about the source of the error and how to fix it. You can specify the path of your custom log file using the error_log directive of your system configuration. Here’s an example of an error log file:

undefined

You can also choose to have your errors logged to the system’s log file, usually located in – /var/log/syslog. We’ll cover this in a later section in the post.

Now let’s look at how we can configure where and how we want our errors logged.

Enabling and Configuring Error Reporting in PHP 

As we discussed previously, logging in PHP is slightly less straightforward than other languages and frameworks. You might have to tweak a few options in configuration files to customize logging patterns. For example, when you install PHP on your machine, the initial configuration comes with some aspects of error logging disabled. This differs from system to system, and therefore, you should manually check these settings before getting started.

Logging Configuration Options in php.ini File

The configuration options are in the php.ini file. This file is read when PHP starts up and allows developers to play around with PHP’s functionality. Usually, this file can be found somewhere in the /etc/php directory on most Linux systems. 

There are a bunch of directives (options) pertaining to the logging of errors in PHP that we can configure in this php.ini file:

  • display_errors (default: 1)

Display errors are the inline errors we previously looked at. This directive can be used to good effect during development to output PHP error messages to the browser or terminal. However, for applications in production, you should likely turn this off to save your users from a poor website experience due to obscure error messages. Not only that, but this also protects you from exposing valuable information about the internals of your application as a security measure.

  • display_startup_errors (default: 0)

As the name suggests, this is to output any errors that take place when PHP starts up. These usually do not provide any valuable information about your application specifically, and therefore need not be turned on.

  • log_errors (default: 0)

This directive allows you to toggle the logging of errors to the specified path (in the next directive). Because this is turned off by default, it would be advisable to toggle this to 1 for recording your application’s error messages in a log file.

  • error_log (default: 0)

This directive allows you to specify the path of your log file. You can also set this to “syslog” for directing all your error log messages to the system log. 

  • error_reporting (default: null)

The error_reporting directive allows you to customize which error levels you want reported, and which you are okay with going unreported. For example, you can use the directive as shown below to have all errors reported: error_reporting = E_ALL

  • track_errors (default: 0)

This directive allows you to access the last raised error message in the $php_errormsg global variable in your code and can keep track of errors across your whole project.

After making changes to your php.ini file, you will need to restart the server for the changes to take effect.

The ini_set() Function

However, if you are unable to locate the php.ini file, or prefer overriding your project’s global configuration options, there is also an option to update these directives using the ini_set() function in your PHP code. For example, the below code can be used for customizing error reporting in your project:

<?php

// enabling error logging
ini_set('log_errors', 1);  

// Customize reporting of errors
ini_set('error_reporting', E_WARNING | E_ERROR | E_PARSE | E_NOTICE);

// specify error log file path
ini_set('error_log', '/tmp/my-logs.log');

?>

The error_reporting() Function

One can also modify the error_reporting configuration option using the error_reporting() function from inside your code during run-time. As in the ini_set function, you can use bitwise operators like OR (|), AND (&), NOT (~), etc., when specifying the error levels to be reported. Below are a few examples of how this function can be used.

// Report only selected kinds of errors
error_reporting(E_ERROR | E_PARSE | E_NOTICE);

or

// Report all errors except E_WARNING
error_reporting(E_ALL & ~E_WARNING);

Now that we have got the system configurations and overall setup out of the way, let’s look at an example of how errors in your project code can be logged to files on your system.

Logging Errors in PHP

First, we will override the logging configuration parameters using the ini_set() function to enable error logging and specify the log file’s path. Then we’ll write some erroneous code to have PHP raise an error that we would like to have logged.

<?php
  ini_set('log_errors', 1); // enabling error logging
  ini_set('error_log', '/path/my-error-file.log'); // specifying log file path

  echo $b; // undefined variable should raise error
?>

After opening the web page on our browser, let’s open the ‘my-error-file.log’ file to see if the error message was logged. Here is the log file output:

[28-Feb-2021 13:34:36 UTC] PHP Notice:  Undefined variable: b in code/my-php/index.php on line 5

As you can see, our notice error was logged with a timestamp. As our code encounters more and more errors, this file will keep getting populated with corresponding timestamps. Note that we haven’t explicitly turned off display_errors, so these error messages are likely to be logged to the browser web page – something you might want to avoid during production. 

This was an example of capturing errors raised by PHP in log files. Now let’s look at how we can raise and log custom error messages for our application.

PHP’s Error Logging Functions

So far, we looked at errors raised by PHP – errors about your code execution. However, oftentimes you would want to also capture custom errors, with custom error messages specific to the functioning of your application. These so-called errors might not necessarily fail your code or halt its execution, but can indicate conditions characterized as erroneous and noteworthy for your application. These can act as indications to the organization about anomalous behavior that the team might want to look into and fix.

To facilitate this, PHP provides a set of functions that we can use to actively log errors in our code.

error_log()

The most common method for actively logging errors is the error_log() function. This sends a string argument for the error message to the log file.

error_log (string $message, int $message_type=0, string $destination=?, string $extra_headers=?) : bool

It also takes many other parameters to send error messages over email or specific log files. However, for the sake of simplicity, we won’t be covering that here. 

The interesting thing about this function is it logs your error message to the file specified in the configuration (or to the system log), regardless of the value of the log_errors directive. Let’s take a very simple example of logging an error when a specific condition in our code is met.

<?php

ini_set('error_log', '/path/my-error-file.log');
$a = 5;
$b = 10;

$c = $a + $b;

if ($c < 20) {
error_log("Sum is less than 20."); // logging custom error message
}

?>

Here is the output of the log file:

[28-Feb-2021 13:31:50 UTC] Sum is less than 20

Similarly, you can also log the values of variables in your code to provide additional context about your errors. Let’s see an example for that:

<?php
  ini_set('error_log', '/path/my-error-file.log');

  $languagesArray = array("PHP", "Python", "Node.js");
  error_log("Lorem ipsum. Array data -> ".print_r($languagesArray, true));
?>

Here’s the output of the log file ->

[28-Feb-2021 13:49:28 UTC] Lorem ipsum. Array data -> Array

(

   [0] => PHP

   [1] => Python

   [2] => Node.js

)

trigger_error()

The trigger_error() function can be used to raise a user-defined error/warning/notice. You can also specify the error type based on the condition. This allows you to customize its reporting and other behavior – for example, using an error type of E_USER_ERROR. We can cause the code to exit immediately compared to an E_USER_WARNING error.

trigger_error (string $error_msg, int $error_type=E_USER_NOTICE) : bool

The difference between trigger_error and error_log is that the former only generates a user error and depends on your system’s logging configurations to handle this error message (whether displayed or logged). error_log, on the other hand, will log your message regardless of the system’s configuration.

Here is the code for the same example we saw previously:

<?php
ini_set('log_errors', 1); // enabling error logging
ini_set('error_log', '/path/my-error-file.log'); // specifying log file path

$a = 5;
$b = 10;
$c = $a + $b;

if ($c < 20) {
trigger_error("Sum is less than 20.", E_USER_ERROR);
      echo "This will not be printed!";
}

?>

This adds a similar log entry to what we saw previously, but with an error level, plus the conventional error source information (log file output below):

[01-Mar-2021 01:16:56 UTC] PHP Fatal error:  Sum is less than 20. in code/my-php/index.php on line 10

syslog()

You can also choose to directly send an error message to the system’s log using the syslog() function.

syslog (int $priority, string $message) : bool

The first argument is the error’s priority level – LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_ALERT, LOG_EMERG, etc. (more about it here).  The second argument is the actual message’s text. This is how the function can be used:

<?php
// opening logger connection
openlog('myApp', LOG_CONS | LOG_NDELAY | LOG_PID, LOG_USER | LOG_PERROR
); // more information about params in documentation

syslog(LOG_WARNING, "My error message!");

closelog();
?>

This should reflect in your system’s logger (usually in /var/log/syslog) as:

Mar 1 13:27:15 zsh php: My error message! 

set_error_handler()

To customize the handling of all the user-defined errors throughout your code, PHP allows you to specify a custom error handler function to override the default handling of errors. This makes it easy for organizations to modify how they want their errors logged, the corresponding error messages send method, and much more. The set_error_handler() function helps with this.

set_error_handler (callable $error_handler, int $error_types=E_ALL | E_STRICT) : mixed

It takes as an argument our custom error handler function, which will define the handling of our errors, and look something like this:

handler (int $errno, string $errstr, string $errfile=?, int $errline=?, array $errcontext=?) : bool

This takes in many parameters like the error number, error string, corresponding file, etc. Let’s understand this better using the same previous example:

<?php
// custom error handler function ->
function myErrorHandler($errno, $errstr, $errfile, $errline, $errcontext){
    $message = date("Y-m-d H:i:s - "); // timestamp in error message
    $message .= "My Error: [" . $errno ."], " . "$errstr in $errfile on line $errline, \n"; // custom error message
    $message .= "Variables:" . print_r($errcontext, true) . "\r\n";
   
    error_log($message, 3, "/path/my-error-file.log");
    die("There was a problem, please try again."); // exit code
}

set_error_handler("myErrorHandler");

$a = 5;
$b = 10;
$c = $a + $b;

if ($c < 20) {
trigger_error("Sum is less than 20.", E_USER_WARNING);
}

echo "This will not be printed!";
?>

Here, we define a custom error handler function where we modify the error message a bit, log it, and exit the code. Then, when we use the trigger_error() function, its logging is handled by the above function, which takes care of the rest. This is what the output in the log file looks like:

2021-03-01 06:58:07 - My Error: [512], Sum is less than 20. in code/my-php/index.php on line 22,

Variables:Array

(

   [a] => 5

   [b] => 10

   [c] => 15

)

As you can see, this can be used to fully customize error logging in applications, allowing organizations to prioritize aspects of errors and contexts that are more important for their application.

Popular PHP Logging Libraries 

Thanks to the huge PHP community support on the internet, there have been very many logging libraries that aim to provide more functionality and ease the overall process for developers and organizations. Each of the renowned PHP frameworks that you must have heard of come equipped with logging libraries built-in. There are also now logging standards established, like the PSR-3 (PHP Standards Recommendation) logger interface, that defines a standardized interface to follow for logging libraries. 

Below is a list of some of the most popular logging libraries in PHP:

  • Monolog
  • Analog
  • KLogger
  • Log4PHP

Feel free to check these out to see what default error logging in PHP is missing out on.

Wrapping Up 

In this post, we covered everything about errors and logging in PHP. We discussed the importance of logging mechanisms in your application, looked at the different types of errors in PHP, and explored the various configuration options and PHP functions that we can use to log errors effectively.

Now that you have a decent understanding of everything, go ahead and start implementing error logging for your application! It doesn’t matter if you are working on a small project where things might seem under control even without log files. Logging your errors is considered one of the top “best practices” in software development that becomes exponentially more important as your applications grow and scale.

To learn more about logging in PHP in general, feel free to check out the Tutorial: Log to Console in PHP on our blog!

To up your application monitoring game by identifying bottlenecks and gaining effective application performance insights, check out ScoutAPM to get started with a 14-day free trial!

Happy coding!

How to Display PHP Errors and Enable Error Reporting

I still vividly remember the first time I learned about PHP error handling.

It was in my fourth year of programming, my second with PHP, and I’d applied for a developer position at a local agency. The application required me to send in a code sample (GitHub as we know it didn’t exist back then) so I zipped and sent a simple custom CMS I’d created the previous year.

The email I got back from the person reviewing the code still chills my bones to this day.

«I was a bit worried about your project, but once I turned error reporting off, I see it actually works pretty well».

That was the first time I searched «PHP error reporting», discovered how to enable it, and died inside when I saw the stream of errors that were hidden from me before.

PHP errors and error reporting are something that many developers new to the language might miss initially. This is because, on many PHP based web server installations, PHP errors may be suppressed by default. This means that no one sees or is even aware of these errors.

For this reason, it’s a good idea to know where and how to enable them, especially for your local development environment. This helps you pick up errors in your code early on.

If you Google «PHP errors» one of the first results you will see is a link to the error_reporting function documentation.

This function allows you to both set the level of PHP error reporting, when your PHP script (or collection of scripts) runs, or retrieve the current level of PHP error reporting, as defined by your PHP configuration.

The error_reporting function accepts a single parameter, an integer, which indicates which level of reporting to allow. Passing nothing as a parameter simply returns the current level set.

There is a long list of possible values you can pass as a parameter, but we’ll dive into those later.

For now it’s important to know that each possible value also exists as a PHP predefined constant. So for example, the constant E_ERROR has the value of 1. This means you could either pass 1, or E_ERROR to the error_reporting function, and get the same result.

As a quick example, if we create a php_error_test.php PHP script file, we can see the current error reporting level set, as well as set it to a new level.

<?php
// echo the current error reporting level
echo error_reporting();
<?php
// report all Fatal run-time errors.
echo error_reporting(1);

Error reporting configuration

Using the error_reporting function in this way is great when you just want to see any errors related to the piece of code you’re currently working on.

But it would be better to control which errors are being reported on in your local development environment, and log them somewhere logical, to be able to review as you code. This can be done inside the PHP initialization (or php.ini) file.

The php.ini file is responsible for configuring all the aspects of PHP’s behavior. In it you can set things like how much memory to allocate to PHP scripts, what size file uploads to allow, and what error_reporting level(s) you want for your environment.

If you’re not sure where your php.ini file is located, one way to find out is to create a PHP script which uses the phpinfo function. This function will output all the information relative to your PHP install.

<?php
phpinfo();

As you can see from my phpinfo, my current php.ini file is located at /etc/php/7.3/apache2/php.ini.

phpinfo

Once you’ve found your php.ini file, open it in your editor of choice, and search for the section called ‘Error handling and logging’. Here’s where the fun begins!

Error reporting directives

The first thing you’ll see in that section is a section of comments which include a detailed description of all the Error Level Constants. This is great, because you’ll be using them later on to set your error reporting levels.

Fortunately these constants are also documented in the online PHP Manual, for ease of reference.

Below this list is a second list of Common Values. This shows you how to set some commonly used sets of error reporting value combinations, including the default values, the suggested value for a development environment, and the suggested values for a production environment.

; Common Values:
;   E_ALL (Show all errors, warnings and notices including coding standards.)
;   E_ALL & ~E_NOTICE  (Show all errors, except for notices)
;   E_ALL & ~E_NOTICE & ~E_STRICT  (Show all errors, except for notices and coding standards warnings.)
;   E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR  (Show only errors)
; Default Value: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
; Development Value: E_ALL
; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT

Finally, at the bottom of all the comments is the current value of your error_reporting level. For local development, I’d suggest setting it to E_ALL, so as to see all errors.

error_reporting = E_ALL

This is usually one of the first things I set when I set up a new development environment. That way I’ll see any and all errors that are reported.

After the error_reporting directive, there are some additional directives you can set. As before, the php.ini file includes descriptions of each directive, but I’ll give a brief description of the important ones below.

The display_errors directive allows you to toggle whether PHP outputs the errors or not. I usually have this set to On, so I can see errors as they happen.

display_errors=On

The display_startup_errors allows for the same On/Off toggling of errors that may occur during PHP’s startup sequence. These are typically errors in your PHP or web server configuration, not specifically your code. It’s recommended to leave this Off, unless you’re debugging a problem and you aren’t sure what’s causing it.

The log_errors directive tells PHP whether or not to log errors to an error log file. This is always On by default, and is recommended.

The rest of the directives can be left as the default, except for maybe the error_log directive, which allows you to specify where to log the errors, if log_errors is on. By default it will log the errors wherever your web server has defined them to be logged.

Custom error logging

I use the Apache web server on Ubuntu, and my project-specific virtual host configurations use the following to determine the location for the error log.

ErrorLog ${APACHE_LOG_DIR}/project-error.log

This means it will log to the default Apache log directory, which is /var/log/apache2, under a file called project-error.log. Usually I replace project with the name of the web project it relates to.

So, depending on your local development environment you may need to tweak this to suit your needs. Alternatively, if you can’t change this at the web server level, you can set it at the php.ini level to a specific location.

error_log = /path/to/php.log

It is worth noting that this will log all PHP errors to this file, and if you’re working on multiple projects that might not be ideal. However, always knowing to check that one file for errors might work better for you, so your mileage may vary.

Find and fix those errors

If you’ve recently started coding in PHP, and you decide to turn error reporting on, be prepared to deal with the fallout from your existing code. You may see some things you didn’t expect, and need to fix.

The advantage though, is now that you know how to turn it all on at the server level, you can make sure you see these errors when they happen, and deal with them before someone else sees them!

Oh, and if you were wondering, the errors I was referring to at the start of this post were related to the fact that I was defining constants incorrectly, by not adding quotes around the constant name.

define(CONSTANT, 'Hello world.');

PHP allowed (and might still allow) this, but it would trigger a notice.

Notice: Use of undefined constant CONSTANT - assumed 'CONSTANT' 

This notice was triggered every time I defined a constant, which for that project was about 8 or 9 times. Not great for someone to see 8 or 9 notices at the top of each page…



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

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

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

  • Отправка сообщила об ошибке 0x800ccc7d
  • Отсутствие жеманства предрассудков начитанность грамматические ошибки
  • Отчет об ошибках тестирование
  • Отправляя сообщение оператор связи допустил ошибки котел
  • Отслеживание ошибок windows

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

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