PHPSeniorTechnical

Каковы лучшие практики обработки ошибок и логирования в PHP-приложении?

В PHP 8+ все ошибки — это \Throwable (Error + Exception). Используйте кастомную иерархию исключений, PSR-3 логирование через Monolog, set_exception_handler() для глобального перехвата и Sentry для production-мониторинга. Никогда не глотайте исключения молча.

Модель ошибок в PHP

PHP имеет две параллельные системы: старые errors (E_WARNING, E_NOTICE и т.д.) и современные exceptions. В PHP 7+ большинство фатальных ошибок превращены в \Error, а оба типа объединены под общим предком \Throwable. В современном коде используют исключения, старые ошибки конвертируют через set_error_handler().

Иерархия Throwable

Throwable
├── Error
│   ├── TypeError
│   ├── ValueError
│   ├── ArithmeticError
│   └── ParseError
└── Exception
    ├── RuntimeException
    │   ├── OutOfBoundsException
    │   └── UnexpectedValueException
    ├── LogicException
    │   ├── InvalidArgumentException
    │   └── DomainException
    └── (ваши кастомные исключения)

Кастомные исключения и иерархия

<?php
declare(strict_types=1);

// Базовое исключение домена
class DomainException extends \RuntimeException {}

// Конкретные исключения с контекстом
class UserNotFoundException extends DomainException
{
    public function __construct(private readonly string $userId)
    {
        parent::__construct("User not found: {$userId}");
    }

    public function getUserId(): string
    {
        return $this->userId;
    }
}

class InsufficientFundsException extends DomainException
{
    public function __construct(
        private readonly int $requested,
        private readonly int $available,
    ) {
        parent::__construct(
            "Insufficient funds: requested {$requested}, available {$available}"
        );
    }
}

Слоистая обработка ошибок

<?php
declare(strict_types=1);

// В Controller/Handler — ловим domain-исключения, конвертируем в HTTP-ответы
class OrderController
{
    public function create(Request $request): JsonResponse
    {
        try {
            $order = $this->orderService->create(
                userId: $request->get('user_id'),
                amount: $request->get('amount'),
            );
            return new JsonResponse(['id' => $order->id], 201);
        } catch (UserNotFoundException $e) {
            return new JsonResponse(['error' => 'User not found'], 404);
        } catch (InsufficientFundsException $e) {
            return new JsonResponse(['error' => 'Insufficient funds'], 422);
        }
        // RuntimeException и Error пробрасываются наверх — глобальный обработчик
    }
}

Логирование через PSR-3 и Monolog

<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Processor\WebProcessor;
use Monolog\Processor\IntrospectionProcessor;

// Настройка логгера
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('/var/log/app.log', Logger::DEBUG));
$logger->pushHandler(
    new SlackWebhookHandler($slackUrl, level: Logger::CRITICAL)
);
$logger->pushProcessor(new WebProcessor()); // добавляет IP, URL, method

// PSR-3: всегда используйте контекст вместо конкатенации
$logger->info('User logged in', [
    'user_id' => $user->id,
    'ip'      => $request->getClientIp(),
]);

$logger->error('Payment failed', [
    'order_id'  => $order->id,
    'amount'    => $order->amount,
    'exception' => $e, // Monolog умеет сериализовать Throwable
]);

// Уровни PSR-3: debug, info, notice, warning, error, critical, alert, emergency

Глобальный обработчик исключений

<?php
use Monolog\Logger;

// В точке входа (index.php / bootstrap)
set_exception_handler(function (\Throwable $e) use ($logger): void {
    $logger->critical('Unhandled exception', [
        'exception' => $e,
        'message'   => $e->getMessage(),
        'file'      => $e->getFile(),
        'line'      => $e->getLine(),
    ]);
    // В production — стандартная страница ошибки без стека
    http_response_code(500);
    echo json_encode(['error' => 'Internal Server Error']);
});

// Конвертация старых PHP-errors в ErrorException
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
    if (!(error_reporting() & $errno)) {
        return false;
    }
    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});

Интеграция с Sentry

<?php
// composer require sentry/sentry
\Sentry\init(['dsn' => $_ENV['SENTRY_DSN']]);

// Автоматически перехватывает необработанные исключения.
// Ручная отправка с контекстом:
try {
    $this->processPayment($order);
} catch (PaymentGatewayException $e) {
    \Sentry\withScope(function (\Sentry\State\Scope $scope) use ($e, $order): void {
        $scope->setContext('order', ['id' => $order->id, 'amount' => $order->amount]);
        \Sentry\captureException($e);
    });
    throw $e; // пробрасываем дальше
}

Подводные камни

  • Глотание исключений (catch без действия). Пустой catch или catch с только // TODO скрывает ошибки навсегда. Минимум — залогировать исключение, даже если не можете его обработать.
  • Логирование в catch с повторным выбросом. При throw $e в catch-блоке используйте тот же объект, а не new Exception($e->getMessage()) — иначе теряется оригинальный stack trace. Для wrapping используйте new DomainException('...', 0, $e) с третьим аргументом.
  • Чувствительные данные в логах. Никогда не логируйте пароли, токены, PAN (номера карт), персональные данные. Используйте маскирование или структурированные логи с явным allowlist полей.
  • Производительность логирования. Синхронная запись в лог на каждый запрос при высоком RPS создаёт I/O-узкое место. Используйте буферизованные хендлеры или отправку логов асинхронно (через очередь, fluentd, vector).
  • error_reporting в production. display_errors = Off и log_errors = On — обязательная конфигурация. display_errors = On на проде раскрывает стек-трейсы с путями файлов и конфигурацией атакующему.
  • Разные уровни для разных окружений. В dev логируйте DEBUG, в production — INFO или WARNING. Loggers в Symfony/Laravel поддерживают env-зависимую конфигурацию через channels и handlers.
  • Корреляция логов. В микросервисах и при обработке очередей добавляйте correlation_id / trace_id в контекст каждого лог-сообщения. Без этого трассировать запрос через несколько сервисов невозможно.
  • Обработка ошибок в CLI и worker-процессах. В PHP-FPM необработанное исключение убивает только один запрос. В долгоживущих worker-процессах (ReactPHP, Swoole, RoadRunner) необработанный \Throwable может уронить весь процесс — нужен явный catch на верхнем уровне event loop.

Common mistakes

  • Сводить error handling logging к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: интерпретируемый runtime PHP 8.x, обычно запускаемый как отдельный запрос в FPM, CLI или worker-процессе.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет error handling logging через конкретную точку lifecycle в PHP.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics