Задача публикации: доступно изложить способ организации иерархии исключений и их обработки в приложении. Без привязки к фреймворкам и конкретной архитектуре. Описываемый способ является де-факто стандартом в сообществе: он используется во многих серьёзных библиотеках и фреймворках. В том числе 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: Исключения. Где я их использую
Основные аспекты иерархии исключений вашего приложения.
Общий интерфейс
Используйте общий интерфейс (маркер) для всех исключений определяемых в вашем приложении. Это же правило применимо и к отдельным компонентам, модулям, пакетам, т.е. подпространствам имён вашего кода. Например \YourVendor\YourApp\Exception\ExceptionInterface, либо \YourVendor\YourApp\SomeComponent\Exception\ExceptionInterface, в соответствии с PSR-4.
Пример можно посмотреть в любом компоненте Symfony, Zend, etc.
Для каждой ситуации — свой тип
Каждая новая исключительная ситуация должна приводить к созданию нового типа (класса) исключения. Имя класса должно семантично описывать эту ситуацию. Таким образом код с вызовом конструктора исключения, и его броском, будет: читаем, самодокументирован, прозрачен.
Симметрично и код с отловом исключения, будет явным. Код внутри catch не должен пытаться определить ситуацию по сообщению или друг��м косвенным признакам.
Расширяйте базовые типы
Базовые типы служат для наследования от них собственных типов исключений вашего приложения. Об этом прямо сказано в документации.
Пример можно посмотреть в любом компоненте Symfony, либо Zend.
Проброс и преобразование в соответствии с уровнем абстракции
Поскольку исключения всплывают по всему стеку вызовов, в приложении может быть несколько мест где они отлавливаются, пропускаются, или преобразуются. В качестве простого примера: стандартное PDOException логично поймать в слое DAL, либо в ORM, и пробросить дальше вместо него собственный DataBaseException, который в свою очередь уже ловить в слое выше, например, контроллере, где преобразовать в HttpException. Последний может быть перехвачен в коде диспетчера, на самом верхнем уровне.
Таким образом, контроллер не знает о существовании PDO — он работает с абстрактным хранилищем.
Решение, что должно быть проброшено, что преобразовано, а что обработано, в каждом случае лежит на программисте.
Используете возможности стандартного конструктора
Не стоит переопределять стандартный конструктор Exception — он идеально спроектирован для своих задач. Просто не забывайте его использовать по назначению и вызывать родительский со всеми аргументами, если всё же потребовалась перегрузка. Суммируя этот пункт с предыдущим, код может выглядеть примерно так:
namespace Samizdam\HabrahabrExceptionsTutorial\DAL; 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 Samizdam\HabrahabrExceptionsTutorial\SomeModule\Controller; use Samizdam\HabrahabrExceptionsTutorial\Http\Exception; 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 Samizdam\HabrahabrExceptionsTutorial\Http; 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.
В бета-тестировании, вы вероятно захотите выводить трейсы всех предыдущих исключений для формирования баг-репортов.
К началу экплуатации вам понадобится единая точка для логирования исключительных ситуаций.
И всё время развития приложения вы будете всего лишь, по мере надобности, добавлять код с логикой обработки, без необходимости внесения изменений в основной код, где исключения генерируются.

