Обробка помилок · Laravel
Вступ
Коли ви починаєте новий проєкт на Laravel, обробка помилок і винятків уже налаштована за вас; проте в будь-який момент ви можете скористатися методом withExceptions у файлі bootstrap/app.php вашого застосунку, щоб керувати тим, як застосунок звітує про винятки й рендерить їх.
Обʼєкт $exceptions, який передається в замикання withExceptions, є екземпляром Illuminate\Foundation\Configuration\Exceptions і відповідає за керування обробкою винятків у вашому застосунку. Ми розглянемо цей обʼєкт детальніше далі в цій документації.
Конфігурація
Опція debug у файлі конфігурації config/app.php визначає, скільки інформації про помилку буде показано користувачеві. За замовчуванням ця опція враховує значення змінної середовища APP_DEBUG, яка зберігається у файлі .env.
Під час локальної розробки змінну середовища APP_DEBUG варто виставити в true.
Попередження У продакшн-середовищі значення
APP_DEBUGзавжди має бутиfalse. Якщо в продакшні воно будеtrue, ви ризикуєте показати кінцевим користувачам чутливі значення конфігурації.
Обробка винятків
Звітування про винятки
У Laravel звітування про винятки використовується для логування винятків або для надсилання їх у зовнішній сервіс, як-от Laravel Nightwatch, Sentry чи Flare. За замовчуванням винятки логуються згідно з вашою конфігурацією логування. Проте ви можете логувати винятки так, як вам потрібно.
Якщо вам потрібно звітувати про різні типи винятків по-різному, скористайтеся методом винятків report у файлі bootstrap/app.php, щоб зареєструвати замикання, яке виконається, коли треба буде відзвітувати про виняток певного типу. Laravel визначає тип винятку, про який звітує замикання, за його type-hint:
use App\Exceptions\InvalidOrderException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->report(function (InvalidOrderException $e) {
// ...
});
})
Коли ви реєструєте власний колбек звітування через метод report, Laravel усе одно залогує виняток згідно зі стандартною конфігурацією логування застосунку. Якщо ви хочете припинити передачу винятку до стандартного стека логування, використайте метод stop при визначенні колбека або поверніть із колбека false:
use App\Exceptions\InvalidOrderException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->report(function (InvalidOrderException $e) {
// ...
})->stop();
$exceptions->report(function (InvalidOrderException $e) {
return false;
});
})
Примітка Щоб налаштувати звітування для конкретного винятку, можна також скористатися reportable винятками.
Глобальний контекст логу
Якщо дані доступні, Laravel автоматично додає ID поточного користувача до кожного лог-повідомлення про виняток як контекстні дані. Ви можете визначити власні глобальні контекстні дані за допомогою методу винятків context у файлі bootstrap/app.php. Ця інформація буде включена в кожне лог-повідомлення про виняток, яке пише ваш застосунок:
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->context(fn () => [
'foo' => 'bar',
]);
})
Контекст логу винятку
Додавати контекст до кожного лог-повідомлення корисно, але іноді конкретний виняток має власний контекст, який ви хотіли б бачити в логах. Визначивши метод context на одному з винятків вашого застосунку, ви можете вказати будь-які релевантні для цього винятку дані, які потрібно додати до його запису в лозі:
<?php
namespace App\Exceptions;
use Exception;
class InvalidOrderException extends Exception
{
// ...
/**
* Отримати контекстну інформацію винятку.
*
* @return array<string, mixed>
*/
public function context(): array
{
return ['order_id' => $this->orderId];
}
}
Хелпер report
Іноді потрібно відзвітувати про виняток, але продовжити обробку поточного запиту. Функція-хелпер report дозволяє швидко відзвітувати про виняток, не показуючи користувачеві сторінку помилки:
public function isValid(string $value): bool
{
try {
// Валідація значення...
} catch (Throwable $e) {
report($e);
return false;
}
}
Дедуплікація звітів про винятки
Якщо ви користуєтеся функцією report по всьому застосунку, то час від часу можете відзвітувати про той самий виняток кілька разів, створивши дублікати записів у логах.
Якщо ви хочете гарантувати, що про конкретний екземпляр винятку буде відзвітовано лише один раз, викличте метод винятків dontReportDuplicates у файлі bootstrap/app.php:
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontReportDuplicates();
})
Тепер, коли хелпер report викликається з тим самим екземпляром винятку, відзвітовано буде лише перший виклик:
$original = new RuntimeException('Whoops!');
report($original); // відзвітовано
try {
throw $original;
} catch (Throwable $caught) {
report($caught); // проігноровано
}
report($original); // проігноровано
report($caught); // проігноровано
Рівні логування винятків
Коли повідомлення записуються в логи застосунку, вони пишуться на певному рівні логування, який вказує на серйозність або важливість повідомлення.
Як зазначено вище, навіть якщо ви реєструєте власний колбек звітування через метод report, Laravel усе одно залогує виняток згідно зі стандартною конфігурацією логування застосунку; проте оскільки рівень логування іноді впливає на те, у які канали піде повідомлення, вам може знадобитися налаштувати рівень, на якому логуються певні винятки.
Для цього скористайтеся методом винятків level у файлі bootstrap/app.php. Цей метод приймає тип винятку першим аргументом, а рівень логування - другим:
use PDOException;
use Psr\Log\LogLevel;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->level(PDOException::class, LogLevel::CRITICAL);
})
Ігнорування винятків за типом
Під час розробки застосунку зʼявляються типи винятків, про які ви взагалі не хочете звітувати. Щоб ігнорувати такі винятки, скористайтеся методом винятків dontReport у файлі bootstrap/app.php. Будь-який клас, переданий цьому методу, ніколи не потрапить у звіти; проте для нього все ще може працювати власна логіка рендерингу:
use App\Exceptions\InvalidOrderException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontReport([
InvalidOrderException::class,
]);
})
Як альтернатива, ви можете просто «позначити» клас винятку інтерфейсом Illuminate\Contracts\Debug\ShouldntReport. Виняток, позначений цим інтерфейсом, ніколи не буде відзвітований обробником винятків Laravel:
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;
class PodcastProcessingException extends Exception implements ShouldntReport
{
//
}
Якщо вам потрібно ще більше контролю над тим, коли саме конкретний тип винятку ігнорується, передайте замикання в метод dontReportWhen:
use App\Exceptions\InvalidOrderException;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontReportWhen(function (Throwable $e) {
return $e instanceof PodcastProcessingException &&
$e->reason() === 'Subscription expired';
});
})
Усередині Laravel уже ігнорує деякі типи помилок за вас: винятки від HTTP-помилок 404, відповіді 403 через невідповідність origin або відповіді 419 через недійсні CSRF-токени. Якщо ви хочете сказати Laravel припинити ігнорувати певний тип винятку, скористайтеся методом винятків stopIgnoring у файлі bootstrap/app.php:
use Symfony\Component\HttpKernel\Exception\HttpException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->stopIgnoring(HttpException::class);
})
Рендеринг винятків
За замовчуванням обробник винятків Laravel перетворює винятки на HTTP-відповідь. Проте ви можете зареєструвати власне замикання рендерингу для винятків певного типу. Для цього скористайтеся методом винятків render у файлі bootstrap/app.php.
Замикання, передане в метод render, має повертати екземпляр Illuminate\Http\Response, який можна створити через хелпер response. Laravel визначає тип винятку, який рендерить замикання, за його type-hint:
use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->render(function (InvalidOrderException $e, Request $request) {
return response()->view('errors.invalid-order', status: 500);
});
})
Метод render можна також використати, щоб перевизначити поведінку рендерингу для вбудованих винятків Laravel або Symfony, як-от NotFoundHttpException. Якщо замикання, передане в render, не повертає значення, буде використано стандартний рендеринг винятків Laravel:
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
})
Рендеринг винятків як JSON
Під час рендерингу винятку Laravel автоматично визначає, чи слід віддати його як HTML, чи як JSON-відповідь, орієнтуючись на заголовок Accept запиту. Якщо ви хочете змінити те, як Laravel вирішує між HTML і JSON, скористайтеся методом shouldRenderJsonWhen:
use Illuminate\Http\Request;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
if ($request->is('admin/*')) {
return true;
}
return $request->expectsJson();
});
})
Налаштування відповіді для винятку
Зрідка вам може знадобитися змінити всю HTTP-відповідь, яку рендерить обробник винятків Laravel. Для цього зареєструйте замикання налаштування відповіді через метод respond:
use Symfony\Component\HttpFoundation\Response;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->respond(function (Response $response) {
if ($response->getStatusCode() === 419) {
return back()->with([
'message' => 'The page expired, please try again.',
]);
}
return $response;
});
})
Reportable і renderable винятки
Замість того щоб визначати власну поведінку звітування й рендерингу у файлі bootstrap/app.php, ви можете визначити методи report і render безпосередньо на винятках вашого застосунку. Якщо ці методи існують, фреймворк викличе їх автоматично:
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class InvalidOrderException extends Exception
{
/**
* Відзвітувати про виняток.
*/
public function report(): void
{
// ...
}
/**
* Відрендерити виняток як HTTP-відповідь.
*/
public function render(Request $request): Response
{
return response(/* ... */);
}
}
Якщо ваш виняток успадковує виняток, який уже є renderable, наприклад вбудований виняток Laravel або Symfony, ви можете повернути false з методу render, щоб відрендерити стандартну HTTP-відповідь цього винятку:
/**
* Відрендерити виняток як HTTP-відповідь.
*/
public function render(Request $request): Response|bool
{
if (/** Визначити, чи потрібен винятку власний рендеринг */) {
return response(/* ... */);
}
return false;
}
Якщо ваш виняток містить власну логіку звітування, потрібну лише за певних умов, вам може знадобитися сказати Laravel, що іноді про виняток треба звітувати згідно зі стандартною конфігурацією обробки винятків. Для цього поверніть false з методу report винятку:
/**
* Відзвітувати про виняток.
*/
public function report(): bool
{
if (/** Визначити, чи потрібне винятку власне звітування */) {
// ...
return true;
}
return false;
}
Примітка Ви можете вказати type-hint для будь-яких залежностей, потрібних методу
report, і Laravel автоматично впровадить їх у метод через контейнер сервісів.
Обмеження кількості звітів про винятки
Якщо ваш застосунок звітує про дуже велику кількість винятків, ви можете обмежити, скільки з них насправді логується або надсилається до зовнішнього сервісу відстеження помилок.
Щоб узяти випадкову вибірку винятків, скористайтеся методом винятків throttle у файлі bootstrap/app.php. Метод throttle приймає замикання, яке має повертати екземпляр Lottery:
use Illuminate\Support\Lottery;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
return Lottery::odds(1, 1000);
});
})
Вибірку також можна робити умовно, залежно від типу винятку. Якщо ви хочете брати вибірку лише для екземплярів певного класу винятку, повертайте екземпляр Lottery тільки для цього класу:
use App\Exceptions\ApiMonitoringException;
use Illuminate\Support\Lottery;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof ApiMonitoringException) {
return Lottery::odds(1, 1000);
}
});
})
Ви також можете обмежити частоту винятків, які логуються або надсилаються до зовнішнього сервісу відстеження помилок, повернувши екземпляр Limit замість Lottery. Це корисно, коли ви хочете захиститися від раптових сплесків винятків, що заливають ваші логи, наприклад коли сторонній сервіс, який використовує ваш застосунок, лежить:
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof BroadcastException) {
return Limit::perMinute(300);
}
});
})
За замовчуванням ліміти використовують клас винятку як ключ обмеження частоти. Це можна змінити, вказавши власний ключ через метод by на Limit:
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof BroadcastException) {
return Limit::perMinute(300)->by($e->getMessage());
}
});
})
Звісно, для різних винятків ви можете повертати і Lottery, і Limit:
use App\Exceptions\ApiMonitoringException;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Lottery;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
return match (true) {
$e instanceof BroadcastException => Limit::perMinute(300),
$e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
default => Limit::none(),
};
});
})
HTTP-винятки
Частина винятків описує коди HTTP-помилок від сервера. Наприклад, це може бути помилка «сторінку не знайдено» (404), «помилка авторизації» (401) або навіть згенерована розробником помилка 500. Щоб створити таку відповідь із будь-якого місця застосунку, скористайтеся хелпером abort:
abort(404);
Власні сторінки HTTP-помилок
Laravel дозволяє легко показувати власні сторінки помилок для різних HTTP-статусів. Наприклад, щоб змінити сторінку помилки для HTTP-статусу 404, створіть шаблон представлення resources/views/errors/404.blade.php. Це представлення буде рендеритися для всіх помилок 404, які генерує ваш застосунок. Представлення в цій теці треба називати відповідно до HTTP-статусу, якому вони відповідають. Екземпляр Symfony\Component\HttpKernel\Exception\HttpException, який піднімає функція abort, передається в представлення як змінна $exception:
<h2>{{ $exception->getMessage() }}</h2>
Ви можете опублікувати стандартні шаблони сторінок помилок Laravel за допомогою Artisan-команди vendor:publish. Після публікації шаблонів ви можете змінювати їх, як вам потрібно:
php artisan vendor:publish --tag=laravel-errors
Резервні сторінки HTTP-помилок
Ви також можете визначити «резервну» (fallback) сторінку помилки для цілої серії HTTP-статусів. Така сторінка відрендериться, якщо для конкретного HTTP-статусу, що виник, немає відповідної сторінки. Для цього визначте шаблони 4xx.blade.php і 5xx.blade.php у теці resources/views/errors вашого застосунку.
Резервні сторінки помилок не впливають на відповіді 404, 500 і 503, бо для цих статусів Laravel має внутрішні, спеціально призначені сторінки. Щоб змінити сторінки, які рендеряться для цих статусів, визначте для кожного з них окрему сторінку помилки.
Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.