Задача публикации: доступно изложить способ организации иерархии исключений и их обработки в приложении. Без привязки к фреймворкам и конкретной архитектуре. Описываемый способ является де-факто стандартом в сообществе: он используется во многих серьёзных библиотеках и фреймворках. В том числе Zend, Symfony. Не смотря на его логичность и универсальность, формального описания предлагаемого подхода на русском языке я не нашёл. После неоднократного устного изложения концепции коллегам, родилась мысль оформить её в виде публикации на Хабрахабр.


В языке PHP, начиная с 5-ой версии, доступен механизм исключений. В актуальной, 7-ой, версии этот механизм был улучшен и переработан с целью единнобразной обработки разных ошибок при помощи конструкции try{} catch...


В стандартной библиотеке (SPL) PHP предоставляет готовый набор базовых классов и интерфейсов для исключений. В 7-ой версии этот набор был расширен интерфейсом Throwable. Вот диаграмма всех имеющихся в версии 7 типов (изображение — ссылка):


Диаграмма типов исключения в PHP7


Для junior-разработчиков, может быть полезным предварительно уяснить все тонкости синтаксиса, логики работы исключений и обработки ошибок в целом. Могу порекоммендовать следующие статьи на русском языке:



Основные аспекты иерархии исключений вашего приложения.


Общий интерфейс


Используйте общий интерфейс (маркер) для всех исключений определяемых в вашем приложении. Это же правило применимо и к отдельным компонентам, модулям, пакетам, т.е. подпространствам имён вашего кода. Например \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.


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


К началу экплуатации вам понадобится единая точка для логирования исключительных ситуаций.


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