保守性の高いコードのためのPHPエラーハンドリングのベストプラクティス
新しい機能をデプロイした直後に、PHPアプリケーションが致命的なエラーをスローすることがあります。ユーザーには空白のページが表示され、ログは空です。聞き覚えがありますか?不適切なエラーハンドリングは、PHPコードが保守不能になる最も一般的な理由の1つです。この記事では、コードをより堅牢にし、デバッグを容易にし、保守を簡単にする実践的なPHPエラーハンドリングのベストプラクティスを紹介します。
PHPエラーハンドリングが重要な理由
PHPはデフォルトで寛容です。警告の後でも実行を続けることが多く、エラーは黙って無視されることがあります。この柔軟性は諸刃の剣です。適切な処理がないと、エラーは以下の問題を引き起こす可能性があります:
- ユーザーに機密情報を公開する(例:スタックトレース内のデータベース認証情報)。
- データを破損したり、アプリケーションを一貫性のない状態に置く。
- エラーが散在したり抑制されたりするため、デバッグを悪夢にする。
適切なエラーハンドリングにより、何か問題が発生したときにそれを把握し、迅速に修正し、ユーザーに優雅な体験を提供できます。
1. 適切なエラー報告レベルを設定する
最初のステップは、環境に応じてPHPのエラー報告を正しく設定することです。開発環境ではすべてのエラーを表示し、本番環境ではログに記録するが表示しないようにします。
// 開発環境
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// 本番環境
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/path/to/php-error.log');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
環境変数や設定ファイルを使用して、これらの設定を自動的に切り替えます。本番サーバーでphp.iniを手動で編集することに依存しないでください。
2. エラーコードの代わりに例外を使用する
エラーコード(falseや-1など)を返すのは、コードを乱雑にし、失敗を無視しやすくするレガシーパターンです。例外はエラーを明示的に処理することを強制し、正常系のコードをクリーンに保ちます。
// 悪い例: エラーコード
function getUser($id) {
$user = db_find($id);
if (!$user) {
return false; // 呼び出し元がチェックする必要がある
}
return $user;
}
// 良い例: 例外
function getUser($id) {
$user = db_find($id);
if (!$user) {
throw new UserNotFoundException("User $id not found");
}
return $user;
}
異なるエラータイプに対してカスタム例外クラスを作成します。これにより、特定のエラーをキャッチして適切に処理することが容易になります。
3. 適切なレベルで例外をキャッチする
よくある間違いは、例外を早すぎたり広すぎたりしてキャッチすることです。例外をキャッチするのは、実際に何かできる場合(ログに記録する、再試行する、ユーザーフレンドリーなメッセージを表示する)だけにしましょう。
try {
$user = getUser($id);
$order = createOrder($user, $items);
} catch (UserNotFoundException $e) {
// ユーザーが見つからない場合の具体的な処理
return response('User not found', 404);
} catch (PaymentFailedException $e) {
// 支払い失敗の処理
return response('Payment failed: ' . $e->getMessage(), 400);
} catch (Throwable $e) {
// 予期しないエラーのキャッチオール
log_error($e);
return response('Something went wrong', 500);
}
Throwable(PHP 7+)を使用して、例外とエラーの両方をキャッチします。空のcatchブロックは避け、キャッチしたら意味のある処理を行いましょう。
4. コンテキストを含めてエラーをログに記録する
ロギングは本番環境の問題をデバッグするための最良の友です。しかし、「Error occurred」のようなログメッセージは役に立ちません。ユーザーID、リクエストパラメータ、スタックトレース、タイムスタンプなどのコンテキストを含めましょう。
try {
processPayment($order);
} catch (PaymentException $e) {
error_log(sprintf(
"Payment failed for order %d: %s in %s:%d\nStack trace: %s",
$order->id,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
throw $e; // ログ記録後に再スロー
}
構造化ログにはMonologのようなロギングライブラリの使用を検討してください。さまざまなハンドラ(ファイル、syslog、Slack)とログレベル(debug、info、warning、error)をサポートしています。
5. カスタムエラーハンドラを作成する
PHPのデフォルトのエラーハンドラはエラーを画面に出力するため、本番環境には適していません。カスタムエラーハンドラを使用すると、エラーを例外に変換したり、ログに記録したり、フレンドリーなエラーページを表示したりできます。
set_error_handler(function ($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
return false; // error_reporting設定を尊重
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function ($e) {
log_error($e);
http_response_code(500);
include 'views/error.php';
});
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
log_error(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']));
}
});
この設定により、致命的なエラーを含むすべてのエラーがログに記録され、優雅に処理されます。
6. 入力を検証し、早期に失敗する
多くのエラーは無効な入力から発生します。アプリケーションの境界(コントローラ、APIエンドポイント)でデータを検証し、検証が失敗した場合はすぐに例外をスローします。これにより、エラーがコードの深部に伝播するのを防ぎます。
function createUser(array $data) {
if (empty($data['email'])) {
throw new InvalidArgumentException('Email is required');
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
// ... 自信を持って続行
}
PHPのフィルタ関数やバリデーションライブラリ(Respect\Validationなど)を使用して、検証を一貫させます。
7. @でエラーを抑制しない
@演算子はエラーを無音にし、デバッグを難しくします。警告を発する可能性のある関数(file_get_contentsなど)を呼び出すときに使用したくなりますが、実際の問題を隠してしまいます。代わりに、事前条件をチェックするか、例外を伴うtry-catchを使用してください。
// 悪い例
$content = @file_get_contents($url);
// 良い例
if (!is_readable($url)) {
throw new RuntimeException("Cannot read $url");
}
$content = file_get_contents($url);
どうしても抑制する必要がある場合は、よく理解されているケースに限定し、その理由を文書化してください。
8. 集中化されたエラーハンドリングミドルウェアを使用する
LaravelやSymfonyのようなフレームワークでは、エラーハンドリングはしばしばミドルウェアや例外ハンドラに集中化されています。独自に構築する場合は、すべての例外をキャッチしてHTTPレスポンスに変換する単一のエントリポイントを作成します。
// フロントコントローラ(index.php)内
try {
$response = $router->dispatch($request);
} catch (HttpException $e) {
$response = new Response($e->getMessage(), $e->getStatusCode());
} catch (Throwable $e) {
log_error($e);
$response = new Response('Internal Server Error', 500);
}
$response->send();
これにより、エラーハンドリングロジックが1か所にまとまり、一貫性が保たれます。
比較: エラーハンドリングのアプローチ
| アプローチ | 長所 | 短所 |
|---|---|---|
| エラーコード | シンプル、例外なし | 無視しやすい、コードが乱雑になる |
| 例外 | 処理を強制、関心の分離がクリーン | 過度に使用される可能性、パフォーマンスオーバーヘッド |
| カスタムエラーハンドラ | 集中化、すべてのエラーをキャッチ | セットアップが必要、誤設定するとエラーをマスクする可能性 |
| ロギングのみ | 非侵入的、監視に適している | エラーを処理せず、記録するだけ |
FAQ
PHPにおけるエラーと例外の違いは何ですか?
エラーは構文エラーや型エラーなどの低レベルの問題であり、例外は例外的な条件を表すスローされたオブジェクトです。PHP 7+では、両方ともThrowableインターフェースを実装しているため、単一のcatchブロックで両方をキャッチできます。
すべての関数呼び出しにtry-catchを使用すべきですか?
いいえ、それは過度に防御的なコードにつながります。例外をキャッチするのは、意味のある処理ができる場合(ログ、再試行、ユーザーフレンドリーなメッセージの表示)だけにしましょう。予期しないケースでは、例外を中央ハンドラにバブルアップさせます。
機密データを公開せずにエラーをログに記録するにはどうすればよいですか?
パスワード、トークン、個人データを削除してログメッセージをサニタイズします。コンテキストフィールドを持つ構造化ロギングを使用し、ロギングライブラリを設定して機密キーを秘匿します。また、ログファイルが安全に保存され、アクセスが制限されていることを確認してください。
PHPエラーハンドリングを合理化する準備はできましたか?まず、現在のエラー報告設定を監査し、カスタムエラーハンドラを実装することから始めましょう。JSONペイロードやログの迅速なデバッグには、JSON Formatterを試して、データを検証および整形出力してください。