Задача публикации: доступно изложить способ организации иерархии исключений и их обработки в приложении. Без привязки к фреймворкам и конкретной архитектуре. Описываемый способ является де-факто стандартом в сообществе: он используется во многих серьёзных библиотеках и фреймворках. В том числе Zend, Symfony. Не смотря на его логичность и универсальность, формального описания предлагаемого подхода на русском языке я не нашёл. После неоднократного устного изложения концепции коллегам, родилась мысль оформить её в виде публикации на Хабрахабр.
В языке PHP, начиная с 5-ой версии, доступен механизм исключений. В актуальной, 7-ой, версии этот механизм был улучшен и переработан с целью единнобразной обработки разных ошибок при помощи конструкции try{} catch...
В стандартной библиотеке (SPL) PHP предоставляет готовый набор базовых классов и интерфейсов для исключений. В 7-ой версии этот набор был расширен интерфейсом Throwable
. Вот диаграмма всех имеющихся в версии 7 типов (изображение — ссылка):
Для junior-разработчиков, может быть полезным предварительно уяснить все тонкости синтаксиса, логики работы исключений и обработки ошибок в целом. Могу порекоммендовать следующие статьи на русском языке:
- Евгений Пястолов: Стандартные исключения в PHP. Когда какое применить.
- Антон Шевчук (старожил Хабра a.k.a. AntonShevchuk) (перевод Exceptional Code – PART 1): “Исключительный” код – Часть 1
- @kotiara:
- @expolit: Исключения. Где я их использую
Основные аспекты иерархии исключений вашего приложения.
Общий интерфейс
Используйте общий интерфейс (маркер) для всех исключений определяемых в вашем приложении. Это же правило применимо и к отдельным компонентам, модулям, пакетам, т.е. подпространствам имён вашего кода. Например YourVendorYourAppExceptionExceptionInterface
, либо YourVendorYourAppSomeComponentExceptionExceptionInterface
, в соответствии с PSR-4.
Пример можно посмотреть в любом компоненте Symfony, Zend, etc.
Для каждой ситуации — свой тип
Каждая новая исключительная ситуация должна приводить к созданию нового типа (класса) исключения. Имя класса должно семантично описывать эту ситуацию. Таким образом код с вызовом конструктора исключения, и его броском, будет: читаем, самодокументирован, прозрачен.
Симметрично и код с отловом исключения, будет явным. Код внутри catch
не должен пытаться определить ситуацию по сообщению или другим косвенным признакам.
Расширяйте базовые типы
Базовые типы служат для наследования от них собственных типов исключений вашего приложения. Об этом прямо сказано в документации.
Пример можно посмотреть в любом компоненте Symfony, либо Zend.
Проброс и преобразование в соответствии с уровнем абстракции
Поскольку исключения всплывают по всему стеку вызовов, в приложении может быть несколько мест где они отлавливаются, пропускаются, или преобразуются. В качестве простого примера: стандартное PDOException логично поймать в слое DAL, либо в ORM, и пробросить дальше вместо него собственный DataBaseException
, который в свою очередь уже ловить в слое выше, например, контроллере, где преобразовать в HttpException. Последний может быть перехвачен в коде диспетчера, на самом верхнем уровне.
Таким образом, контроллер не знает о существовании PDO — он работает с абстрактным хранилищем.
Решение, что должно быть проброшено, что преобразовано, а что обработано, в каждом случае лежит на программисте.
Используете возможности стандартного конструктора
Не стоит переопределять стандартный конструктор Exception
— он идеально спроектирован для своих задач. Просто не забывайте его использовать по назначению и вызывать родительский со всеми аргументами, если всё же потребовалась перегрузка. Суммируя этот пункт с предыдущим, код может выглядеть примерно так:
namespace SamizdamHabrahabrExceptionsTutorialDAL;
class SomeRepository
{
public function save(Model $model)
{
// .....
try {
$this->connection->query($data);
} catch (PDOException $e) {
throw new DataBaseException(i18n('Error on sql query execution: ' . $e->getMessage(), $e->getCode()), $e);
}
}
}
// .....
namespace SamizdamHabrahabrExceptionsTutorialSomeModuleController;
use SamizdamHabrahabrExceptionsTutorialHttpException;
class SomeController
{
public function saveAction()
{
// .....
try {
$this->repository->save($model);
} catch (DataBaseException $e) {
throw new HttpException(i18n('Database error. '), HttpException::INTERNAL_SERVER_ERROR, $e);
}
}
}
// .....
namespace SamizdamHabrahabrExceptionsTutorialHttp;
class Dispatcher
{
public function processRequest()
{
// .....
try {
$controller->{$action}();
} catch (HttpException $e) {
// упрощенно для примера
http_response_code($e->getCode());
echo $e->getMessage();
}
}
}
Резюме
Зачем построение целой иерархии, с участием интерфейсов, типов, подтипов и весь этот полиморфизм ради обработки ошибок? Каков смысл этой абстракции и чем оправдана её цена?
При проектировании приложения, абстракция — это то, что добавляет гибкости, и позволяет откладывать конкретные решения и детальную реализацию, пока не известны все нюансы, а требования к коду ещё не известны, либо могут поменяться. Это инвестиция, которая окупается со временем.
Когда ваши исключения обладают одновременно интерфейсом-маркером, супертипом SPL (как RuntimeException), конкретным типом соответствующим ситуации, вы можете полностью контролировать их обработку и гибко менять её стратегию в будущем. Закладывая на раннем этапе разработки эти абстракции, в будущем, по мере появления и ужесточения требований к обработке ошибок, вы будете иметь в распоряжении инструмент, который поможет эти требования реализовать.
На этапе прототипа достаточно показать надпись "опаньки", для этого достаточно поймать любой Throwable в index.php.
В альфа версии будет не лишним отличать ситуации 401, 404 и 500.
В бета-тестировании, вы вероятно захотите выводить трейсы всех предыдущих исключений для формирования баг-репортов.
К началу экплуатации вам понадобится единая точка для логирования исключительных ситуаций.
И всё время развития приложения вы будете всего лишь, по мере надобности, добавлять код с логикой обработки, без необходимости внесения изменений в основной код, где исключения генерируются.
Автор: samizdam