Обработка ошибок

Фанат

oncle terrible
Команда форума
Обработка ошибок

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

Сначала сформулируем несколько исходных положений.
1. Ситуацию, когда сайт в случае критической ошибки выдает невразумительную строчку на белом фоне (или на обрывках "дизайна"), можно считать приемлемой, если сайт изначально делается для небольшого круга друзей, которые быстро наберут на мобилу и до исправления потерпят.
В остальных случаях мы высказываем желание показать пользователю полностью оформленную страницу с извинениями.
2. Для этого мы делим наш скрипт функционально на две части. получение данных и отдача данных. Отдача начинается только после получения, которое завершилось успешно.

Для достижения этого мы,
1. В процессе получения данных мы вставляем разные проверки на ошибки. При этом, исходя из постулированных задач, реально нам нужна только последняя в цепочке(-ах). Такой метод назовем "грязным": ведь если не произошло подключение, условно, к БД, то остальной код гарантированно будет выдавать кучу ошибок, не имеющих, при этом, смысла. Большой беды в этом нет, но хочется красоты. Поэтому.
2. Хочется вставлять проверки выполнения критических операций, чтобы не плодить лишнего мусора в логах.
3. Исходя из постулированных задач, мы не применяем exit. Учше, он же die() - бяка и проклят во веки веков.
4. Попытавшись оформить if-ами задачу чуть более сложную, чем один запрос к одной таблице, понимаем, что mission impossible
5. Тут там рассказывают об исключениях, в которых throw - это и есть маленький локальный exit внутри блока try.

Вопросы.
1. В самом примитивном варианте использования try сведется к catch, в котором будет единственный оператор show_error_page(). Но. Если все только ради этой функции, то никто не мешает нам сделать вместо die функцимю petit_mort(), в которой будет тот же самый show_error_page()! получается, что exit не так уж ужасен, а try - не стакан c граалем.
2. Самый сложный вопрос. Разумеется, область применения исключениев не ограничивается единственным тупым траем на блок получения данных. Вот хочется осмысленных примеров другого применения траев.
3. Самый похожий на вопрос. Так ли уж нужен "чистый" стиль обработки?

Для предметности возьмем тестовый пример: создание файла дампа БД с последующей выдачей файла на экран.
При "грязном" стиле все просто, кроме факта определения успешности. Впрочем, пустой файл дампа вполне годится. получается, нигде не пишем никаких проверок кроме как перед самой выдачей.
PHP:
mysql_connect();
mysql_select_db();
$fp=fopen();
mysql_query();
while ($row=mysql_fwtch_array) {
  fwrite()
}
fclose();
$content=file_get_contents($filename);
if (!$content) $content = "I am sorry";
load_template();
в случае, если не откроется файл на запись, мы получим в логе длинную бороду ошибок записи.

с исключениями будет так (поправьте, если неправ):
PHP:
try {
  mysql_connect() or throw new Exception('db_connect');
  mysql_select_db() or throw new Exception('db_select');
  $fp=fopen()  or throw new Exception('fopen');
  mysql_query() or throw new Exception('query'. mysql_error());
  while ($row=mysql_fetch_array) {
    fwrite()
  }
  fclose();
  $content=file_get_contents($filename)  or throw new Exception('fread');
} catch (Exception $e) {
  $content = "I am sorry";
  load_template();
} 
load_template();
В логах сообщение только о ключевой ошибке


PHP:
function petit_mort() {
  $content = "I am sorry";
  load_template();
  exit;
}
mysql_connect() or petit_mort('db_connect');
mysql_select_db() or petit_mort('db_select');
$fp=fopen()  or petit_mort('fopen');
mysql_query() or petit_mort('query'. mysql_error());
  while ($row=mysql_fetch_array) {
  fwrite()
}
fclose();
$content=file_get_contents($filename)  or petit_mort('fread');
load_template();
результат тот же.

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

admLoki

Новичок
try/catch варианты хороши только в ООП, в процедурном лучше использовать обыкновенные функции аля show_error($message) и проч.
 

dimagolov

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

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

Фанат

oncle terrible
Команда форума
А зачем генерить что-то особенное? мне кажется, что стандартной фразы "Извините, модуль сейчас недоступен, администрация извещена. пожалуйста упопробуйте позже" должно хватить на любой случай. Где я неправ?

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

AmdY

Пью пиво
Команда форума
на самом деле try будет многоступенчатым, в зависимости от тяжести ошибки и этапа на котором она произошла
PHP:
try {
    try {
        $content = "Всё ок";
   } catch(Exception $e) {
        $content = "Мамочки, ошибка!!!";
   }
   load_template(); 
} catch (NoRender_Exception $e) {
die("отстуствует возможность вывести шаблонную страницу");
} catch (Exception $e) {
die("шаблонная страница с текстом ошибки");
}
 

Макс

Старожил PHPClub
У исключений есть как преимущества, так и недостатки.

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

Недостатки:
исключения усложняют интерфейс функции - то есть вызывая какую-то функцию я должен знать список исключений, который она может бросить. И в ПХП нет средств отследить, какие исключения может кинуть функция. Например в java/c++ (насколько я помню) при объявлении функции описываются и исключения, которые она может бросить. В ПХП такого нет.
А еще весь фреймворк должен использовать исключения для обработки ошибок, а значит почти все функции ПХП нужно оборачивать в какие-то обертки, которые бы по ошибке кидали бы исключения.

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

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

-~{}~ 30.04.09 16:41:

*****
насчет примера твоего кода с исключениями, в реальной жизни он выглядел бы примерно так :
PHP:
try {
   $DB = Db::getConnection(...);
   $File = new File(...);
   $Records = $DB->query(...);
 ....
} catch(Exception $e) {
....
}
то есть никаких throw обычно в коде страницы не кидается, они все во внутренностях фреймворка
 

dimagolov

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

Gorynych

Посетитель PHP-Клуба
Re: Обработка ошибок

Автор оригинала: *****
...
2. Для этого мы делим наш скрипт функционально на две части. получение данных и отдача данных. Отдача начинается только после получения, которое завершилось успешно.
- в общем случае отдачи только после получения, которое завершилось успешно может не случится. В сущности (имхо) это не проходит в большинстве портальных решений, и веб 2.0. проектах со слабыми связями между различными элементами страницы. В итоге - при "сборке" конечной страницы приходится схлопывать недополученные данные и блоки до определенного уровня допуска.

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

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

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

Кстати, не коннект с БД это тоже не повод прерывать выполнение скрипта, если у вас есть другие источники данных (кеш, последняя сохраненная статическая копия и т.п.). Но логично предполагать, что в этом случае нас будет интересовать только первая ошибка - невозможность соединения и не будут интересовать остальные. Как вариант - класс-обертка, который просто тупо возвращает FALSE при обращении ко всякимм запросам к БД, если нам не удалось с ней соединиться. При этом суть ошибки - именно невозможность соединения, т.е. первая ошибка, случившаяся при попытке работы с БД.
 

whirlwind

TDD infected, paranoid
Приведите пример обработки исключений из рабочего кода. Не "выглядел бы примерно так" а из рабочего кода.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Это какое-то общественное бессознательное :)
Я вот тоже последние месяцы убираю слои, перевожу объекты в набор функций, недавно переписал обработку ошибок.

Я использую оба метода одновременно.

Из объектов для базы, курла и вычислений бросаю исключения - они формируют внутреннее сообщения с нужными данными.
dbException дает текст запроса к БД, а curlException - адрес и детали запроса к сайту.
Все исключения реализуют один интерфейс, который ловится - это позволяет не грузить сами классы исключений.
Реализованные методы генерируют тексты логов для записи в файл или с форматированием для браузера.

Для всего остального - error_handler с проверкой текущего error level, обработкой стека вызова и запись в лог в удобном виде.

В финансовых системах написание проверок и обработка всех возможных ошибок занимает 70% времени.
В сайтах знакомств - до 20%.
Подход часто зависит от задачи.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
по вопросам:
1. использую exit спокойно, особенно с AJAX.
2. OOP-шное goto :)
3. даже необходим при написании аддонов и модификаций через год, когда хороший лог сэкономит день на вспоминание логики

-~{}~ 01.05.09 04:05:

whirlwind
нет, опыт работы в банках + около 5 лет создания их под заказ :)

-~{}~ 01.05.09 04:25:

Кусок реального кода - обработка уведомления платежной системы о платеже.
PHP:
$user_id = filter_pos_int('pUserId',INPUT_POST);

try{
    $system_account=$SVC->configValue('lr_account');
    if ($payee_acc != $system_account){
        throw new pException(1711);
    }
    if (!$user_id){
        throw new pException(1090);
    }

    if ($SVC->checkExternalTransaction($transaction_no)){
        exit;
    }

    $data=array(
        'amount'=>$amount,
        'user_id'=>$user_id,
        'user_external_account'=>$payer_acc,
        'external_transaction_id'=>$transaction_no,
        'details'=>var_export($_POST,true),
    );
    $SVC->deposit($data);
    
}catch(gksException $E){
    if ($E->getCode()<1000){
        //internal error, need attention
        mail(ADMIN_EMAIL,'Error processing ',"Exception during IPN processing:\n".
            $E->getLogMessage());
    }
    _log($E->getLogMessage());
    exit();
}
 

findnext

Новичок
а у нас такая же архитектура как и у grigori, 1 в 1.

Из объектов для базы, курла и вычислений бросаю исключения - они формируют внутреннее сообщения с нужными данными.
dbException дает текст запроса к БД, а curlException - адрес и детали запроса к сайту.
Все исключения реализуют один интерфейс, который ловится - это позволяет не грузить сами классы исключений.
-~{}~ 01.05.09 09:53:

мы конечно не вебом занимаемся, но система постороена именно так
 

whirlwind

TDD infected, paranoid
grigori у тебя в кетче нету никакого "финансового" кода. Зато минус налицо - захочешь хендлер поправить, придется ломать рабочий финансовый код. Хотя вовсе даже не обязательно.

-~{}~ 01.05.09 11:04:

PS. обработка ошибок связанная с финансами, это например когда деклайны с определенными кодами ошибок минусуют участника системы. В большинстве случаев тупо - прошла не прошла. Где ты углядел 70% я не знаю. Аналогичные ситуации встречаются крайне редко.
 

Lightning

Трудоголик
1. В самом примитивном варианте использования try сведется к catch, в котором будет единственный оператор show_error_page(). Но. Если все только ради этой функции, то никто не мешает нам сделать вместо die функцимю petit_mort(), в которой будет тот же самый show_error_page()! получается, что exit не так уж ужасен, а try - не стакан c граалем.
Основное отличие твоего petit_mort() от throw/try/catch в месте, где принимается решение об обработке ошибки. Тут
PHP:
mysql_connect() or petit_mort('db_connect');
решение принимается в том же месте, где и произошла ошибка: если не можем соединиться, то сразу выдаем сообщение об ошибке и exit. Но ошибки бывают разные и обрабатывать их бывает нужно по-разному, причем в зависимости от ситуации одна и та же ошибка может требовать разной обработки. Если ты точно уверен, что при данной ошибке всегда нужно будет выводить сообщение и завершать работу скрипта, то можно использовать petit_mort(). Я вот не уверен, вдруг в каком-либо случае, мне понадобиться обработать эту ошибку по-другому, поэтому
PHP:
mysql_connect(...) or throw new DBConnectException(...);
PHP:
try {
    //точка входа в приложение
} catch (Exception $e) {
    /* Если исключение никто не перехватил, значит это критическая ошибка. В прокшен-версии пишем в лог и выводим специальную страницу; в дебаг-версии выводим подробное сообщение об ошибке.*/
}
2. Самый сложный вопрос. Разумеется, область применения исключенеев не ограничивается единственным тупым траем на блок получения данных. Вот хочется осмысленных примеров другого применения траев.
Опять же, ошибки бывают разные и обрабатывать их бывает нужно по-разному, причем в зависимости от ситуации одна и та же ошибка может требовать разной обработки, поэтому есть смысл передавать ошибки на верхние уровни, где будут приниматься решения об их обработке.Более осмысленные примеры - это примеры реального кода, где исключения перехватываются на разных уровнях и обрабатываются по-разному.
3. Самый похожий на вопрос. Так ли уж нужен "чистый" стиль обработки?
Да.

-~{}~ 01.05.09 16:38:

grigori
Зачем смешивать два подхода?
Зачем использовать непонятные throw new pException(1711); ?
Почему
PHP:
}catch(gksException $E){
    if ($E->getCode()<1000){
        //internal error, need attention
        mail(ADMIN_EMAIL,'Error processing ',"Exception during IPN processing:\n".
            $E->getLogMessage());
    }
    _log($E->getLogMessage());
    exit();
}
,а не хотя бы
PHP:
} catch (InternalException $E) {
    mail(ADMIN_EMAIL,'Error processing ',"Exception during IPN processing:\n".
        $E->getLogMessage());
    writeLogAndExit($E);
} catch (Exception $E) {
    writeLogAndExit($E);
}
 

Gorynych

Посетитель PHP-Клуба
с какого-то момента понял, что ошибку "здесь и сейчас":

а) никто не видит
б) никто не ловит
в) допускают намеренно

утверждение "а" для меня связано с тем, что в результате каких-то коллизий ситуация ночью с субботы на воскресенье на части страниц портала почему-то не такая, как была в пятницу, в 18:00.

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

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

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

не хватало нам полдня выдавать: "вам очень повезло! в этот момент мы добавляем на сайт что-то новенькое!". Ну да, в 6 утра воскресенья - ха-ха два раза (ничего, что тут все занимаются банкоми и финансами, а я все еще про веб?). Гасим ошибку, логируем, идем дальше. И только если явно задано, что при возникновении такой-то ошибки все бросать и отдавать 503-ю страницу - тогда да, это критическая ошибка и дальнейшая работа не возможна.

про утверждение "в". Я не очень верю в обработку ошибок на более верхнем уровне: скорее всего там (на верхнем уровне) не знают в чем проблема. На более высоком уровне можно только делать выбор - продолжать или не продолжать работу, если что-то не случилось?

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

из примеров,... ну давайте о вынужденных или прогнозируемых ошибках общения с БД:
PHP:
...
$this->connected=false;

try{
	$this->object=new mysqli($this->db_host, $this->db_user, $this->db_pass, $this->db_name, $this->db_port, $this->db_socket);
} catch(E_WARNING $e){
	if (!is_null($this->db_socket)){
		// В случае передачи неверного сокета, MySQLi не создаёт соединение. Этот
		// workaround - попытка это обойти.

		$this->db_socket=false;
		try{
			$this->object=new mysqli($this->db_host, $this->db_user, $this->db_pass, $this->db_name, $this->db_port);
		} catch(E_WARNING $e){
			return null;
		}
	} else {
		return null;
	}
}

$this->connected=true;
...
когда я дома вожусь со своим ноутом, то и у меня нет возможности работать через сокеты со связкой баз мастер-слейв, у меня в логах от этого дела остается:

[01-Май-2009 16:47:30] Host: ХХХХ.dev
[01-Май-2009 16:47:30] URI: /<тут записан REQUEST_URI>/
[01-Май-2009 16:47:30] Error: mysqli::mysqli() [<a href='http://php.net/mysqli.mysqli'>mysqli.mysqli</a>]: (HY000/2002): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
[01-Май-2009 16:47:30] File: /..../datadb.php, Line: 64
[01-Май-2009 16:47:30] MD5: 5B4FBDDAA632422D6DB7AB786EE9764B
[01-Май-2009 16:47:30] Level: E_WARNING (2)
[01-Май-2009 16:47:30] EID: 357B59C177C69FFE275462968784B1EB
[01-Май-2009 16:47:30] Session: FALSE
[01-Май-2009 16:47:30] Call: /.../lib/absclasses.php, Line: 84

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

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Автор оригинала: whirlwind
у тебя в кетче нету никакого "финансового" кода. Зато минус налицо - захочешь хендлер поправить, придется ломать рабочий финансовый код. Хотя вовсе даже не обязательно.
"Финансовый" код (проводки) - это хранимые процедуры. Он исполняется только при успехе всех проверок целостности данных (их более 10), что обосновано принципами бух. учета.

PS. обработка ошибок связанная с финансами, это например когда деклайны с определенными кодами ошибок минусуют участника системы. В большинстве случаев тупо - прошла не прошла.
Эта тема заслуживает отдельного топика. Прошу прощения за офтоп у людей, далеких от учета и финансов.
[offtop]
Для меня "обработка ошибок связанная с финансами, это" недопущение потери целостности данных.
Транзакции, в которых начало и завершение разделены по времени, исполняются через т.н. транзитные счета и счета "до выяснения" (банковские термины).
Потребности "минусования при деклайне" нет - транзакция заканчивается возвратом средств на счета отправителей с транзитных.
"деклайны" для меня - ожидаемые данные.
В учете на предприятиях этого нет. Там просто нет таких понятий и специалистов, с ними знакомых, а откат транзакции, то что ты называешь "не прошла", называется "сторно".
[/offtop]
ситуации встречаются крайне редко.
Можем обсудить различие "ситуаций" и учета в разных отраслях экономики. Это кому-то интересно?

-~{}~ 01.05.09 20:24:

Lightning
>Зачем смешивать два подхода?
а почему нет?
>непонятные throw new pException(1711); ?
Я из "старой школы", где программы возвращают цифровой код ошибки. Не наглядно, но я так привык.
Мне удобно свести все исключения в единый реестр, который подгружается по необходимости.

>} catch (Exception $E) {
Я люблю форматировать лог по-разному в зависимости от вывода (файл/консоль/браузер).
Мне нравинся, что все эксепшены приведены к единому интерфейсу.

>writeLogAndExit($E);
тут уже ты смешиваешь запись лога и окончание работы
 

Lightning

Трудоголик
grigori
>Зачем смешивать два подхода?
а почему нет?
Потому что смешение подходов усложняет разработку. Когда обработка ошибок унифицирована не нужно каждый раз думать и решать какой способ использовать. А в чем плюсы двух подходов?
Мне нравинся, что все эксепшены приведены к единому интерфейсу.
Мне тоже.
>writeLogAndExit($E);
тут уже ты смешиваешь запись лога и окончание работы
Не красиво конечно, но это чтобы дублирования не было.
PHP:
} catch (InternalException $E) {
    mail(ADMIN_EMAIL,'Error processing ',"Exception during IPN processing:\n".
        $E->getLogMessage());
    writeLogAndExit($E);
} catch (gksException $E) {
    writeLogAndExit($E);
}

//...

function writeLogAndExit($E) {
    _log($E->getLogMessage());
    exit(); 
}
Но код мне все еще не нравиться.
grigori, я конечно уважаю "старую школу", но код который ты привел мне вообще, мягко говоря, не нравиться.
PHP:
}catch(gksException $E){
    if ($E->getCode()<1000){          //Внутренних ошибок только 1000 ? А вдруг понадобиться больше?
        //internal error, need attention
        mail(ADMIN_EMAIL,'Error processing ',"Exception during IPN processing:\n".
            $E->getLogMessage());     //Сплошные литеральные константы
    }
    /* Если понадобиться обрабатывать разные ошибки по-разному, то нужно писать switch ?
А ведь можно было воспользоваться средствами языка и обрабатывать разные ошибки множественными catch, а не создавать свои системы кодов ошибки.
*/
    _log($E->getLogMessage());
    exit();
}
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Lightning 3 года назад я думал так же
gksException - интерфейс
вместо "_log($E->getLogMessage())" бывает "echo $E->getHtmlMessage();"
это пример по просьбе whirlwind, пиши по-своему
>Если понадобиться обрабатывать разные ошибки по-разному, то нужно
делать рефакторинг и упрощать иерархию. Здесь несколько catch не нужно, надо - напишу.
>Внутренних ошибок только 1000 ?
менее 60. Sapienti sat.
 
Сверху