Содержание
За свою практику у меня сформировалось несколько правил формирования исключений для проекта в контексте WEB API. Это не новшество и некоторые из них уже существовали (вот и вот) или были сформированы, но резюмирую ключевые моменты.
Создадим свой базовый класс исключений, для того чтобы отделять свои исключения от встроенных, так как имена могут совпадать, и обработка может значительно отличаться:
phpnamespace MyProject\Exceptions; class BaseException extends \Exception { }
Теперь мы можем создать свое исключение InvalidArgumentException
и выбрасывать его в случае когда самостоятельно производим валидацию переданных аргументов:
phpnamespace MyProject\Exceptions\Config; class InvalidConfigException extends BaseException { }
При этом четко отделяется встроенное исключение \InvalidArgumentException
от MyProject\Exceptions\InvalidArgumentException
:
phpuse MyProject\Exceptions\InvalidArgumentException; try { $model = $db->dispense('mymodel', $_POST)); } catch (InvalidArgumentException $e) { ... } catch (\InvalidArgumentException $e) { ... }
Код ниже ниже производит перехват и дальнейший проброс исключения:
Бесполезный код, но это только с точки зрения автора кода, который держит весь контекст в своей памяти.phptry { $model = $db->load('mymodel', $id)) } catch (DataBaseException $e) { throw $e; }
Что будет по прошествии времени или когда новый разработчик увидит код выше? Определенно будет понятно что операция заключенная в try
/catch
может выбросить исключение.
Возможно, в следущей версии этого кода, потребуется перехывать и выбрасывать исключение другого типа:
phptry { $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); } // ... }
Пользователь этого кода, в случае ошибки получит конкретное исключение не зависящее от внутренней реализации, и будет основываться на интерфейсе.
А теперь посмотрим на пользовательский код использующий интерфейс слоя данных из примера выше:
phptry { $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