Pull to refresh

Comments 70

Благодарю за прекрасную статью. Но мой дух противоречия требует крови:
Следует упомянуть что разрабатывая классы эксепшенов мы должны следовать принципу информативного интерфейса. Грубо говоря — учитывать их логический смысл, а не физический. Например, если адреса у нас храняться в файлах, то отсутствие файла адреса вызовет FileNotFoundException. Мы же должны перехватить его и вызвать более осмысленный AddressNotFoundException.

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

Или вы имели ввиду, что КАЖДЫЙ адрес хранится в отдельном файле, и его отсутствие по бизнес-логике это отсутствие записи в базе?
Или я под утро уже засыпаю и торможу, или вы чуть ушли от локаничности, за которую ратуете в статье.

И тем не менее статья супер :)
Вы не совсем поняли. Судя по контексту, автор имел ввиду ситуацию, когда каждый конкретный адрес-сущность хранится в отдельном файле
Например, если адреса у нас хранятся в файлах

в файлах, но не в файле.
А вот в случае хранения в файле и отсутствия этого самого файла (таблицы или даже базы) — ошибка более глобальная, чем отсутствие конкретного адреса — вы правы.
Как ни странно, но в C++ тоже очень часто или забывают про существование исключений, или наоборот перебарщивают с их использованием.
Было бы здорово, если бы вы рассказали и про негативные последствия чрезмерного употребления исключений (если они есть).
Собственно та же самая проблема, что и у в любом другом языке: потеря контроля над последовательностью исполнения. Исключение — это нелокальный GoTo. То есть в коде
create_temporary_table();
read_json();
remove_temporary_table();
при отсутствии исключений таблица будет уничтожена всегда, а вот если read_json кинет исключение — то всё «рассыпется».

Грубо говоря написания кода без исключений и с исключениями — это сильно разные навыки. А если учесть, что нормальная поддержка исключений появилась в PHP только начиная с версии 5.6, которому меньше года от роду… стоит ли удивляться, что мало кто использует исключения в PHP???
а кто так пишет? Это и без эксепшенов проблема.
таблица должна или уничтожаться субд, или должен быть обработчик на случай проблем…
ну или переиспользовать таблицу с обнулением в процедуре создания (если логика позволяет).
В общем тут ошибка архитектуры и без эксепшенов.
А это от архитектуры зависит. И от того, что мы в этих таблицах храним. Если там какая-нибудь статистика, то можно и без обработчика (вернее он будет внутри функции create_temporary_table). Вы правы в том смысле, что PHP во многих случаях спасается за счёт того, что за ним «убирает» DBMS (собственно именно этим разработчики PHP годами аргументировали своё нежелание реализовывать final), но это не значит, что это будет проиходить совсем уж всегда.
Да я больше о том, что даже если эксепшенов там не будет, то ошибка всё равно там может произойти, и как следствие до удаления таблицы мы можем не дойти. Эту ситуацию нужно предусматривать полюбому. Ну или забить, в надежде что ничего не будет, и потом ты ее сам и удалишь если что, или будет висеть чуть мусора, места на диске много…
Я понимаю, что это синтетический пример, но…

1. try catch finally
2. using (var transaction = BeginTransaction())

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

Но главное не в том, как эту ситуацию обработать, а в том, что её нужно обрабатывать — а без исключений об этом не нужно было даже и задумываться.
Воистину, век живи — век учись. Большое человеческое спасибо за подробную и полезную статью; я как раз один из тех, кто уже лет пять активно пишет на старом добром Пыхчанском, и до сих пор ни разу не выбрасывал свои собственноручно написанные объекты-эксепшены. А ведь в этом действительно есть большая польза, почему я сам до этого никогда не мог додуматься?..

В общем, еще раз спасибо за статью, да еще и с множеством примеров, да еще и по главам разбитую)
Если всё работает и никаких проблем нет — то не за что и ругать себя
Мм… Ну я вообще в корне не согласен с подобным утверждением. Для уровня начинающего кодера оно конечно вполне нормально, таким образом расценивать свою работу: нашлёпал какую-то кучу кода. Если всё работает — значит молодец.

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

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

Вот вам статья моя на данную тему, немного более широко разъясняющую мой посыл: believer.su/programmirovanie/chistiy-kod
Этот пост немного про другое. Тут архитектура не принципиальна — хоть в одну функцию все пишите. Тут про безбажность — как не допустить баг, а если допустил — не пропустить.

В целом же, имхо, когда у разработчика довольно большой опыт, то после прочтения задачи архитектура рисуется за пару минут. И делать по правильному ничуть не дольше чем быдлокодить, как правило. Даже более того — опытный разработчик не будет быдлокодить в принципе, ибо в таком случае он не получит удовольствие от работы.
Прекрасный слог. Последовательно, обстоятельно. Мы бы с Вами написали много прекрасных статей. Жаль, что Вы из «другого лагеря» =)
Послесловие, правда, несколько экспрессивное…
После продолжительного рассказа про иерархию исключений, документцию, интерфейсы и прочее показывать код, утыканный «throw new Exception» — это, как минимум, странно.

Про обработку исключений мало сказано, среди примеров презираемый многими «catch (Exception)»…
я думаю, что catch «на месте» должен употребляться только для переброса исключений, как в примере с FileNotFound => AdressNotFound. Во всех прочих случаях исключения должны обрабатываться «общей пачкой» на уровне приложения.

<? namespace App\Support\Exceptions\Handlers;

use App\Supprot\Exceptions\Contracts\ExceptionHandlerContract;
use App\Supprot\Exceptions\Handlers\DefaultHandler;

class ExceptionHandler implments ExceptionHandlerContract {
      protected $defaultHandler = DefaultHandler::class;
               
      protected $customHandlers = [];    

      public function handle(Exception $e)
      {
             $exceptionType = get_class ($e);

             if(array_key_exists($exceptionType, $this->customHandlers))
             {
                   $handler = new $this->customHandlers[$exceptionType];

                   return $this->runHandler($handler, $e);
             }
             
             return $this->handleDefault($e);
      }

      protected function handleDefault(Exception $e)
      {
           $handler = new $this->defaultHandler;

           return $this->runHandler($handler, $e);
      }

      public function addCustomHandler($exceptionClassName , $handlerClassName)
      {
           $this->customHandlers[$exceptionClassName] = $handlerClassName;
      }

      public function runHandler(ExceptionHandlerContract $handler, Exception $e)
      {
             return $handler->handle($e);
      }
}

Где-то в дебрях инициализации приложения:
      $app->exceptionHandler->addCustomHandler(CustomHandler::class);

Ну и сам запуск приложения.
rquire_once('../paths.php');

rquire_once(VENDOR_AUTOLAD_PATH);

$app = new App;

try 
{    
  $app->run();
} 
catch (Exception $e) 
{

    $app->exceptionHandler->handle($e);
}


Простите, если где-то ошибся накатал прямо сейчас в браузере…
>Во всех прочих случаях исключения должны обрабатываться «общей пачкой» на уровне приложения.
Это убивает весь смысл, получается тот же самый trigger_error.
Код в последнем примере является избыточным, но в то же время очень характерным для классического пхпешника.
Но вообще-то обработчик исключений вызывается сам, его не надо специально вызывать.
Поэтому код сокращается до

$app = new App;
$app->run();

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

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

А вот если мы во время обработки картинок улетели по памяти — то да, здесь как раз пригодится стандартный обработчик.
Код был образный, и накатан прямо в браузере.
Вот те раз. Умыли меня. Во всех моих приложениях есть обработчики на кучу различных «нефатальных» исключений. Например ModelNotFoundException в большинстве случаев преобразуется в PageNotFoundException, который в свою очередь обрабатывается на уровне приложения возвращает страницу 404 в текущем лэйауте. Если же запрос аяксовый, то возвращается json в формате соответствующем jsonapi. Есть также различные исключения, которые обрабатываются на уровне приложения. Например ValidationFailureException в при обычных запросах Делает
redirect()->back()->withErrors($validator->errors);

Эти самые ошибки пишутся в сессию, при повторной обработки запроса, после редиректа извлекаются в MessageBag, и выводятся на странице пользователю.

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

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

Это все к мануалу, статья не про это, а про то что, как верно подметил FanatPhp — обработка исключений не заключается чисто на try catch
среди примеров презираемый многими «catch (Exception)

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

В обучающих материалах никакой «контекст» не может оправдать дурной пример. Вас же никто не заставляет детально описывать все классы исключений, вместо «throw new Exception» можно просто написать «throw new InvalidStatusException».

Это все к мануалу

Как бросать исключения — это тоже в мануале описано. Но смысл-то как раз в том, чтобы описать в статье, как правильно работать с исключениями, а ловля и обработка исключений — это тоже нетривиально.
В большинстве случаев хватает стандартных spl-исключений, про них можно было бы более подробно упомянуть автору. Часто нет смысла городить лишнюю иерархию на каждый чих, не считаю это хорошей практикой, но есть места, где это действительно нужно и удобно.
Согласен, разведение мощной иерархии исключений, как минимум, с отдельным типом исключения под каждый модуль «для удобства логирования», как предлагается в статье — сомнительная практика. Надо исходить из того, как будут ловить исключения и как обрабатывать, а не машинально добавлять классы.
> СActiveRecord::find() не бросает эксепшн, и это логично
Не соглашусь, если я хочу открыть конкретную страничку с постом (допустим есть моделька с постами), то лучше бы этот метод кидал NotFound exception, в таком случае обработка дальше не пойдет и фреймворк кинет 404 (если он понимает этот эксепшн) или (нежелательно) 500.
А вот CActiveRecord::where(['id' => 342342]) вполне может выдавать просто нули, так как подразумевается что данных может не быть.
Поиск по БД не имеет ни какого отношения к какой-то там страничке, не путайте уровни. Если чего-то нет в БД — на глобальном уровне это штатная ситуация. Если же вам где-то надо показать 404 при отсутствии чего-либо в БД, то этим должен заняться контроллер, который сделает проверку на наличие данных в БД и кинет специальное исключение, которое интерпретируется, как 404. А вот ORM/ActiveRecord ну никак не может кидать HttpException.
Простой контрпример.
Пустая корзина у пользователя это страница пустой корзины или 404?
Ну тут все просто: нахрен нужны покупатели, которые только шарятся по магазину, жирными руками все прилавки заляпали, и ничего так и не купили? На выход, товарищи, на выход!

Так что это 402, очевидно. А при повторной попытке — 503.
Корзина должна при старте сессии создаться, так что да, если она не создалась то это ошибка приложения.
Не, ну отсутствие корзины и пустая корзина несколько разные вещи.
Зачем? Можно при добавлении товара.
Не можно, а нужно, ИМХО. Человек не делал нагруженных приложений просто :)
Человек просто писал на Битриксе, там это норма вещей, и без корзины в БД что-то да отваливается.
Исключения на то и называются именно исключениями, т.к. исключительные ситуации.
Т.е. если СActiveRecord::find() должен всегда что-то вернуть, то он должен это что-то вернуть, иначе исключение.
Можно еще с помощью исключений передавать данные обратно, но это спорный метод, это очень похоже на слабоуправляемое GOTO.
С чего вы решили что СActiveRecord::find() должен всегда что-то вернуть? мне кажется она должна «найти» или «не найти», обе ситуации не исключительные. Может и MySQL на SELECT должен бросать исключение при пустом результате?
where подразумевает что выборка может быть и пуста, а допустим find()/find_by_id() подразумевает что такой элемент есть, иначе дальнейшая логика ломается.
Логика может быть разная. Например, если элемент не найден, то создать его. Для ОРМ, как и для СУБД не найденная запись — норма. Если для бизнес-логики нет, то и бросайте исключение на её уровне.
СActiveRecord::find() также подразумевает наличие дополнительных условий установленные критериями, scopes и прочими. Так что все аналогично, find может вернуть найденную строку либо не найдя строку вернул null.
>С чего вы решили что СActiveRecord::find() должен всегда что-то вернуть
Я как раз и говорю — если он должен вернуть -то ожидаем что вернет. Но ведь вы можете решить, что например отсутствие данных это для вас исключительная ситуация. Т.е. по идее можно и исключение заюзать.
Но вот как по мне — тут главное некоторую грань не переступать, а то можно начать передавать данные через исключение.
Очень удобно, когда есть два метода: один кидает исключение, например UnexistentEntityException и всегда возвращает объект, например ActiveRecord::getById(), а второй возвращает объект либо null, например ActiveRecord::findById().
Где должны располагаться описания собственных классов исключений?

В том же файле, что и класс, кидающий свое специфическое исключение? Вроде удобно, но противоречит п. 3 PSR-1 — каждый класс должен быть в своем файле.

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

Что касается автозагрузки, то если у вас нормальный автозагрузчик (например как в Yii2) то проблем с автозагрузкой никогда не возникнет.

Про папочки — это как душе угодно. Я обычно делаю папку exceptions в папке модуля/компонента и туда складываю все классы эксепшенов.
Таким образом получается вот так (неймспейсы повторяют физическое размещение):

kladr\KladrService
kladr\exceptions\AddressNotFoundException
kladr\exceptions\UnresoledAddressException
Вот я не знаю.
Читаешь бывает вопрос, а там «PSR-1», «пространстов имён», то есть человек, кажется, более шаристый чем 90% пхпшников.
А сам вопрос какая-то глупость.
В тех же PSR исчерпывающе же описан ответ на вопрос «в каких файлах что должно располагаться».
>а там «PSR-1», «пространстов имён», то есть человек, кажется, более шаристый чем 90% пхпшников.
Экак вы с плеча кучу пехепешников обидели) Особенно учитывая то, что полностью PSR следует печально маленький процент даже успешных и красивых по коду проектов)
Про finally забыли расказать, и spl exceptions для общего развития стоило упомянуть.
Это же все в манулае можно прочитать. Я же хотел сосредоточиться на немого другом аспекте: как верно подметил FanatPHP — что обработка исключений не заканчивается на try catch
Мне кажется, что самого главноего в статье не написано :)
Что обработка исключений не заключается в коде вида
try {
   // something
} catch (Ecxeption $e) {
    die($e->message());
}

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

Хотел бы добавить пару моментов:

1) Никогда не бросайте базовый Exception, лучше использовать собственные исключения, либо на крайний случай один из SPL. Т.к. базовое исключение сложно словить.

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

class InvalidArgumentException extends \InvalidArgumentException implements MyApplicationExceptions {
}

теперь ваш обработчик ошибок и исключений будет проще.

3) Обработчик ошибок и исключений, не обязательно должен быть один в приложении. Особенно если вы используйте слоистую систему, вполне возможно Вам будет полезно иметь отдельный обработчик в каком-то слое, например вы захотите для Presentation layer выловить все кроме Http exceptions и сделать их таковыми.

4) Если Вам позволяет версия php, в 5.5+ не забывайте про finally, на ряду с try/catch является очень удобной, работая с исключениями.

<чуть чуть сарказма>Автор, в 4 абзаце речь идет о стандартах и лучших практиках, как насчет PSR1,2? :)</чуть чуть сарказма>
<чуть чуть сарказма>Автор, в 4 абзаце речь идет о стандартах и лучших практиках, как насчет PSR1,2? :)</чуть чуть сарказма>


Согласен. Просто я работаю в проекте который начали писать задолго до PSR. И переводить CodeStyle на PSR желаение нет: попробуйте смержить 2 больших файла, когда один разработчик переделал CodeStyle, а другой закоммитил +500 изменений — это может правреатиться в ад. Поэтому и не трогаем CodeStyle..., на качество кода как крути не влияет.
UFO landed and left these words here
Дело в том, что исключение это штука, которая помогает программисту код ошибки, сообщение и даже пердыдущее исключение удобно пробросить на верх стека. Возврат кода ошибки может быть не интересен контексту из которого была вызвана функция вовсе, но проброс этого кода придётся организовывать ручками.
UFO landed and left these words here
Вот типичный пример работы на разных уровнях.
Допустим, есть ORM — слой ниже бизнес-логики.
Как в продакшин режиме узнать, что что-то не так на уровне ORM?
При этом ORM вообще не знает какой логгер ошибок вы используете, на каком уровне он работает и используется ли он вообще или ошибка будет обработана как-то иначе.
Передачей кода ошибки очень сложно сделать такое разделение. Зато логгеру достаточно ловить все непойманные исключения.
UFO landed and left these words here
Интересно вот, почему обсуждение исключений сводится обычно всего лишь к пробрасыванию ошибок? Ошибка != исключительная контролируемая ситуация.
UFO landed and left these words here
Если не нравится — не используйте. Как по мне — наличие или отсутствие исключений в пхп коде никак не говорит о его качестве. Однако при большом количестве слоев абстракций механизм с кодами ошибок поддерживать намного тяжелее. При этом исключения это не просто механизм передачи ошибок.
UFO landed and left these words here
Дело в том, что коды ошибок и гетЛастЕррор это неSOLIDно.
Лишняя ответственность, неоправданный рост связности…
Выкинули исключение. Если в биз.логике это не исключительная ситуация, то можно и не выкидывать, и будет обычное ветвление, или ловить сразу, тут-же.
Или просто выкидывать, а когда потом, на будущем этапе развития появится воркараунд для этой ситуации, то прямо на месте, в контексте ответственности прописываем его перехват и обработку. Например переадресация на форму создания объекта которого может не быть, с плашкой о том, что не нашли, можете создать (к примеру как в википедии со статьями).
Но изначально всё уходит вверх, на умолчания.
В базовом алгоритме просто идет проверка на исключительность, и выброс исключения. Всё. Можно не разбираться дальше.
Его потом подберут, и доложат админу.
В свою очередь можно потом делать разбор по тому кому смс писать — девелоперу или админу. Если непонятная невыловленная ошибка, то деву, если там места нет, или файлик потерялся, то админу…
Но всё это другая ответственность. Класс или метод где произошло исключение, не должен знать всё это.
Отход от SOLID возможен. К примеру я не согласен с критиками Yii по поводу AR включающий и валидацию, и еще немножко шьющего. Так удобнее, и всё такое. Разумное обоснованное отклонение. Но отклоняться потому что «я так привык»… сомнительно.
UFO landed and left these words here
Или должна или нет?
Если должна, это частный случай.
А если не должна?
Все слои которые не должны были знать об исключении ВЫНУЖДЕНЫ о нем знать, просто чтобы его передать кому следует.
Сколько исключения существуют в PHP, столько их используем для проброса ошибок на разные уровни: от обработки системных критических ошибок (к примеру, упала база) и до пользовательских, с валидацией форм. А работа с откатом транзакций вообще является классическим примером использования исключений.
Исключения нужны, но использовать их для валидации пользовательских данных — перебор.
В посте не предлагается использовать их для валидации пользовательских данных — наоборот — есть пример где показано объяснено почему в этом случае не стоит бросать эксепшн.
Only those users with full accounts are able to leave comments. Log in, please.