Exceptions в PHP
Table of Contents
Когда вы впервые начинаете работать с исключениями, может возникнуть сильное желание генерировать их каждый раз, когда вы обнаруживаете ошибку. Постарайтесь удержаться от этого. Вместо этого думайте об исключениях как об ошибках, которые невозможно предвидеть. Другими словами, если ошибку можно предотвратить, она не должна быть исключением.
https://www.honeybadger.io/blog/php-exceptions
Пример
// Какая-то функция кидает Exception
public function doSomethingVeryUsefulOrThrowException()
{
if ($this->weirdConditionHappened()) {
throw new Exception("Some weird condition happened");
}
$this->doSomethingVeryUseful();
}
// Потом в try / catch можем словить ее
try {
$helperObject->doSomethingVeryUsefulOrThrowException();
} catch (Exception $exception) {
$this->handleException($exception);
}
Когда использовать
try-catch ловит только исключения (Exception или Throwable). Если внутри блока try нет кода, который может их выбросить, то catch никогда не выполнится. Он просто висит как декоративная конструкция.
try {
$a = 5 + 3;
} catch (Exception $e) {
echo $e->getMessage();
}
Здесь catch никогда не сработает. Сложить два числа не может выбросить исключение. PHP даже не рассматривает такую ситуацию.
try-catch появляется только когда есть источник исключения. Обычно это:
- Функция или метод, который явно делает
throw - Библиотека, которая кидает исключения
- Код, где ты сам проверяешь условия и бросаешь
throw new Exception - Работа с файлами, БД, API, парсингом, JSON и т.д.
Если ты видишь try-catch в коде, это почти всегда означает одно из трёх:
- граница системы (API, файл, БД)
- транзакция
- место где ошибка превращается в пользовательский ответ
Базовый механизм:
try {
// код который может упасть
throw new Exception("что-то пошло не так");
} catch (Exception $e) {
// обработка ошибки
echo $e->getMessage(); // "что-то пошло не так"
echo $e->getCode(); // 0
echo $e->getFile(); // путь к файлу
echo $e->getLine(); // номер строки
} finally {
// выполнится ВСЕГДА — и после try, и после catch
// используется для очистки ресурсов
}
Где ловить исключение?
Представь 5 уровней вызова:
Controller
Service
Repository
DatabaseClient
PDO
Где ловить исключение?
Правильный ответ почти всегда – в Controller, а не на каждом уровне.
Поэтому хороший код часто выглядит так:
public function create()
{
try {
$this->service->createUser($data);
} catch (Throwable $e) {
return response('Error', 500);
}
}
А внутри service и repository никаких try-catch вообще нет.
в PHP есть не только Exception, но и Error
И ещё один тонкий момент PHP, который часто удивляет людей, изучающих ООП:
в PHP есть не только Exception, но и Error, и оба они наследуются от Throwable. Поэтому современный код обычно ловит именно:
catch (Throwable $e)
а не только Exception.
Кастомные исключения:
Представим простую систему: есть пользователи, они лежат в базе, и мы хотим их получить по id.
Начнем с исключения. Сначала создаётся свой тип ошибки.
class UserNotFoundException extends Exception
{
}
Теперь репозиторий. Его задача – сходить в хранилище и вернуть пользователя. Если пользователя нет, он бросает исключение.
class UserRepository
{
private array $users = [
1 => ['id' => 1, 'name' => 'John'],
2 => ['id' => 2, 'name' => 'Kate'],
];
public function find(int $id): array
{
if (!isset($this->users[$id])) {
throw new UserNotFoundException("User with id {$id} not found");
}
return $this->users[$id];
}
}
Вот ключевой момент всей конструкции:
throw new UserNotFoundException(...)
Без этого try-catch вообще не имеет смысла.
Теперь код, который использует репозиторий.
class UserService
{
private UserRepository $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function getUserOrNull(int $id): ?array
{
try {
$user = $this->repository->find($id);
return $user;
} catch (UserNotFoundException $e) {
return null;
}
}
}
Не пойманное исключение → fatal error → краш.
Здесь происходит интересная вещь.
Мы переводим исключение в обычное значение.
То есть:
Exception -> null
Зачем создавать свой тип ошибки?
Коротко: свой тип ошибки нужен не ради красоты. Он нужен, чтобы точно понимать, что произошло, и уметь ловить именно эту ситуацию, а не все подряд.
Представь простой код.
throw new Exception("User not found");
Теперь где-то выше:
try {
$user = $repository->find($id);
} catch (Exception $e) {
echo "Ошибка";
}
Проблема тут в том, что Exception – это слишком общий тип.
В этот catch попадёт всё:
- пользователь не найден
- ошибка базы
- ошибка сети
- баг в коде
- любая другая Exception
Ты теряешь контекст.
Теперь тот же код, но со своим исключением.
class UserNotFoundException extends Exception {}
В репозитории:
if (!$user) {
throw new UserNotFoundException("User not found");
}
А ловим так:
try {
$user = $repository->find($id);
} catch (UserNotFoundException $e) {
echo "Пользователь не найден";
}
Теперь происходит магия архитектуры.
Ты обрабатываешь конкретную ситуацию.
Например:
try {
$user = $repository->find($id);
} catch (UserNotFoundException $e) {
return response("User not found", 404);
} catch (DatabaseException $e) {
return response("Database error", 500);
}
Одна ошибка → 404
Другая → 500
Это уже поведение приложения.
Есть ещё одна причина, которую новички часто не замечают. Свои исключения делают код самодокументируемым.
Сравни два варианта.
throw new Exception("Payment failed");
и
throw new PaymentDeclinedException();
Во втором случае даже без комментариев ясно, что произошло.
Когда не стоит создавать свой Exception.
Если ошибка:
- одноразовая
- не будет отдельно обрабатываться
- не важна для логики приложения
то обычного Exception достаточно.
Но если ошибка:
- часть бизнес-логики
- должна обрабатываться отдельно
- важна для API или UI
тогда свой Exception – правильный инструмент.
Иногда исключение не ловят вообще, а дают ему подняться вверх.
public function getUser(int $id): array
{
return $this->repository->find($id);
}
И ловят уже в контроллере:
try {
$user = $service->getUser(3);
} catch (UserNotFoundException $e) {
echo "404 user not found";
}
Это даже более распространенный вариант.
Есть любопытная философия проектирования:
исключения – это не ошибки программы, а альтернативный поток управления.
То есть фактически это скрытый goto, только структурированный.
Программа говорит:
“Если пользователя нет, прыгни сразу туда, где знают что делать”.
И в больших системах это спасает от километров if.
Представь систему с 6 слоями:
Controller
Service
UserManager
Repository
Database
PDO
Если не использовать исключения, код будет выглядеть так:
if (!$user) return false
if (!$user) return false
if (!$user) return false
лес из if.
А исключение просто пробивает все уровни вверх как пузырек воздуха в воде. Это одна из причин, почему они появились в языках вроде Java и потом перекочевали в PHP.
Exception попадает в ближайший catch
PHP идёт вверх по стеку вызовов и ищет первый catch, который подходит по типу:
function c() {
throw new SprmBlocksException("ошибка"); // 1. бросили тут
}
function b() {
c(); // 2. тут нет catch — идём выше
}
function a() {
try {
b(); // 3. нашли try/catch — ловим тут
} catch (SprmBlocksException $e) {
error_log($e->getMessage());
}
}
Если catch ловит другой тип — пропустит и пойдёт дальше вверх:
try {
try {
throw new SprmBlocksException("ошибка");
} catch (InvalidArgumentException $e) {
// не подходит по типу — пропускаем
}
} catch (SprmBlocksException $e) {
// поймали тут
}
Если нигде не поймали — fatal error и краш.
В catch должны указать какой конткретный exception ловим?
Не обязательно конкретный. Есть три варианта:
1. Конкретный тип — ловишь только его и его потомков:
catch (InvalidArgumentException $e)
// поймает InvalidArgumentException и всё что от него наследует
// пропустит RuntimeException, LogicException и т.д.
2. Общий Exception — ловишь вообще всё:
catch (Exception $e)
// поймает любое исключение
3. Несколько типов через | (PHP 8+):
catch (InvalidArgumentException | RuntimeException $e)
// поймает оба типа, но не другие
Какой выбирать? Зависит от того, что ты хочешь сделать:
// Знаешь конкретную ошибку — лови конкретно
try {
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
error_log("Bad JSON: " . $e->getMessage());
}
// Не знаешь что может упасть — лови всё
try {
$block->render();
} catch (Exception $e) {
error_log("SPRM Blocks: " . $e->getMessage());
}
Правило: лови максимально конкретный тип, который можешь осмысленно обработать. catch (Exception $e) — это как «поймать всё» — работает, но ты можешь случайно проглотить ошибку, о которой стоило бы узнать.
Когда делаем throw new Exception, что увидит пользователь?
Если исключение никто не поймал (try/catch нет) — PHP покажет fatal error. Что именно увидит пользователь, зависит от настройки WP_DEBUG:
WP_DEBUG = true (разработка):
Fatal error: Uncaught SprmBlocksException: Template not found: /var/www/.../affiliate-models.php
in /var/www/.../src/Block.php on line 48
Stack trace:
#0 /var/www/.../src/blocks/AffiliateModels.php(31): Block::renderBlock()
#1 ...
WP_DEBUG = false (продакшен):
Белый экран. Пустая страница. Пользователь не понимает что произошло.
Исключение имеет смысл только если ты его ловишь:
// Бросаешь
throw new SprmBlocksException("Template not found: {$template}");
// Ловишь — где-то выше по стеку
try {
$block->render();
} catch (SprmBlocksException $e) {
error_log('SPRM Blocks: ' . $e->getMessage());
// пользователь видит пустоту, не краш
}
Exceptions в WordPress
WordPress почти не использует исключения. Вместо этого — паттерн WP_Error:
// WordPress-способ
function wp_get_something($id) {
if (!$id) {
return new WP_Error('invalid_id', 'ID is required');
}
return $data;
}
// Проверка
$result = wp_get_something(0);
if (is_wp_error($result)) {
echo $result->get_error_message(); // "ID is required"
echo $result->get_error_code(); // "invalid_id"
}
Почему WP не использует exceptions:
- WordPress старше, чем
try/catchв PHP (WP — 2003, exceptions — PHP 5, 2004) - Обратная совместимость — нельзя сломать тысячи плагинов
- Философия: ошибка не должна ронять весь сайт
Что использовать в WP-плагине:
| Ситуация | Подход |
|---|---|
| Критическая зависимость | admin_notices + return |
| Файл не найден | error_log() + return |
| AJAX-ошибка | wp_send_json_error() |
| REST API ошибка | return new WP_Error(...) |
| Ошибка в данных | WP_Error |
Exceptions в Laravel
Laravel строится на исключениях. Каждая ошибка — exception.
Как работает:
Исключение бросается где угодно
↓
Летит вверх по стеку
↓
Если try/catch — обрабатывается на месте
↓
Если нет — попадает в глобальный Exception Handler
↓
Handler решает: логировать, показать страницу ошибки, вернуть JSON
Глобальный обработчик (app/Exceptions/Handler.php):
class Handler extends ExceptionHandler
{
// Не логировать эти
protected $dontReport = [
ValidationException::class,
];
// Кастомный рендер
public function render($request, Throwable $e)
{
if ($e instanceof PaymentException) {
return response()->json(['error' => $e->getMessage()], 402);
}
return parent::render($request, $e); // стандартная обработка
}
}
Встроенные исключения Laravel:
// 404 — автоматически
$user = User::findOrFail($id); // не нашёл → ModelNotFoundException → 404
// 403
abort(403, 'Нет доступа');
// 422 — валидация (автоматически)
$request->validate(['email' => 'required|email']);
// не прошло → ValidationException → JSON с ошибками
// Свои
throw new PaymentException('Платёж не прошёл');
Сравнение
| PHP | WordPress | Laravel | |
|---|---|---|---|
| Механизм | throw/catch | WP_Error + is_wp_error() | throw/catch + Handler |
| Глобальный обработчик | Нет (fatal error) | Нет | Да (Handler) |
| Не пойманное исключение | Краш | Краш | Красивая страница ошибки |
| Философия | Сам решай | Сайт не должен падать | Fail fast, обработай централизованно |