はじめに
PHPのデフォルトエラーハンドラは、エラーをブラウザやログに出力するだけです。本番環境でユーザーにエラー詳細を見せてしまったり、エラーをデータベースやSlackに通知できなかったりと、そのままでは不十分な場面が多くあります。
set_error_handler() は、PHPのエラー処理を自分で実装したカスタム関数に差し替えるための関数です。エラーの種類・メッセージ・発生場所を受け取り、ログ記録・通知・フォールバック処理など自由な後処理を実装できます。try-catch では捕捉できないNotice・Warning・Deprecatedといった非致命的エラーをハンドリングする唯一の手段でもあります。
関数の概要
| 項目 | 内容 |
|---|
| 関数名 | set_error_handler() |
| 所属 | PHP エラー処理関数 |
| 導入バージョン | PHP 4.0.1以降 |
| PHP 8.x | 対応済み |
構文
set_error_handler(?callable $callback, int $error_levels = E_ALL): callable|null
パラメータ
| パラメータ | 型 | 説明 |
|---|
$callback | callable|null | カスタムハンドラ関数。null を渡すとデフォルトに戻す |
$error_levels | int | ハンドラを適用するエラーレベル(ビットマスク)。デフォルトは E_ALL |
戻り値
- 以前に設定されていたハンドラ(callable)を返します
- 以前のハンドラがない場合は
null を返します
コールバック関数のシグネチャ
function errorHandler(
int $errno, // エラーレベル定数(E_WARNING など)
string $errstr, // エラーメッセージ
string $errfile, // エラー発生ファイル
int $errline // エラー発生行番号
): bool
戻り値の意味
| 戻り値 | 動作 |
|---|
true | PHP のデフォルトエラーハンドラを呼ばない(ハンドル済みとみなす) |
false | PHP のデフォルトエラーハンドラを引き続き呼ぶ |
ハンドリングできるエラーレベル
| 定数 | 値 | 説明 |
|---|
E_ERROR | 1 | 致命的エラー(ハンドラでは捕捉不可) |
E_WARNING | 2 | 警告(実行は継続) |
E_NOTICE | 8 | 注意(未定義変数など) |
E_USER_ERROR | 256 | trigger_error() によるユーザー定義エラー |
E_USER_WARNING | 512 | trigger_error() によるユーザー定義警告 |
E_USER_NOTICE | 1024 | trigger_error() によるユーザー定義通知 |
E_DEPRECATED | 8192 | 非推奨機能の使用 |
E_ALL | 32767 | すべてのエラー |
⚠️ E_ERROR / E_PARSE / E_CORE_ERROR / E_COMPILE_ERROR は set_error_handler() では捕捉できません。これらは register_shutdown_function() + error_get_last() で対処します。
基本的な使い方
<?php
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
echo "[エラー] [{$errno}] {$errstr} in {$errfile}:{$errline}\n";
return true; // デフォルトハンドラを呼ばない
});
// テスト
trigger_error('テストエラー', E_USER_WARNING);
echo "処理継続中\n";
/*
出力例:
[エラー] [512] テストエラー in /path/to/script.php:8
処理継続中
*/
実践的なクラスベースの使用例
例1:ログファイル記録エラーハンドラクラス
<?php
/**
* エラーを構造化してログファイルに記録するエラーハンドラクラス
* 環境(本番・開発)に応じてエラー表示とログ記録を切り替える
*/
class FileLoggingErrorHandler
{
private string $logFile;
private bool $displayErrors;
private array $levelNames = [
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_NOTICE => 'NOTICE',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING => 'USER_WARNING',
E_USER_NOTICE => 'USER_NOTICE',
E_DEPRECATED => 'DEPRECATED',
E_USER_DEPRECATED => 'USER_DEPRECATED',
E_STRICT => 'STRICT',
];
public function __construct(
string $logFile = '/var/log/app/php_errors.log',
bool $displayErrors = false
) {
$this->logFile = $logFile;
$this->displayErrors = $displayErrors;
}
/**
* このハンドラを登録する
*/
public function register(int $errorLevels = E_ALL): ?callable
{
return set_error_handler([$this, 'handle'], $errorLevels);
}
/**
* エラーハンドラ本体
*/
public function handle(int $errno, string $errstr, string $errfile, int $errline): bool
{
// error_reporting() で抑制されているエラーは無視(@ 演算子対応)
if (!(error_reporting() & $errno)) {
return false;
}
$levelName = $this->levelNames[$errno] ?? "E_{$errno}";
$timestamp = date('Y-m-d H:i:s');
$shortFile = basename($errfile);
$entry = sprintf(
"[%s] [%s] %s in %s:%d\n",
$timestamp,
$levelName,
$errstr,
$shortFile,
$errline
);
// ログファイルに記録
$this->writeLog($entry);
// 開発環境ではブラウザにも表示
if ($this->displayErrors) {
echo "<pre style='color:red'>{$entry}</pre>";
}
// E_USER_ERROR は処理を止める
if ($errno === E_USER_ERROR) {
exit(1);
}
return true;
}
private function writeLog(string $entry): void
{
$dir = dirname($this->logFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($this->logFile, $entry, FILE_APPEND | LOCK_EX);
}
}
$handler = new FileLoggingErrorHandler(
logFile: '/tmp/app_errors.log',
displayErrors: true
);
$handler->register();
trigger_error('データベース接続に失敗しました', E_USER_WARNING);
trigger_error('非推奨の関数を使用しています', E_USER_DEPRECATED);
/*
出力例:
[2025-09-01 12:00:00] [USER_WARNING] データベース接続に失敗しました in script.php:XX
[2025-09-01 12:00:00] [USER_DEPRECATED] 非推奨の関数を使用しています in script.php:XX
*/
例2:エラーを例外に変換するハンドラクラス
<?php
/**
* PHP の Warning・Notice・Deprecated などを
* ErrorException に変換して try-catch で捕捉できるようにするクラス
*
* PHP 8.0以降の一部 Warning は自動的に例外になるが、
* それ以外のエラーレベルにはこのハンドラが有効
*/
class ErrorToExceptionConverter
{
/** 例外に変換するエラーレベル */
private int $convertLevels;
public function __construct(
int $convertLevels = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
) {
$this->convertLevels = $convertLevels;
}
/**
* このハンドラを登録する
*/
public function register(): ?callable
{
return set_error_handler([$this, 'handle']);
}
/**
* エラーを ErrorException に変換して投げる
*/
public function handle(int $errno, string $errstr, string $errfile, int $errline): bool
{
// @ 演算子で抑制されているエラーは変換しない
if (!(error_reporting() & $errno)) {
return false;
}
// 対象レベルのみ変換する
if (!($errno & $this->convertLevels)) {
return false; // デフォルトハンドラに委譲
}
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
}
/**
* ハンドラ登録を解除してデフォルトに戻す
*/
public function unregister(): void
{
restore_error_handler();
}
}
$converter = new ErrorToExceptionConverter();
$converter->register();
try {
// 未定義変数への参照(通常は Notice)→ ErrorException に変換される
$value = $undefinedVariable ?? null; // PHP 8 では ?? で Notice を抑制可能
trigger_error('存在しないキーへのアクセス', E_USER_WARNING);
} catch (\ErrorException $e) {
echo "捕捉した例外: [{$e->getSeverity()}] {$e->getMessage()}\n";
echo " 発生場所: {$e->getFile()}:{$e->getLine()}\n";
}
/*
出力例:
捕捉した例外: [512] 存在しないキーへのアクセス
発生場所: /path/to/script.php:XX
*/
例3:エラーレベル別の通知振り分けクラス
<?php
/**
* エラーの重大度に応じてログ・メール・Slack などへ振り分けるクラス
* 致命的なエラーだけ即時通知し、軽微なエラーはログにまとめる
*/
class TieredErrorNotifier
{
private array $handlers = [];
private array $buffer = [];
public function __construct()
{
// デフォルトハンドラを設定
$this->handlers['critical'] = [$this, 'notifyCritical'];
$this->handlers['warning'] = [$this, 'bufferWarning'];
$this->handlers['info'] = [$this, 'logInfo'];
}
public function register(): ?callable
{
return set_error_handler([$this, 'handle']);
}
public function handle(int $errno, string $errstr, string $errfile, int $errline): bool
{
if (!(error_reporting() & $errno)) {
return false;
}
$context = [
'errno' => $errno,
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
'time' => microtime(true),
];
match (true) {
in_array($errno, [E_ERROR, E_USER_ERROR], true)
=> ($this->handlers['critical'])($context),
in_array($errno, [E_WARNING, E_USER_WARNING], true)
=> ($this->handlers['warning'])($context),
default
=> ($this->handlers['info'])($context),
};
return true;
}
private function notifyCritical(array $ctx): void
{
echo "🚨 [CRITICAL] {$ctx['message']} in {$ctx['file']}:{$ctx['line']}\n";
// 実際には mail() や Slack Webhook で即時通知する
// mail('admin@example.com', '致命的エラー', $ctx['message']);
}
private function bufferWarning(array $ctx): void
{
$this->buffer[] = $ctx;
echo "⚠️ [WARNING] {$ctx['message']} (バッファリング: " . count($this->buffer) . "件)\n";
}
private function logInfo(array $ctx): void
{
$levelName = $this->levelName($ctx['errno']);
echo "ℹ️ [{$levelName}] {$ctx['message']}\n";
}
/**
* バッファリングされた Warning をまとめて通知する
*/
public function flushBuffer(): void
{
if (empty($this->buffer)) {
return;
}
echo "\n=== バッファリング済み WARNING: " . count($this->buffer) . "件 ===\n";
foreach ($this->buffer as $i => $ctx) {
printf(" #%d %s in %s:%d\n", $i + 1, $ctx['message'], basename($ctx['file']), $ctx['line']);
}
$this->buffer = [];
}
private function levelName(int $errno): string
{
return [
E_NOTICE => 'NOTICE',
E_USER_NOTICE => 'USER_NOTICE',
E_DEPRECATED => 'DEPRECATED',
E_USER_DEPRECATED => 'USER_DEPRECATED',
E_STRICT => 'STRICT',
][$errno] ?? "E_{$errno}";
}
}
$notifier = new TieredErrorNotifier();
$notifier->register();
trigger_error('設定ファイルが読み込めません', E_USER_WARNING);
trigger_error('非推奨のAPIを使用しています', E_USER_DEPRECATED);
trigger_error('キャッシュの書き込みに失敗', E_USER_WARNING);
$notifier->flushBuffer();
/*
出力例:
⚠️ [WARNING] 設定ファイルが読み込めません (バッファリング: 1件)
ℹ️ [USER_DEPRECATED] 非推奨のAPIを使用しています
⚠️ [WARNING] キャッシュの書き込みに失敗 (バッファリング: 2件)
=== バッファリング済み WARNING: 2件 ===
#1 設定ファイルが読み込めません in script.php:XX
#2 キャッシュの書き込みに失敗 in script.php:XX
*/
例4:エラーハンドラのスタック管理クラス
<?php
/**
* set_error_handler() / restore_error_handler() を使って
* 複数のエラーハンドラをスタック管理するクラス
*
* 特定の処理ブロックだけ別のハンドラを適用し、
* ブロック終了後は元のハンドラに自動で戻す
*/
class ErrorHandlerStack
{
private array $stack = [];
/**
* 新しいハンドラをスタックに積む
*/
public function push(callable $handler, int $levels = E_ALL): void
{
$previous = set_error_handler($handler, $levels);
$this->stack[] = [
'handler' => $handler,
'previous' => $previous,
'levels' => $levels,
];
echo "ハンドラを追加: スタック深さ " . count($this->stack) . "\n";
}
/**
* 最後に積んだハンドラを取り出して元に戻す
*/
public function pop(): void
{
if (empty($this->stack)) {
return;
}
restore_error_handler();
array_pop($this->stack);
echo "ハンドラを復元: スタック深さ " . count($this->stack) . "\n";
}
/**
* スコープ付きでハンドラを適用するユーティリティ
* コールバック終了後に自動で pop する
*/
public function withHandler(callable $handler, callable $scope, int $levels = E_ALL): mixed
{
$this->push($handler, $levels);
try {
return $scope();
} finally {
$this->pop(); // 例外が発生しても必ず復元
}
}
public function depth(): int
{
return count($this->stack);
}
}
$stack = new ErrorHandlerStack();
// 通常のハンドラを登録
$stack->push(function (int $errno, string $errstr): bool {
echo " [通常ハンドラ] {$errstr}\n";
return true;
});
trigger_error('通常のエラー', E_USER_NOTICE);
// 特定ブロックだけ別ハンドラを使う
$result = $stack->withHandler(
handler: function (int $errno, string $errstr): bool {
echo " [特殊ハンドラ] {$errstr}\n";
return true;
},
scope: function (): string {
trigger_error('特殊ブロック内のエラー', E_USER_WARNING);
return '処理完了';
}
);
trigger_error('ブロック後のエラー', E_USER_NOTICE);
$stack->pop();
echo "スコープの戻り値: {$result}\n";
/*
出力例:
ハンドラを追加: スタック深さ 1
[通常ハンドラ] 通常のエラー
ハンドラを追加: スタック深さ 2
[特殊ハンドラ] 特殊ブロック内のエラー
ハンドラを復元: スタック深さ 1
[通常ハンドラ] ブロック後のエラー
ハンドラを復元: スタック深さ 0
スコープの戻り値: 処理完了
*/
例5:致命的エラーも捕捉するシャットダウンハンドラ組み合わせクラス
<?php
/**
* set_error_handler() では捕捉できない致命的エラー(E_ERROR など)を
* register_shutdown_function() + error_get_last() で補完するクラス
*
* 両者を組み合わせてほぼすべてのエラーを一元管理する
*/
class ComprehensiveErrorHandler
{
private array $errorLog = [];
private bool $registered = false;
public function register(): void
{
if ($this->registered) {
return;
}
// 通常エラーのハンドラ(E_ERROR など致命的エラーは除く)
set_error_handler([$this, 'handleError']);
// 未捕捉例外のハンドラ
set_exception_handler([$this, 'handleException']);
// 致命的エラーのハンドラ(シャットダウン時に確認)
register_shutdown_function([$this, 'handleShutdown']);
$this->registered = true;
echo "包括的エラーハンドラ登録完了\n";
}
/**
* 通常エラーハンドラ(Warning / Notice / Deprecated など)
*/
public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
{
if (!(error_reporting() & $errno)) {
return false;
}
$this->record('error', $errno, $errstr, $errfile, $errline);
if ($errno === E_USER_ERROR) {
$this->sendErrorResponse(500, '内部サーバーエラーが発生しました');
exit(1);
}
return true;
}
/**
* 未捕捉例外ハンドラ
*/
public function handleException(\Throwable $e): void
{
$this->record('exception', $e->getCode(), $e->getMessage(), $e->getFile(), $e->getLine());
$this->sendErrorResponse(500, '予期しないエラーが発生しました');
exit(1);
}
/**
* 致命的エラーのシャットダウンハンドラ
*/
public function handleShutdown(): void
{
$error = error_get_last();
// 致命的エラーレベルのみ対処
$fatalLevels = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
if ($error !== null && ($error['type'] & $fatalLevels)) {
$this->record('fatal', $error['type'], $error['message'], $error['file'], $error['line']);
echo "🚨 致命的エラー: {$error['message']} in {$error['file']}:{$error['line']}\n";
}
// バッファリングされたエラーを最終出力
$this->flushLog();
}
private function record(string $kind, int $code, string $msg, string $file, int $line): void
{
$entry = [
'kind' => $kind,
'code' => $code,
'message' => $msg,
'file' => basename($file),
'line' => $line,
'time' => date('H:i:s'),
];
$this->errorLog[] = $entry;
echo "[{$entry['time']}] [{$kind}:{$code}] {$msg} ({$entry['file']}:{$line})\n";
}
private function sendErrorResponse(int $statusCode, string $message): void
{
if (!headers_sent()) {
http_response_code($statusCode);
header('Content-Type: application/json');
}
// echo json_encode(['error' => $message]);
}
private function flushLog(): void
{
if (count($this->errorLog) > 0) {
echo "\n合計 " . count($this->errorLog) . " 件のエラーを記録しました\n";
}
}
public function getErrorLog(): array
{
return $this->errorLog;
}
}
$handler = new ComprehensiveErrorHandler();
$handler->register();
trigger_error('設定値が不正です', E_USER_WARNING);
trigger_error('非推奨メソッドを呼び出しました', E_USER_DEPRECATED);
// 未定義の定数参照(E_WARNING)
// echo UNDEFINED_CONST;
/*
出力例:
包括的エラーハンドラ登録完了
[12:00:00] [error:512] 設定値が不正です (script.php:XX)
[12:00:00] [error:16384] 非推奨メソッドを呼び出しました (script.php:XX)
合計 2 件のエラーを記録しました
*/
例6:テスト環境でのエラー検証クラス
<?php
/**
* ユニットテストでエラーが正しく発生しているかを検証するクラス
* set_error_handler() でエラーをキャプチャしてアサーションに使用する
*/
class ErrorCapture
{
private array $captured = [];
private bool $capturing = false;
private ?callable $previous = null;
/**
* エラーキャプチャを開始する
*/
public function start(int $levels = E_ALL): void
{
$this->captured = [];
$this->capturing = true;
$this->previous = set_error_handler(
function (int $errno, string $errstr, string $errfile, int $errline): bool {
$this->captured[] = [
'errno' => $errno,
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
];
return true; // デフォルトハンドラを呼ばない
},
$levels
);
}
/**
* エラーキャプチャを停止して元のハンドラに戻す
*/
public function stop(): array
{
restore_error_handler();
$this->capturing = false;
return $this->captured;
}
/**
* キャプチャしたエラーを検証する
*/
public function assertCaptured(int $expectedErrno, string $expectedMessage = ''): void
{
foreach ($this->captured as $error) {
if ($error['errno'] === $expectedErrno) {
if ($expectedMessage === '' || str_contains($error['message'], $expectedMessage)) {
echo "✅ エラー捕捉確認: [{$error['errno']}] {$error['message']}\n";
return;
}
}
}
throw new \AssertionError(
"期待するエラー [{$expectedErrno}] '{$expectedMessage}' が発生しませんでした。\n"
. "実際に発生したエラー: " . json_encode($this->captured, JSON_UNESCAPED_UNICODE)
);
}
public function assertNone(): void
{
if (!empty($this->captured)) {
throw new \AssertionError(
"エラーが発生しないことを期待しましたが "
. count($this->captured) . " 件発生しました"
);
}
echo "✅ エラーなし確認: PASS\n";
}
public function assertCount(int $expected): void
{
$actual = count($this->captured);
if ($actual !== $expected) {
throw new \AssertionError("エラー件数: 期待={$expected}, 実際={$actual}");
}
echo "✅ エラー件数確認: {$actual}件\n";
}
public function getCaptured(): array
{
return $this->captured;
}
}
// テスト利用例
function validateAge(int $age): bool
{
if ($age < 0) {
trigger_error("年齢は0以上である必要があります: {$age}", E_USER_WARNING);
return false;
}
if ($age > 150) {
trigger_error("年齢が現実的ではありません: {$age}", E_USER_NOTICE);
}
return true;
}
$capture = new ErrorCapture();
// 正常系テスト
$capture->start();
validateAge(25);
$capture->stop();
$capture->assertNone();
// 異常系テスト
$capture->start();
validateAge(-5);
$errors = $capture->stop();
$capture->assertCaptured(E_USER_WARNING, '年齢は0以上');
$capture->assertCount(1);
/*
出力例:
✅ エラーなし確認: PASS
✅ エラー捕捉確認: [512] 年齢は0以上である必要があります: -5
✅ エラー件数確認: 1件
*/
set_error_handler() と set_exception_handler() の使い分け
┌─────────────────────────────────────────────────────┐
│ PHP のエラー体系 │
├──────────────────────┬──────────────────────────────┤
│ エラー(Error) │ 例外(Exception / Throwable) │
│ E_WARNING │ throw new RuntimeException │
│ E_NOTICE │ throw new InvalidArgument │
│ E_USER_ERROR │ TypeError(PHP 7以降) │
│ E_DEPRECATED │ Error(PHP 7以降) │
├──────────────────────┼──────────────────────────────┤
│ set_error_handler() │ set_exception_handler() │
│ で捕捉 │ で捕捉 │
└──────────────────────┴──────────────────────────────┘
致命的エラー(E_ERROR / E_PARSE):
→ register_shutdown_function() + error_get_last() で対処
@ 演算子との連携
<?php
set_error_handler(function (int $errno, string $errstr): bool {
echo "ハンドラ呼び出し: {$errstr}\n";
return true;
});
// @ でエラーを抑制すると error_reporting() が 0 になる
// → ハンドラ内で error_reporting() & $errno が 0 になるためスキップ可能
$result = @file_get_contents('/nonexistent/file');
// ハンドラは呼ばれない(抑制されている)
// @ なしだと呼ばれる
$result2 = file_get_contents('/nonexistent/file');
// → ハンドラ呼び出し: ...
関連関数との比較
| 関数 | 役割 |
|---|
set_error_handler() | Warning / Notice などのエラーをカスタムハンドラで処理 |
set_exception_handler() | 未捕捉の例外をカスタムハンドラで処理 |
register_shutdown_function() | スクリプト終了時に処理を実行(致命的エラーに対処) |
error_get_last() | 最後に発生したエラー情報を取得(shutdown 内で使用) |
restore_error_handler() | 直前のエラーハンドラに戻す |
trigger_error() | ユーザー定義エラーを発生させる(テストやライブラリ内で使用) |
error_reporting() | 報告するエラーレベルを取得・設定する |
よくある落とし穴
<?php
// ❌ NG:E_ERROR・E_PARSE は set_error_handler() では捕捉できない
set_error_handler(function ($errno, $errstr): bool {
echo "捕捉: {$errstr}\n";
return true;
});
// undeclared_function(); // E_ERROR → ハンドラは呼ばれない・スクリプト終了
// ✅ 致命的エラーは register_shutdown_function() で対処する
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error && ($error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR))) {
echo "致命的エラー検出: {$error['message']}\n";
}
});
<?php
// ❌ よくある誤解:@ 演算子でエラーを抑制してもハンドラが常に呼ばれると思っている
set_error_handler(function ($errno, $errstr): bool {
// error_reporting() を確認しないと @ 抑制エラーも処理してしまう
echo "エラー: {$errstr}\n";
return true;
});
// ✅ @ 演算子に対応するには error_reporting() のチェックが必要
set_error_handler(function ($errno, $errstr): bool {
if (!(error_reporting() & $errno)) {
return false; // 抑制されているのでスキップ
}
echo "エラー: {$errstr}\n";
return true;
});
<?php
// ❌ NG:set_error_handler() の戻り値(以前のハンドラ)を捨てると復元できない
set_error_handler($myHandler); // 以前のハンドラを保存しない
// ✅ 以前のハンドラを保存して必要なら restore する
$previous = set_error_handler($myHandler);
// ... 処理 ...
restore_error_handler(); // または set_error_handler($previous);
まとめ
| 項目 | 内容 |
|---|
| 関数名 | set_error_handler(?callable $callback, int $error_levels = E_ALL): callable|null |
| 主な用途 | Warning / Notice / Deprecated などのエラーをカスタム処理する |
| 捕捉できないエラー | E_ERROR / E_PARSE / E_CORE_ERROR(shutdown + error_get_last() で補完) |
| 戻り値 | 以前のハンドラ(callable)または null |
@ 演算子対応 | ハンドラ内で error_reporting() & $errno をチェックする |
| ハンドラの復元 | restore_error_handler() または set_error_handler(null) |
| 例外との連携 | ErrorException を throw することで try-catch で捕捉可能にできる |
set_error_handler() は PHP のエラー処理基盤を自分でコントロールするための中核関数です。ログ記録・例外変換・通知振り分け・テスト用キャプチャといった用途に合わせてカスタマイズし、set_exception_handler() と register_shutdown_function() と組み合わせることで、あらゆるエラーを漏れなくハンドリングできる堅牢なエラー処理基盤を構築できます。
参考リンク