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.