Передовой опыт обработки исключений PHP
Обработка ошибок или других неблагоприятных ситуаций крайне важна при создании надежных приложений PHP. В то время как ошибки были основной конструкцией для этого в PHP 4, исключения существуют начиная с PHP 5. В настоящее время их следует считать основным механизмом для обработки альтернативных или исключительных путей. Однако кажется, что эти альтернативные пути все еще не всегда привлекают внимание, которого они заслуживают.
Надлежащая обработка исключений требует значительных усилий, но в конечном итоге приводит к гораздо более стабильному приложению. Разумная стратегия обработки исключений дает понять, каких исключений следует ожидать (и, следовательно, обрабатывать!) в данной точке кода. Более того, он будет поддерживать инкапсуляцию и абстракцию, которые вы тщательно применяли в своем объектно-ориентированном дизайне. И последнее, но не менее важное: это должно упростить отладку.
В этом посте я хотел бы познакомить вас с набором лучших практик, которые мы применяли в Moxio на протяжении многих лет. Мы обнаружили, что они очень хорошо работают для нас, но имейте в виду, что они являются нашими лучшими практиками; ваш пробег может отличаться. Следующие рекомендации предназначены для PHP-кода, но основные принципы, лежащие в их основе, также (с некоторыми переводами) будут работать и для аналогичных языков.
Типы исключений
Мы проводим различие между двумя типами исключений верхнего уровня, которые определяет библиотека PHP SPL. это LogicException
и RuntimeException
. Интерпретация этих двух типов в литературе различается. Однако мы придаем им следующее значение:
-
LogicException
— это исключение, которое требует исправления или изменения кода. Под «кодом» мы подразумеваем не только исходный код, но и любой контент, управляемый разработчиком. Это включает в себя конфигурацию и содержимое базы данных, которые не обслуживаются конечным пользователем системы.LogicException
в основном является внутренней защитой или утверждением. В идеально написанном и смонтированном коде такого никогда не должно быть. -
RuntimeException
— это исключение, которое также может возникать в коде, который написан и сконфигурирован «идеально». Такое исключение может быть вызвано вводом данных от конечного пользователя или ошибкой внешней системы (взаимодействия с ней). Когда выдается исключение
, оно не должно требовать исправления в коде. Однако, если исключение не будет перехвачено, мы должны добавить код для его обработки. Это может означать регистрацию ошибки, использование резервной стратегии, сообщение об ошибке пользователю или их комбинацию.
Обратите внимание, что эта интерпретация отличается от общего восприятия в Java, где RuntimeException
— это то, что мы назвали бы LogicException
. Более того, в наших стандартах мы классифицируем все исключения по одной из этих категорий. Мы не создаем исключения как прямой подкласс
.
Мы видим возможные варианты RuntimeException
, которые могут быть выброшены или всплывают из функции как часть контракта этой функции. Это означает, что такие исключения должны быть аннотированы в функции с помощью @выдает
.
Если функция является частью реализации интерфейса, этот интерфейс должен указывать, что (надтип) исключение в случае может быть сгенерировано. Аннотировать такое исключение также в реализации не нужно. Функция, реализующая интерфейс, никогда не должна генерировать исключение времени выполнения, не объявленное для интерфейса. Такой случай был бы нарушением принципа замещения Лискова.
Наоборот, мы рассматриваем разные виды LogicException
, чтобы не быть частью контракта функции. Конечно, мы предполагаем идеальную реализацию, но в то же время мы знаем, что где-то всегда может быть ошибка. Поэтому LogicException
всегда можно ожидать и неожиданно одновременно. Следовательно, аннотировать его с помощью @throws
нежелательно.
Создание подклассов исключений
В соответствии с этими рекомендациями создание иерархии подтипов до RuntimeException
очень желательно. Чем более специфичны исключения, которые мы выбрасываем (и аннотируем как часть нашего контракта), тем более детально мы можем их обрабатывать. Однако подклассы LogicException
не нужны. В любом случае они не являются частью контракта функции.
Перехват исключений
Для нас RuntimeException
является проверенным исключением. Когда такое исключение может быть вызвано функцией, вызывающая функция должна либо перехватить это исключение, либо объявить его как возможное исключение от самой себя, используя @выдает
. Перехват исключения во время выполнения является хорошей идеей, когда вызывающий код может разумно обработать исключение или когда он может повторно создать его на более высоком уровне абстракции (см. ниже). По крайней мере, важно подумать об обработке этих исключений при вызове функции, которая может их генерировать.
Никогда не следует перехватывать логическое исключение, по крайней мере, не основанное на типе LogicException
или его подклассе. Эти исключения никогда не должны возникать в правильной реализации. Поэтому не имеет смысла пытаться обрабатывать их вдоль линии. Вместо этого следует исправить логику, вызвавшую исключение.
В какой-то момент может возникнуть необходимость перехватить все исключения , включая оба варианта RuntimeException
и LogicException
. Поскольку нам требуется как минимум PHP 7, мы делаем это, используя Throwable
в нашем catch
-clause, а не Exception
. Подобные конструкции предназначены исключительно для точек входа компонентов и никогда не должны делать предположений о конкретной причине или характере исключения. Таким образом, такой код никогда не будет содержать определенную логику обработки ошибок. Вместо этого они представляют собой универсальное средство для регистрации или сообщения об ошибке или для предоставления конечному пользователю обратной связи о том, что что-то пошло не так.
Иногда подобное универсальное средство также используется для очистки ресурсов или закрытия открытых соединений. Блок finally
часто лучше в таких ситуациях, особенно если исключение выдается повторно в конце блока catch
.
Особый случай: отладочная информация
Одной из веских причин для перехвата LogicException
в любом случае является добавление к нему дополнительной отладочной информации, которая не была доступна глубже в стеке вызовов. В таком случае мы сразу бросаем новую LogicException
с дополнительными данными и исходным исключением в $previous
. Мы предпочтительно перехватываем такое исключение на основе наиболее универсального возможного типа. Это может быть общий базовый класс или маркерный интерфейс для всех LogicException
, которые могут встречаться в данном фрагменте кода.
Альтернативой такому catch
может быть передача отладочной информации вызываемым объектам, которые в конечном итоге вызывают ошибку. Мы предпочитаем не делать этого, если такая информация используется только для передачи ее в исключение.
Генерация нового исключения после перехвата
После перехвата исключения, конечно, можно создать новое исключение. Действительно, для разумной обработки исключений это необходимо больше, чем можно было бы ожидать. Просто всегда устанавливайте параметр $previous
нового исключения в исходное (перехваченное) исключение. Это гарантирует, что полная причина исключения все еще может быть получена. Из-за порядка параметров конструктора Exception
часто необходимо указать $код
. Мы не используем этот параметр, поэтому просто устанавливаем его равным 0
.
Преобразование на правильный уровень абстракции
Часто необходимо перехватывать и выбрасывать исключение, чтобы гарантировать, что исключение проявляется на подходящем уровне абстракции. Предположим, у нас есть UserRepositoryInterface
, который реализован DatabaseUserRepository
. Последний хранит пользователей в базе данных, что вызывает исключение DuplicateDatabaseKeyException
, если пользователь с данным именем пользователя уже существует. Согласно правилам, описанным ранее, мы должны использовать @выдает
, чтобы аннотировать это исключение в интерфейсе, но это справедливо кажется немного странным. Зачем универсальному интерфейсу, предназначенному для абстрагирования механизма хранения, знать о типе исключения, специфичном для базы данных? Решение состоит в том, чтобы поймать DuplicateDatabaseKeyException
в DatabaseUserRepository
и выбросить что-то вроде UserAlreadyExistsExeption
вместо него. Это исключение соответствует уровню абстракции UserRepositoryInterface 9.0014: он знает о пользователях, но не знает, как они сохраняются. Поэтому его можно без проблем добавить в сигнатуру этого интерфейса.
От RuntimeException до LogicException
Вполне возможно, что исключение, которое было RuntimeException
, в какой-то момент преобразуется в LogicException
. Это связано со специфическими знаниями, которые у нас есть на тот момент, из которых мы знаем, что данное исключение не должно быть возможным. Это знание не было доступно глубже в стек вызовов .
Чтобы проиллюстрировать это, предположим, что у нас есть средство чтения XML с методом getUniqueTagContents
. Этот метод считывает содержимое одного уникального тега из XML-файла на основе имени тега. Многое может пойти не так внутри такого метода: файл XML может быть искажен, данный тег может отсутствовать или он может встречаться несколько раз. Все это примеры RuntimeException
. Без дополнительных знаний о происхождении XML (который может быть загружен пользователем) и имени тега они также могут встречаться в идеально запрограммированном приложении. Но возможно, что мы используем этот метод для фрагмента XML, который мы только что проверили по схеме XML, которая обеспечивает существование и уникальность данного тега. В такой ситуации мы знаем, что getUniqueTagContents
не должен завершаться ошибкой. То же самое происходит, когда мы используем метод для чтения файла конфигурации, который мы сами помещаем в VCS и который, таким образом, полностью контролируем.
В таких ситуациях нам все равно приходится перехватывать «невозможные» исключения времени выполнения, поскольку мы считаем их проверенными . В этом блоке catch
мы затем выбрасываем LogicException
: такой ситуации никогда не должно быть. Конечно, мы сохраняем исходное исключение через $previous 9.0014 параметр.
Это часто встречающийся шаблон. Глубоко в стеке вызовов , где общая картина недоступна, многие ошибки представляют собой RuntimeException
. Когда исключение всплывает (независимо от того, переведено ли оно на другой уровень абстракции или нет), оно достигает точки, в которой мы знаем, что ошибка должна быть невозможна. В этот момент он становится LogicException
. Обратите внимание, что обратное невозможно: неожиданной ошибки ожидать нельзя.
Серая область
Различие между RuntimeException
и LogicException
не всегда ясно на 100%. Существует серая область, где правильный тип исключения зависит от интерпретации и семантического контракта функции. Несколько примеров, иллюстрирующих это:
Синтаксическая ошибка в запросе
Метод executeQuery
для выполнения запроса к базе данных может завершиться ошибкой из-за синтаксической ошибки в этом запросе. На первый взгляд это выглядит как RuntimeException 9.0014 : мы не знаем, откуда пришел запрос, и поэтому не можем гарантировать его синтаксическую корректность. С другой стороны, было бы очень странно, если бы пользовательский ввод (или ввод из другого источника, не зависящего от нас) мог привести к синтаксической ошибке. Пахнет SQL инъекцией. Поэтому очень разумно заявить, что код, вызывающий
executeQuery
, отвечает за синтаксическую правильность запроса. Это делает исключение LogicException
. Исключение было бы, если бы мы создавали приложение, такое как adminer или phpMyAdmin, где мы должны ожидать ошибок в вводимых пользователем запросах SQL.
Элемент кэша не найден
Предположим, у нас есть класс кэша с методом get($key)
для извлечения элемента кэша. Конечно, может случиться, что get
вызывается с несуществующим ключом. Мы предполагаем, что решили сообщить об этом через исключение (альтернативно можно использовать возвращаемое значение или параметр по ссылке). Будет ли такое исключение временем выполнения или логическим исключением?
Ответ, вероятно, зависит от других методов класса кеша и от того, как мы предполагаем их использовать. Если в кеше имеет метод
, мы можем потребовать, чтобы потребитель использовал этот метод для проверки существования элемента кэша перед его извлечением с помощью get
. В этом случае LogicException
разумно. В зависимости от реализации кеша может быть небольшая вероятность того, что элемент кеша будет удален между вызовами has
и get
. В таких случаях даже идеальная реализация (которая сначала проверяет существование) не может полностью предотвратить ошибку. «Правильная» категория исключений здесь не очень ясна. На данный момент мы склонны использовать LogicException
для ситуаций с такими редкими крайними случаями, как этот.
Поврежденные данные в базе данных
Еще одна серая зона образована ошибками, которые могут возникнуть только в случае поврежденной базы данных. С пуристической точки зрения это пример RuntimeException
, база данных является внешним фактором. С другой стороны, нежелательно учитывать возможность повреждения базы данных во всем коде. Это тем более применимо, если в базу данных пишет только приложение, а иногда и разработчик или системный администратор. Более прагматичный подход состоит в том, чтобы рассматривать повреждение базы данных как LogicException
. Скорее всего, поврежденные данные были вызваны ошибкой реализации в приложении. В любом случае для исправления данных вручную требуется вмешательство разработчика или системного администратора.
Резюме
Мы отличаем проверенные исключения, которые представляют собой «неисправимые» ситуации, от непроверенных исключений, которые представляют ошибки программирования. Поэтому мы знаем в каждой точке кода, какие исключения следует ожидать и, таким образом, обрабатывать. Перехватывая и повторно вызывая исключения, мы удостоверяемся, что они находятся на надлежащем уровне абстракции и, таким образом, не нарушают инкапсуляцию. Цепочка исходного исключения с использованием Параметр $previous
предотвращает потерю отладочной информации.
Вы применяете эти методы в своем проекте? Дайте нам знать, работают ли они для вас, есть ли препятствия, с которыми вы сталкиваетесь, или если у вас есть улучшения в этих рекомендациях. Мы будем рады получить ваши отзывы!
Блог Дил дезе
Почему следует использовать исключения SPL в PHP для лучшей обработки исключений — веб-разработчики и т.
д.Почему следует использовать исключения SPL в PHP для лучшей обработки исключений Содержание
- Каковы классы исключений SPL?
- Почему следует использовать исключения SPL?
- Как выбрать соответствующее исключение
- 13 исключений SPL - и когда их использовать!
- logicexception
- BadFunctionCallexception
- BadMethodCallexception
- DomainException
- InvalidargumentException
- LengthException
- Outfboundsexception
- LengthException
- Outfboundsexception
- 99.ceptimeemeexexcept0024
- OverflowException
- UnderflowException
- RangeException
- UnexpectedValueException
Начиная с PHP 5.1.0, у нас есть доступ к исключениям SPL.
До того, как они появились, большинство людей просто использовали старый добрый throw new Exception(...)
или создали свой собственный класс Exception, который расширил базовый класс Exception.
Но теперь у нас есть возможность использовать (или расширять) определенные типы исключений.
И вы должны всегда использовать исключения SPL (или расширять их с помощью подкласса) и никогда больше не использовать старый добрый /Exception
!
Каковы классы исключений SPL?
Существует около дюжины классов исключений SPL, которые в конечном итоге расширяют базовый класс \Exception
.
Сами по себе они ничего особенного не делают, но могут использоваться для сообщения и обработки определенных типов ошибок.
Они используются точно так же — просто используйте и создайте новое исключение OverflowException
.
Почему следует использовать исключения SPL?
Они предоставляют более подробные отчеты об ошибках. Плохая практика иметь сотни строк кода, выдающих только Exception
. В идеале у вас должны быть свои собственные исключения (которые должны расширять исключения SPL), но нецелесообразно делать это каждый раз, когда вы можете создать исключение.
13 исключений SPL охватывают широкий спектр распространенных ситуаций, с которыми вы столкнетесь, например недопустимые аргументы (InvalidArgumentException) или переполнения (OverflowException).
Как выбрать подходящее исключение
Иногда может быть совершенно очевидно, какое исключение выдать (или расширить с помощью подкласса), однако в некоторых случаях это неочевидно или когда оно может соответствовать двум или более классам исключений. .
Если неясно, какой из них выбрать, вы всегда можете выбрать родительский класс исключений. Существует два «основных» класса:
- LogicException , который включает следующие подклассы:
- BadFunctionCallException
- BadMethodCallException
- Исключение домена
- ИнвалидАргументИсключение
- Длина исключения
- OutOfRangeException
- BadFunctionCallException
- RuntimeException , который включает следующие подклассы:
- OutOfBoundsException
- Исключение переполнения
- RangeException
- UnderflowException
- UnexpectedValueException
Таким образом, вы всегда можете просто выбрать соответствующее родительское исключение (но больше не используйте базовый класс \Exception!)
Большинство из них являются вариантами RuntimeException.
13 исключений SPL -
и когда их использовать!LogicException
Это родительский класс примерно для половины исключений SPL (RuntimeException является родительским классом для остальных).
Довольно часто используется (хотя во многих случаях следует использовать один из его подклассов). Используйте его, когда есть ошибка в логике программы.
BadFunctionCallException
По моему опыту, это исключение редко используется. Вы должны выбросить его, если обратный вызов относится к неопределенной функции или если отсутствуют некоторые аргументы.
BadMethodCallException
Это подкласс BadFunctionCallException
Это похоже на BadFunctionCallException, но для методов. Его следует использовать, когда метод класса не существует или имеет неправильные параметры (т.е. отсутствующие параметры).
Вы часто обнаружите, что BadFunctionCallException вызывается в методе класса __call($method, $params)
, если self::$method() не существует.
DomainException
Это исключение следует использовать, если значение не соответствует определенной допустимой области данных.
Часто используется для «проверки работоспособности». Например, если вы пытаетесь имитировать бросок шестигранной кости, то, если значение > 6, вы можете сгенерировать исключение DomainException (поскольку значения всегда должны быть от 1 до 6). Он довольно часто используется в классах базы данных Laravel, если вы пытаетесь выполнить какое-либо действие, которое драйвер базы данных не поддерживает (например, выполнение doAdvisoryLock() вызовет исключение DomainException, если ваше подключение к базе данных SQLite, поскольку SQLite не поддержка рекомендательных замков
InvalidArgumentException
Это еще одно часто используемое исключение. Вы видите это, когда аргумент (например, метод или параметр функции) недействителен.
Например, если вы ожидаете, что $input
будет массивом, вы можете сгенерировать InvalidArgumentException, если is_array($input) != true
.
LengthException
Это исключение предназначено для создания, если длина недействительна. Например, если длина строки слишком велика или слишком мала.
Например, если вы имеете дело со штрих-кодами ISBN-10 (длина которых всегда составляет 9 символов), если strlen($input) != 9
, вы должны создать исключение LengthException.
OutOfBoundsException
Если значение не является допустимым ключом, следует создать исключение OutOfBoundsException.
Обычно используется при попытке доступа к несуществующим элементам массива или при слишком большом смещении.
Используется только для значения ключа, не являющегося индексом (например, $array['некоторый-ключ-который-не-существует']
.)
Если вы имеете дело с целочисленным индексом, выходящим за пределы, вам следует использовать следующее исключение: OutOfRangeException.
RuntimeException
Это родительский класс примерно для половины исключений SPL, и его следует вызывать, если возникает ошибка, которую можно обнаружить только во время выполнения.
OutOfRangeException
Это похоже на OutOfBoundsException, но должно использоваться для недопустимых индексов массива (целых).
OverflowException
Если у вас есть класс-контейнер, и вы пытаетесь добавить в него элемент, но он заполнен, вам следует создать исключение OverflowException. Их также можно использовать в ситуациях, когда у вас есть максимальное количество повторных попыток, и после $i > $maxRetries
было бы неплохо создать исключение OverflowException.
UnderflowException
Это противоположно OverflowException и используется при удалении элементов из пустого контейнера.
Если у вас есть класс-контейнер с нулевыми элементами в нем, и ваш код пытается удалить элемент, вы должны создать исключение UnderflowException.
RangeException
Используется, когда значение не находится в определенном диапазоне значений.
Похож на DomainException, но этот класс расширяет RuntimeException.
UnexpectedValueException
Вы должны сгенерировать исключение UnexpectedValueException, если значение не совпадает с набором значений.