PHP 7+ 引入 Throwable 介面統一 Error 與 Exception,配合 set_error_handler、set_exception_handler、register_shutdown_function,可構建完整的錯誤捕獲、日誌記錄、友善回應機制。本文系統整理從基礎配置到現代框架整合的最佳實踐。
Throwable (介面)
├── Error (引擎錯誤,不可捕獲繼續執行)
│ ├── ArithmeticError
│ ├── AssertionError
│ ├── DivisionByZeroError
│ ├── CompileError
│ ├── ParseError
│ └── TypeError
└── Exception (使用者代碼拋出,可捕獲處理)
├── LogicException
│ ├── InvalidArgumentException
│ ├── DomainException
│ └── ...
└── RuntimeException
├── OutOfBoundsException
├── UnexpectedValueException
└── ...
// 開發環境:報告所有錯誤
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php/error.log');
// 生產環境:隱藏顯示,記錄日誌
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php/error.log');
ini_set('log_errors_max_len', '4096');
// 運行時動態調整
error_reporting(E_ALL); // 當前腳本報告所有
set_error_handler(function (int $severity, string $message, string $file, int $line) {
// 忽略被 @ 抑制的錯誤
if (error_reporting() === 0) return false;
// 映射嚴重度到 ErrorException
$exception = new \ErrorException($message, 0, $severity, $file, $line);
throw $exception;
}, E_ALL); // 捕獲所有級別
set_exception_handler(function (\Throwable $e) {
// 記錄日誌
$log = [
'time' => date('c'),
'type' => get_class($e),
'code' => $e->getCode(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
'request' => [
'uri' => $_SERVER['REQUEST_URI'] ?? '',
'method' => $_SERVER['REQUEST_METHOD'] ?? '',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
],
];
error_log(json_encode($log, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n", 3, '/var/log/php/exception.log');
// 回應客戶端
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "{$e->getMessage()}\n{$e->getTraceAsString()}\n");
exit(1);
}
http_response_code($e instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface ? $e->getStatusCode() : 500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => [
'code' => $e->getCode() ?: 500,
'message' => APP_ENV === 'prod' ? 'Internal Server Error' : $e->getMessage(),
'debug' => APP_ENV !== 'prod' ? ['file' => $e->getFile(), 'line' => $e->getLine()] : null,
],
], JSON_UNESCAPED_UNICODE);
});
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) {
// 模擬異常處理
$e = new \ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line']);
// 復用異常處理器邏輯
// ...
}
});
namespace App\Exception;
abstract class BusinessException extends \RuntimeException {
protected int $httpCode = 400;
public function getHttpCode(): int { return $this->httpCode; }
}
class ValidationException extends BusinessException {
private array $violations;
public function __construct(string $message, array $violations = [], int $code = 400) {
parent::__construct($message, $code);
$this->violations = $violations;
$this->httpCode = 422;
}
public function getViolations(): array { return $this->violations; }
}
class NotFoundException extends BusinessException {
public function __construct(string $resource = 'Resource') {
parent::__construct("{$resource} not found", 404);
$this->httpCode = 404;
}
}
class UnauthorizedException extends BusinessException {
public function __construct(string $message = 'Unauthorized') {
parent::__construct($message, 401);
$this->httpCode = 401;
}
}
public function show(int $id): JsonResponse {
$user = $this->userRepository->find($id);
if (!$user) {
throw new NotFoundException('User');
}
return new JsonResponse($user->toArray());
}
public function update(int $id, Request $request): JsonResponse {
$data = json_decode($request->getContent(), true);
$violations = $this->validator->validate($data);
if ($violations) {
throw new ValidationException('Invalid input', $violations);
}
// ...
}
Kernel::handle() 內建 ErrorListener,自動將錯誤轉為 HttpException,透過 kernel.exception 事件分發,TwigBundle 渲染錯誤頁,API 回傳 JSON。配置 debug: false 生產環境隱藏堆疊。App\Exceptions\Handler 統一處理,render() 方法判斷異常類型回應 JSON/視圖,report() 送往 Sentry/Flare/日誌。renderable() 註冊自定義異常渲染閉包。use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\JsonFormatter;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\WebProcessor;
$log = new Logger('app');
$handler = new RotatingFileHandler('/var/log/app/app.log', 30, Logger::DEBUG);
$handler->setFormatter(new JsonFormatter());
$handler->pushProcessor(new IntrospectionProcessor(Logger::DEBUG, ['Monolog\\', 'Symfony\\']));
$handler->pushProcessor(new WebProcessor());
$log->pushHandler($handler);
// 使用
$log->info('User logged in', ['user_id' => 123, 'ip' => '1.2.3.4']);
$log->error('Payment failed', ['order_id' => 'ABC', 'exception' => $e]);
set_error_handler 無法捕獲 E_ERROR、E_PARSE、E_CORE_ERROR 等致命錯誤,需靠 register_shutdown_function。error_log() 類型 1 (郵件) 需配置 sendmail_path,類型 3 (檔案) 需確保目錄可寫。display_errors=Off,但 log_errors=On。RuntimeException (可預期業務錯誤) 或 LogicException (程式邏輯錯誤),不要繼承 Error。構建健壯的 PHP 錯誤處理體系:錯誤→異常轉換、統一異常處理器、關閉函數捕獲致命錯誤、業務異常分層、PSR-3 結構化日誌、框架原生機制優先。這樣無論是 CLI 腳本、HTTP API、佇列消費者,都能獲得一致的錯誤觀測與恢復能力。