Pull to refresh

Comments 98

Отличная статья, спасибо. И блог у Вас интересный :)
1. Формальный подход требует, чтобы в случае невозможности аутентификации выбрасывалось AutentificationException. С точки зрения общности и целостности восприятия кода это тоже наиболее эффективно — не надо думать, ошибка ли это, или не ошибка. Исключительная ситуация — это любая ситуация, когда поведение программы отличается от нормального. В том числе, и ввод неверного логина и пароля.

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

2. Каждая функция, которую вы пишете, должна знать обо всех исключениях, которые могут произойти из-за вызова других функций в ее теле, и должна уметь их все обрабатывать хотя бы на уровне catch(Exception $ex){$log->write($ex->Message);}. Это просто правило хорошего тона. Еще правило хорошего тона требует, чтобы запись в лог шла на самом верхнем уровне программы, когда уже точно исключение либо выбрасывается пользователю, либо просто пожирается.

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

Исключения — это еще один способ возврата значения из функции, при этом подобное «значение» всегда означает, что что-то идет не так.
Исключительная ситуация — это любая ситуация, когда поведение программы отличается от нормального. В том числе, и ввод неверного логина и пароля.

Так всё-таки, ввод (!) логина и пароля — это поведение программы? Или пользовательское действие?

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

То есть, чисто формально, конкретный наш пример следует рассматривать так:
1. Пользователь должен ввести логин и пароль.
2. Логин и пароль могут быть введены некорректно — надо предусмотреть фильтрацию.
3. Логин и пароль могут быть введены неверно — надо предусмотреть код возврата.
4. Логин и пароль могут быть не обработаны из-за серверной ошибки — надо предусмотреть исключение.

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

Логично, что все. Почему?

Потому что во всех случаях у нас одинаковое поведение — показать пользователю жуткое окно и редиректуть его для повторного ввода (если возможно, если сервер не упал).

Разделяя сущности, мы придем к дублированию кода. Наоборот, выкидывая исключение на каждый из типов, мы делаем поведение более общим и не теряем возможность расширения.
Я с вами согласен. Вообще, по-моему, использование кодов возврата в эпоху освоения космоса и дешевого интернета — дичь. Естественно, под «кодом возврата» я подразумеваю неочевидные данные. Например, такая функция (довольно часто раньше я так писал):

bool login(username, password)…

Что нам говорит код возврата? «Логин не удался». Никакой конкретной информации. А уж конструкции вида if (! login())… еще сильней добавляют туману.

Другое дело, когда «код возврата» — суть некое смысловое значение (например, int getHttpError() ...).

Поэтому, с моей точки зрения, исключениям — быть. Единственное, что неудобно — это отсутствие в пыхе возможности спецификации исключений, наподобие ветки throw в C++:

void f() throw (exception1, exception2) {… }

Приходится спасаться тегами PHPDocumentor'а, что все-же не так удобно.
В C# точно так же, кстати. XML-теги спасают.
это вы — дичь, а нормальные люди пишут чекеры и хранят стэки возвращаемых данных

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

а уже внутри — ловятся ошибки, пишутся номера ошибок, номера пишутся в класс ошибок, там преобразуются в мессаги и xajax-ом аппентядтся в афтер лабелы для полей

все-таки, имхо, «исключение» и «правило» — разные вещи.
проверка логина — это правило, которое должен соблюсти пользователь.
а исключение — это если, например, он пытается взломать сайт. подставляет всякие символы и тд…

ну даже по логике русского языка «правило» и «исключвение»…
>проверка логина — это правило, которое должен соблюсти пользователь.
>а исключение — это если, например, он пытается взломать сайт. подставляет всякие символы и тд…

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

Как разруливаем? А главное как отличаем «правило» от «исключения».
а вы помните, как работает throw в плюсах? тут Вам не джава, знающие люди советуют не пользоваться этой конструкцией.
Но можно сделать намного проще, и мне кажется, логичнее. Обрабатывая все сообщения к пользователю через исключения (что, часто, очень удобно)

1. Пользователь должен ввести логин и пароль — throw(«Необходимо ввести логин и пароль», E_USER_NOTICE);
2. Логин и пароль могут быть введены некорректно — throw(«Не корректный логин или пароль», E_USER_WARNING);
3. Логин и пароль могут быть введены неверно — throw(«Не верно введён логин или пароль», E_USER_WARNING);
4. Логин и пароль могут быть не обработаны из-за серверной ошибки — throw(«Ошибка на сервере», E_USER_ERROR);

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

Можно написать с десяток своих классов, наследником Exception и как в языке java разруливать их.

У исключений уйма применений и они достаточно удобны, когда знаешь как ими пользоваться.
А с каких пор сообщения типа notice и warning должны прерывать поток выполнения и сворачивать стек? исключения не могут быть использованы для таких сообщений
Простите, я не совсем вас понимаю.

Исключения не прерывают выполнение, обычно… Это да… Но если у вас свой обработчик «error_handler», то вы можете и прервать. Я говорил именно про этот случай.

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

Можно написать «class ExceptionUserError extend Exceptions {}» и использовать его для обозначения ошибок или неверно введённых данных от пользователя.

Почему нет?

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

Исключение прерывает нормальный поток выполнения программы и сворачивает стек к близжайшей точке, где возможна обработка этого же исключения. Но логика сообщений notice и warning не подразумевает прерывание нормального потока выполнения программы. Именно поэтому использовать исключения для этих целей не нужно
1. Да, мне тоже очень нравится когда код предсказуем. Тут я с вами не хочу даже спорить. Но я не изобретаю велосипед, я лишь немного «нестандартно» использую исключения.

2. Попробуйте такой «финт ушами».
set_error_handler("UserErrorHandler");

function UserErrorHandler($errno, $errstr, $errfile, $errline){
switch ($errno) {
case E_USER_ERROR:
trigger_error("*USER ERROR* [$errno] $errstr
\n".
" User error on line $errline in file $errfile
\n");
exit(1);
case E_USER_WARNING:
print("*USER WARNING* [$errno] $errstr
\n".
" User warning on line $errline in file $errfile
\n");
break;
case E_USER_NOTICE:
print("*USER NOTICE* [$errno] $errstr
\n".
" User notice on line $errline in file $errfile
\n");
break;
}
return true;
}



PS Да, я понимаю что это идеологически не верно, но чёрт возьми, на мелких проектах это очень удобно :)
Главное, чтоб то, что удобно на мелких проектах не вошло в привычку.
Нелогично. Исключение, как средство сообщения об исключительных ситуациях должно использоваться только в последнем случае. Во-первых — потому что такова суть исключений, во-вторых, потому что некорректный ввод не является исключительной ситуацией (это вполне ожидаемая ситуация). И в-третьих — потому что поведение все таки может быть разное: самый простой пример — только в последнем случае мы пишем в лог о серверной ошибке, для того, чтобы позже с ней разобраться
>перебрасывает его с помощью throw
еще надо отметить, что перебрасывать надо именно само исключение, а то некоторые говнокодят catch (Exception $e) {throw new Exception($e->getMessage());}
А концепции inner exception как в .NET, в PHP нету?
дотнетом вообще не владею.
это оно? it.toolbox.com/blogs/coding-dotnet/innerexception-14200
если я правильно понял, то это хранения одного эксепшена в другом, что можно вполне организовать и на пхп.
только не запутывание кода ли это? если есть, например, 3 уровня, 1й ловит второй, а 2й — 3й, и каждый перебрасывает свой эксепшн, то 1й уровень о 3м знать не должен, а то банально инкапсуляция нарушается
Не совсем, все несколько хитрее.

Оборачивать можно внутренние эксепшены в том случае, если их выбрасывание наружу ничего не скажет о сути произошедшей ошибки. Например, если у нас в процессе логина вычисляется какой-нибудь хитрый хеш, и оттуда появляется DivisionByZeroException — мы не будем выбрасывать его наружу (какое нафиг деление на ноль в логине?) — мы обернем его в FaultAutentificationException с пояснением и выкинем.
а какая польза для верхнего уровня от этого? или там как-то автоматически разворачивается стектрейс?
Исключительно в прозрачности обработки исключительны ситуаций.
Если даже и нет, Вы можете создать свой класс, наследованный от Exception, который будет в конструкторе принимать вложенное исключение.
Мне вот больше интересно, а в PHP можно получить стектрейс исключения?
//
(я тоже .NET разработчик)
а, наверное, getTrace() как раз и делает то, о чем я говорю.
В общем то статья не о PHP, так как стратегия обработки исключений одинакова для многих языков.
А вообще спасибо, для себя много не узнал нового, но для других будет очень полезно.
А как кстати с производительностью при перехвате исключений? как будет быстрее вернуть какой-то параметр и обработать его или словить исключение?
Быстрее вернуть параметр, естественно. Исключение — это медленно.
М… это в php так? я могу сказать что в питоне — если есть часто вызываемая ф-я где вы время от времени(не очень часто) бросаете исключения и ловите их, то это будет быстрее чем постоянно проверять что же вернула функция.
Если не часто — то нормально. Просто следует помнить, что исключение не может быть вариантом штатного завершения функции.
Если у исключения происходят настолько часто, что влияют на производительность — надо менять логику приложения.
Согласен, сам недавно сталкивался с такой проблемой… главное вовремя понять что исключения не надо пихать во все щели :) с исключениями было 15-20% загрузки процессора, без — ближе к 1%. прочувствуй разницу, как говорится.
Абсолютно согласен, использование исключений должно быть прежде всего уместным.
и ещё с форума, как раз обсуждаем
phpclub.ru/talk/showthread.php?s=&threadid=114258&rand=12
Важно понимать, что механизм исключений по своей сути недалеко ушел от всеми любимого GOTO. На тему «неправильно введённые логин или пароль — это не исключительная ситуация!» — весьма спорное утверждение. Неправильно заполненная форма — это исключительная ситуация или нет?
кроме как переход в другую точку кода, что ещё у них общего?
А кроме перехода в другую точку кода, в чем их отличие?
goto определяет, куда будет передано управление. throw — нет.
с тем же успехом можно сказать, что и любая управляющая структура — тоже недалеко ушла от goto. тот же if — он тоже изменяет течение программы.
Форма — это вообще не ситуация, это набор данных, введённых пользователем.

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

try {

Signup(R(«username», «email», «capcha»))->validate()->register();

} catch (IJsonable $e) {

JSON(Fail($e));

}

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

$user->assign( array('usrname','email','captcha'), $request )->register()->log->success
and $response->content= array( 'message' => 'registered', 'user' => $user )
or $response->content= array( 'message' => 'error', 'log' => $user->log )
;
Пардон, а Data_Access_Exception — это какой стандарт именования функций?
я такого мнения что исключения нужно использовать только тогда когда в программе происходит ошибка коотрая «никогда» не должно произойти — вот это исключительная ситуация. Остальное все можно обработать на уровне кодов ошибок и тд

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

Однако если у нас помер запрос в методе query (ну вот кто-то забыл отфильтровать кавычки), то это просто внештатная ситуация, можно вернуть например false и выдать ошибку юзеру мол извините, на сервере проблемы. однако это ни в коей мере не мешает работе приложения, оно просто уйдет в другую ветку логики
А если уходить некуда? Нужно выкидывать исключение :)
Согласен.
И применимо это не только для PHP, но и для других языков, работающих с исключениями.
UFO just landed and posted this here
Большое спасибо, Вы правы.
Лично мне удобнее классы исключений определять там, где они используются. Например, Model_Exception может выбрасываться в классе Model_User или Model_Product_Type. А именование исключений в обратном порядке вынуждает меня дублировать структуру каталогов. Мне это неудобно.
UFO just landed and posted this here
Вот с этим я с некоторой натяжкой могу согласиться. Хотя в моей практике такого небыло. Exception всегда на нижнем уровне. Но и такого, что в каталоге только Exception, тоже небыло.
UFO just landed and posted this here
Ну а тут мне нечего возразить :)
лучше просто дождаться 5.3 :)
UFO just landed and posted this here
случатся неймспейсы
UFO just landed and posted this here
Как ни крути, но вряд ли люди будут создавать неймспейсы Application, Logic, Data\Access только для того, чтобы сложить в них исключения.
UFO just landed and posted this here
Так ли часто мы пишем библиотеки на PHP? Столько уровней, прямо операционная система, а не сайт.
В 90%+ процентах случаев паттерна MVC с головой, а необходимые библиотеки давно написаны.

И хотя статья правильно описывает работу с исключениями, но она больше бы подошла к разработке на java а не php. Особенно, учитывая еще и тот вариант, что в PHP есть fatal_error-ы, которые не являются исключениями, не перебрасывваются никуда и тупо стопорят исполнение программы на месте. Поэтому исключения в php сейчас выглядят как костыль, который есть для галочки.
Оставшиеся 10 — самые интересные ;)
UFO just landed and posted this here
у тебя все формы состоят из одного единственного поля, что ты кидаешься исключениями?
UFO just landed and posted this here
ещё есть аутентификация, права, целостность данных в базе и прочие необходимые проверки. они тоже примёрживаются в одно исключение?
UFO just landed and posted this here
«одного типа» — понятие абстрактное.
при формировании респонса нужно выдавать все ошибки (строка вместо числа, идентификатор уже занят, нет прав на использование хтмл), а не только первого попавшегося типа…
UFO just landed and posted this here
нет прав на содание сущности — нужно выдавать форму перелогина и ошибки в данных.
UFO just landed and posted this here
Отличный способ. Кстати говоря, в Symfony, начинаю с 1.1 валидация организована примерно так же.
UFO just landed and posted this here
UFO just landed and posted this here
ошибка подключения к базе данных является исключительной ситуацией только для метода подключения.
зачем высокоуровневому модулю знать обо всех низкоуровневых, и исключениях, которые могут там возникнуть?
Гм-гм… то есть, если к БД подключиться не удалось, уровень работы с БД должен проворчать типа «блин, нет базы. Ну и фиг с ней, будем работать на файлах и вообще, у меня будет своя база с блэкджеком и шлюхами!» Так что ли?
да, представь себе. приложение не должно падать только от того, что одна база вдруг отвалилась.
А мне казалось, что это зависит, в первую очередь от контекста использования. При чтении данных такой финт еще может прокатить, но при модификации — уж лучше сразу под поезд.
ага, расскажи это пользователю, который 30 минут набирал текст…
Эх, хороши исключения, да вот в PHP они реализованы с изъяном — библиотечные функции, а так же различные исключительные ситуации вроде 1 / 0 или null->foo() реализованы не с помощью исключений. Конечно, есть способ с set_error_handler и выкидыванием, но опять же, он почему-то не всегда работает. Например, в том же случае с null->foo обработчик, установленный с помощью set_error_handler почему-то не срабатывает. А это значит, что вместо исчерпывающего стектрейса я получаю невнятное сообщение всего лишь с одним номером строки.
Статья хорошая.

Расскажу свой способ определения, когда стоит выбрасывать исключение а когда нет: когда я пишу код, то предполагаю что все методы должны возвращать только один любой тип данных (сами определяем какой: Объект определенного типа, массив, число, строку и т.д.) Если по каким-то причинам в методе нужно вернуть еще что-то, или вернуть другой тип данных — выбрасываем исключение
UFO just landed and posted this here
Интересно, если нужно ролбэкнуть транзакцию, вы тоже плюете и заканчиваете выполенение кода? Не смешите, исключения выбрасываются для того чтобы их обработать.

По поводу «данные должны быть всегда те, которые были запрошены» — метод должен возвращать тот тип данных, который он продекларировал, а не менять его в зависимости от настроения. Если каждый метод в вашем коде возвращает разные типы данных, то лучше подумайте как этого избежать. Тогда отпадет вопрос и с исключениями
UFO just landed and posted this here
А с параметрами передаваемыми по ссылке или out параметрами методов вы не знакомы?
вы в мой профиль заглядывали, дядя?
Спасибо, статья действительно очень познавательна, но есть вопрос:
Методы класса не должны перехватывать исключения, сгенерированные другими методами этого же класса. Библиотека вообще ничего не должна знать о том ...

Вот смотрите, у меня есть класс, который читает информацию из файла. Если, вдруг, что-то там происходит, я вызываю исключительную ситуацию. Но тут вопрос, если не обрабатывать её просто в классе, типа:
try
readData
except
print error
А если отлавливать исключение каждый раз, когда нужно прочитать какой-то параметр из файла? Как здесь быть: Оставить отловление исключений в том классе, или испортить читабельность кода?
Я бы еще добавил рекомендацию написать для приложения обработчик Unhandled Exceptions, который обязательно должен вести лог необработанных исключений. Часто об этом забывают поначалу.
сорри, это не ответ, я по ошибке не в корень запостил.
Ничего =). Зато я на него внимания обратил и действительно задумался. Спасибо.
UFO just landed and posted this here
Небольшой но содержательный и полезный пост! Очень здорово было бы рассказать о применении эксцепшенов в «рядовых» ситуациях (ну в стиле «Избавляемся от «if(!$db) die('...');»», чтобы задать вектор мышления у людей, не пользовавшихся ими до этого.
для этого достаточно почитать документацию.
Не думаю, что достаточно — хоть там и есть примеры, но…
Всё приходит с опытом, и если он есть, почему бы им не поделиться?
В крупном приложении необходимо использовать свои собственные классы исключений (унаследованные от встроенного Exception)

В PHP есть не только встроенный Exception, но и еще много разных Excception'ов. В числе прочих там есть и LogicException. И свои классы исключений иногда лучше наследовать от подходящего базового класса (ну и, само собой, вовсю использовать собственно все предоставляемые «бесплатно» встроенные классы, а не только Exception).
Sign up to leave a comment.

Articles