Обработка ошибок

Imhotep преобразует PHP-ошибки в исключения, передаёт необработанные исключения обработчику App\Exceptions\Handler, записывает нужные из них в журнал и формирует HTTP- или консольный ответ. Обычно приложение уже содержит свой класс App\Exceptions\Handler, расширяющий базовый Imhotep\Framework\Exceptions\Handler.

Режим отладки

Параметр debug в config/app.php обычно получает значение из APP_DEBUG файла .env:

APP_DEBUG=true
DOTENV

В локальной среде используйте true. В production установите false: подробности стека, пути к файлам и настройки не должны попадать пользователю.

Отчёты об исключениях

Когда в приложении происходит ошибка, её нужно не только показать, но и записать – чтобы вы могли её увидеть, проанализировать и исправить.

Запись ошибок (логирование) в Imhotep автоматически обрабатывается специальным классом App\Exceptions\Handler. Он сам решает, куда и как писать отчёт об ошибке. Но вы можете добавить свои действия — например, отправить уведомление в Telegram или Slack, сохранить ошибку в базу данных или передать в стороннюю систему мониторинга.

Обработчик reportable

Чтобы добавить свою логику при возникновении ошибки, используйте метод reportable() внутри метода register() вашего обработчика App\Exceptions\Handler:

namespace App\Exceptions;

use App\Exceptions\InvalidOrderException;
use Imhotep\Facades\Log;
use Imhotep\Framework\Exceptions\Handler as HandlerBase;

class Handler extends HandlerBase
{
    protected array $dontReport = [
        // ...
    ];

    public function register(): void
    {
        // Когда возникает ошибка InvalidOrderException — отправляем данные в мониторинг
        $this->reportable(function (InvalidOrderException $exception): void {
            // Здесь вы можете, например, вызвать API вашей системы мониторинга
            sendToMonitoring($exception->getMessage());
        });
        
        
        // Также можно явно указать класс
        $this->reportable(InvalidOrderException::class, function (InvalidOrderException $exception): void {
            // ...
        });
    }

    public function context(): array
    {
        return array_merge(parent::context(), [
            // your global data
        ]);
    }
}
PHP

Колбэк выполняется и для наследников указанного класса. Несколько колбэков одного типа выполняются в порядке регистрации. После их выполнения исключение по-прежнему попадёт в стандартный журнал. Чтобы прекратить дальнейшую обработку отчёта, верните false или вызовите stop:

$this->reportable(function (InvalidOrderException $exception): bool {
    // ...
    return false;
});

// или

$this->reportable(function (InvalidOrderException $exception): void {
    // ...
})->stop();
PHP

Метод report исключения

Логику отчёта можно поместить в сам класс исключения. Метод вызывается через контейнер, поэтому в него разрешено внедрять зависимости. Возврат false отменяет колбэки reportable и стандартное логирование:

namespace App\Exceptions;

use App\Services\ErrorTracker;
use Exception;

class InvalidOrderException extends Exception
{
    public function report(ErrorTracker $tracker): bool
    {
        $tracker->capture($this);

        return false;
    }
}
PHP

Контекст журнала

Если пользователь авторизован, базовый обработчик добавляет его идентификатор в контекст записи под ключом userId. Чтобы добавить общий контекст, переопределите публичный метод context:

public function context(): array
{
    return array_merge(parent::context(), [
        'release' => config('app.version'),
    ]);
}
PHP

Конкретное исключение может вернуть дополнительный контекст методом context. Его значения объединяются с общим контекстом:

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    public function __construct(private int $orderId)
    {
        parent::__construct('Некорректный заказ.');
    }

    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}
PHP

Помощник report

Используйте report, когда нужно записать исключение, но продолжить выполнение кода:

use Throwable;

try {
    // Проверка значения...
} catch (Throwable $exception) {
    report($exception);

    return false;
}
PHP

Исключения без отчёта

Добавьте исключение в $dontReport, если его не нужно записывать в журнал. Оно всё равно будет отображено обычным способом:

use App\Exceptions\InvalidOrderException;

protected array $dontReport = [
    InvalidOrderException::class,
];
PHP

HTTP-исключения, ошибки валидации, аутентификации, авторизации и ответы, созданные через abort(Response|Responsable), уже не записываются в журнал.

Отображение исключений

При обработке HTTP-запроса Imhotep сначала проверяет колбэки renderable, затем метод render исключения, интерфейс Responsable и встроенные обработчики валидации и аутентификации. Если обработчик не вернул ответ, применяется стандартное отображение.

Обработчики renderable

Зарегистрируйте пользовательский ответ для типа исключения в методе register. Как и у reportable, тип можно вывести из первого параметра замыкания:

use App\Exceptions\InvalidOrderException;
use Imhotep\Contracts\Http\Request;

public function register(): void
{
    $this->renderable(function (InvalidOrderException $exception, Request $request) {
        return response()->view('errors.invalid-order', [], 500);
    });
}
PHP

Для встроенного 404-исключения используйте класс Imhotep\Http\Exceptions\NotFoundHttpException:

use Imhotep\Http\Exceptions\NotFoundHttpException;

$this->renderable(function (NotFoundHttpException $exception, $request) {
    if ($request->is('api/*')) {
        return response()->json(['message' => 'Запись не найдена.'], 404);
    }

    return null; // Использовать стандартное отображение.
});
PHP

Методы render и Responsable

Исключение может самостоятельно сформировать ответ методом render. Если метод вернёт null или false, Imhotep продолжит стандартную обработку.

namespace App\Exceptions;

use Exception;
use Imhotep\Contracts\Http\Request;

class InvalidOrderException extends Exception
{
    public function render(Request $request)
    {
        if ($request->expectsJson()) {
            return response()->json(['message' => $this->getMessage()], 422);
        }

        return null;
    }
}
PHP

Вместо метода render исключение может реализовать Imhotep\Contracts\Http\Responsable и вернуть ответ из toResponse(Request $request).

HTTP-исключения и abort

Помощник abort немедленно прекращает выполнение и формирует ответ. Передайте статус, при необходимости сообщение и заголовки:

abort(403, 'Доступ запрещён');

abort(429, 'Слишком много запросов', [
    'Retry-After' => '60',
]);
PHP

Также можно передать готовый Response или объект Responsable; это полезно для раннего возврата из контроллера или middleware:

abort(response()->json(['message' => 'Недоступно'], 503));
PHP

Собственные страницы ошибок

Для HTTP-исключения Imhotep ищет шаблон в resources/views/errors. Сначала используется шаблон точного статуса, затем шаблон группы кодов:

  • 404.moon.php — только для ответа 404;
  • 4xx.moon.php — для любого ответа 4xx, если точный шаблон не найден;
  • 5xx.moon.php — для любого ответа 5xx, если точный шаблон не найден.

В шаблон передаётся объект HTTP-исключения в переменной $exception:

<h2>{{ $exception->getMessage() }}</h2>
PHP

Если шаблон не найден, ответ содержит сообщение исключения, его статус и заголовки.