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