Организация исключений

10.06.2022

За свою практику у меня сформировалось несколько правил формирования исключений для проекта в контексте WEB API. Это не новшество и некоторые из них уже существовали (вот и вот) или были сформированы, но резюмирую ключевые моменты.

Отделение встроенных исключений от новых

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

•••
php
namespace MyProject\Exceptions; class BaseException extends \Exception { }

Теперь мы можем создать свое исключение InvalidArgumentException и выбрасывать его в случае когда самостоятельно производим валидацию переданных аргументов:

•••
php
namespace MyProject\Exceptions\Config; class InvalidConfigException extends BaseException { }

При этом четко отделяется встроенное исключение \InvalidArgumentException от MyProject\Exceptions\InvalidArgumentException:

•••
php
use MyProject\Exceptions\InvalidArgumentException; try { $model = $db->dispense('mymodel', $_POST)); } catch (InvalidArgumentException $e) { ... } catch (\InvalidArgumentException $e) { ... }

ВСЕГДА перехватывать исключения

Код ниже ниже производит перехват и дальнейший проброс исключения:

•••
php
try { $model = $db->load('mymodel', $id)) } catch (DataBaseException $e) { throw $e; }
Бесполезный код, но это только с точки зрения автора кода, который держит весь контекст в своей памяти.

Что будет по прошествии времени или когда новый разработчик увидит код выше? Определенно будет понятно что операция заключенная в try/catch может выбросить исключение.

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

•••
php
try { $model = $db->load('mymodel', $id)) } catch (DataBaseException $e) { throw new InternalServerErrorException($e->getMessage(), $e); }

В этом случае всякий код подверженный исключениям будет обернут в try/catch и обновить логику не составит труда, по крайней мере в выявлении такового кода.

Соответствие слою асбтракции

Рассмотрим код работающий на уровне слоя данных с использованием PDO:

•••
php
$stmt = $pdo->prepare('INSERT ...'); try { $stmt->execute(); } catch (\PDOException $e) { if ($e->getCode() == 23000) { // такая запись в БД уже существует throw new DuplicateEntryException($e->getMessage(), $e->getCode(), $e); } elseif ($e->getCode() == 42000) { // ошибка синтаксиса SQL throw new SQLException($e->getMessage(), $e->getCode(), $e); } // ... }

Пользователь этого кода, в случае ошибки получит конкретное исключение не зависящее от внутренней реализации, и будет основываться на интерфейсе.

А теперь посмотрим на пользовательский код использующий интерфейс слоя данных из примера выше:

•••
php
try { $model = $db->dispense('mymodel', $data); $db->save($model); } catch (DuplicateEntryException $e) { // такие данные уже записаны, выбрасываем исключение в контексте http (409) throw new ConflictException($e->getMessage()); } catch (SQLException | ErrorClassDataModelException $e) { // ошибка реализации выбрасываем исключение в контексте http (500) throw new InternalServerErrorException($e->getMessage(), $e); }

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

Файловая структура

В своей практике я вывел такую файловую структуру исключений:

Файловая структура исключений
Файловая структура исключений

Из этой структур видно:

  • каждый контекст возможных исключений помещен в свою директорию
  • каждый контекст имеет свой базовый класс исключений, который наследуется от базового исключения для проекта в целом (первый наследник Exception)
  • каждый класс исключения имеет постфикс Exception