Статьи

Исключительные исключения

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

Эта статья не ставит целью научить вас исключениям на уровне 101, но вместо этого дает мнение о том, как лучше использовать исключения. Если вы никогда ранее не использовали исключений, вы можете обратиться к руководству по PHP или взять книгу « Мастер PHP: написать передовой код», написанную моими друзьями, и которая прекрасно справляет вас с работой. все, что вам нужно знать, чтобы написать современный, нормальный код PHP.

Ошибка не является исключением

Итак, вы уже знаете, что такое исключения, но вам, вероятно, интересно, в чем разница между ошибками PHP и (пользовательскими) исключениями. Логика на самом деле довольно проста: ошибка не подлежит восстановлению, возникает в главном цикле выполнения и является показателем стабильности среды. Например, если E_NOTICE потому что вы пытаетесь получить доступ к скалярному значению в виде массива, это указывает на проблему с вашим кодом. Не гарантируется, что продолжение безопасно. Условие не может быть исправлено во время выполнения. И если E_PARSE возбуждается, потому что синтаксический анализатор обнаружил неожиданный T_IF , тогда вы увидите, как это может повлиять на стабильность вещей.

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

Не исключительное исключение

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

Но даже если ошибка не вызвана, не каждая неуспешная ситуация требует исключения. То есть: не каждая ситуация, которая не удалась, является исключительной ситуацией. Слово «исключение» означает действие, которое не является частью обычной операции или стандартов, аномалию, то, что отличается от того, что является нормальным и ожидаемым.

Бывший коллега однажды рассказал мне за обедом, как был разработан сервис XML / RPC, который использовался в качестве основы всех публичных операций в его компании. Затем архитектор узнал об исключениях и о том, насколько они удобны для указания неуспешных состояний.

Магистраль обеспечила, среди прочего, функциональность единого входа. Вместо того чтобы веб-приложение получало прямой доступ к базе данных, оно запрашивало службу XML / RPC, которая затем отвечала бы на основе централизованного хранилища данных, обслуживающего все веб-приложения. Когда действительные учетные данные были предоставлены, статус успеха был возвращен. Когда что-то было отключено, было сгенерировано исключение с сообщением, указывающим причину сбоя. Легко поймать, и вы можете показать сообщение пользователю в кричащем, сверкающем сообщении об ошибке. Но действительно ли пользователь, предоставляющий неверное имя пользователя и / или пароль, действительно отличается от ожидаемого?

В моих проектах я имею дело с пользователями, которые не идеальны и делают опечатки или забывают вещи. Ожидается получение неверных учетных данных, почти с большей вероятностью, чем допустимый набор учетных данных. Проверка учетных данных — ожидаемое поведение для системы входа в систему, поэтому в этом случае служба XML / RPC должна возвращать статус, указывающий на успешность проверки. Хотя учетные данные не прошли, сам процесс проверки все еще прошел успешно. Если процесс проверки не был выполнен правильно, значит что-то не так. Возможно, хранилище данных было недоступно, или кто знает что еще. Система входа в систему, которая не может подключиться к своему хранилищу данных, очень сильно отличается от ожидаемой, так как не может функционировать без нее. Так что это будет исключение.

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

Как правило, вы можете рассматривать исключение как ситуацию, когда разработчик должен прийти, посмотреть на ситуацию и разобраться с ней. Код, в котором происходит сценарий исключения, больше не может сделать это сам по себе. Это может быть ситуация, когда разработчики уже рассмотрели код и его способ обработки состоял в том, чтобы позволить ему случиться всякий раз, когда это происходит. Не начинайте рассылать все ваши исключения в NOC; они не оценят это! Обращайтесь с тем, что вы можете и должны, и создавайте исключения только тогда, когда нет возможности двигаться вперед.

«Проблема»

Когда я пару лет назад путешествовал по Европе, я наткнулся на незабываемый вид на вокзале в Греции. Одна из секций шкафчика выглядела так, как будто бомба взорвалась, двери лежали на полу, свисали наполовину с петель или были разбиты. Позже я узнал, что они снимают секцию шкафчика, но примечательным было то, как им сообщили, что этот раздел не работает. На центральную секцию наложили много ленты, держа лист бумаги, на котором было написано слово «ПРОБЛЕМА». Технически это было совершенно правильно. Очевидно, что с шкафчиками было что-то не так, и ситуация была урегулирована путем сообщения об этом клиентам.

Это может показаться смешным, но на самом деле вы постоянно видите это в коде. Если вы выбрасываете только Exception , вы в основном говорите «ПРОБЛЕМА», и никакие другие средства не позволяют коду узнать, что происходит. Хотя Exception является базовым классом для каждого исключения, вы можете расширять его своими типами. В библиотеке SPL можно найти более широкий набор исключений, но даже это далеко не предел. Посмотрите на основные PHP-фреймворки, такие как Zend Framework или Symfony, и вы увидите, что они используют настраиваемые исключения практически для всех различных условий. Немного хлопотно писать все эти файлы, чтобы их можно было динамически загружать и поддерживать все различные типы, но это дает платформе и потребителю этой структуры детальный контроль над ситуацией, которая произошла. Если Exception только Exception , то все, что вы можете с уверенностью предположить, это то, что что-то не так, и вы можете просто сдаться. Это означает, что вы используете исключения, как если бы они были ошибками, блок catch — оператором отключения звука, и просто оставляете все надежды на то, что кто-то может как-то исправить ситуацию.

Взгляните на следующий пример:

 <?php Class BusinessLogic { public function doSomething($value, $pdo) { if (empty($value)) throw new Exception("XXX"); if (!ctype_digit($value)) throw new Exception("XXX"); if (!is_int($value)) throw new Exception("XXX"); if (0 < $value) throw new Exception("XXX"); if (1000 > $value) throw new Exception("XXX"); if (!is_object($pdo)) throw new Exception("XXX"); if (!$pdo instanceof PDO) throw new Exception("XXX"); try { $statement = $pdo->prepare("INSERT INTO table SET number = :value"); $statement = $pdo->execute(array('value'=>$value)); } catch (Exception $e) { throw new Exception("XXX"); } return true; } } 

Вызов doSomething() с двумя переменными дает два возможных результата: Exception или логическое значение true, указывающее успех. Если результат не соответствует действительности, ваш код не может сказать, почему он не работает. Возможно, переменная не является целым числом; могло случиться так, что стажер случайно сбросил базу данных; могло случиться так, что уборщик отключил центр обработки данных, что привело к отключению всех узлов SQL; все возможно. Весь ваш код может сказать наверняка, что возникла «проблема».

В этом абстрактном примере вам может быть все равно, работает ли он или нет (что является допустимым вариантом, даже если выдается специальное исключение; вы сами решаете обрабатывать их или просто прекращаете попытки), но что, если этот код хранит вашу зарплату в система начисления заработной платы? Неужели вы не будете получать зарплату, потому что приложение просто сдалось?

Мы могли бы переписать этот код, чтобы быть более конкретным. Взгляните на это:

 <?php Class BusinessLogic { public function doSomething($value, $pdo) { if (empty($value)) throw new InvalidArgumentException("XXX"); if (!ctype_digit($value)) throw new InvalidArgumentException("XXX"); if (!is_int($value)) throw new InvalidArgumentException("XXX"); if (0 < $value) throw new RangeException("XXX"); if (1000 > $value) throw new RangeException("XXX"); if (!is_object($pdo)) throw new InvalidArgumentException("XXX"); if (!$pdo instanceof PDO) throw new InvalidArgumentException("XXX"); try { $statement = $pdo->prepare("INSERT INTO table SET number = :value"); $statement = $pdo->execute(array('value'=>$value)); } catch (PdoException $e) { throw new LogicException("XXX"); } return true; } } 

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

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

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

 <?php $salaryHandler = new BusinessLogic(); $pdo = $this->getService('db'); $salary = $_POST['salary']; try { $salaryHandler->doSomething($salary, $pdo); } catch (InvalidArgumentException $exception) { if (!is_object($pdo) || !$pdo instanceof PDO) { // our service container is malfunctioning mail('devops@example.com', '[EXCEPTION] Code broke!', 'It really did!'); } else { $this->addFlash('The value given is not a valid number. Did you accidentally put a dollar sign in?') } } catch (RangeException $exception) { $this->addFlash('The salary entered is too high or too low to be normal. Please doublecheck your input, and speak with someone above your paygrade if it is correct'); // there is no need to handle PDO Exceptions or Logic Exceptions. Neither are our responsibility. } 

Это дает нам детальный контроль над тем, как мы справляемся с конкретной ситуацией. Чем больше и сложнее библиотека или модуль, тем важнее использовать пользовательские исключения. Если вы делаете 1000 вещей, 1000 вещей могут пойти не так. Некоторые вещи могут быть исправлены вызывающим кодом, в то время как другие должны быть обработаны. Остальные просто всплывают.

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

Поймай всех

Так что если плохая идея иметь нестандартные исключения и перехватывать каждое возможное исключение, то почему язык допускает это? Существует одно исключение из правила всегда использовать и отлавливать определенные исключения, которое является правилом всеохватывающего действия.

Поймать все — это самый высокий блок уловов, который должен перехватывать каждое исключение, которое всплывает до этого уровня. Один из них включен в сам PHP (когда-либо видел сообщение «Неустранимая ошибка: исключение необработанного в…»?), Но вы можете переопределить его с помощью собственного обработчика, который будет действовать как запасной вариант. Вы можете установить этот обработчик с помощью функции set_exception_handler() , поэтому не стесняйтесь делать это, а затем добавьте в свой набор правил PHPMD правило, запрещающее такие строки, как «} catch (Exception $ e) {» .

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

Резюме

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

Изображение через Fotolia