やりたいこと
- Fatal Errorはログを吐いてエラーページを表示したい
- 誰にもcatchされなかった例外も、ログを吐いてエラーページを表示したい
- 処理が続行可能なエラーはログだけ吐いて処理を続けたい
- @演算子でエラーを抑制している場合は、ログに残らないようにしたい
- エラーハンドラが宣言される前におきた処理続行可能なエラーも、後からログに残しておきたい
注意事項
PHP7未満ではエラーハンドリングの動作が大きく違うので、注意が必要です。
Fatal Errorが発生したら
Fatal Errorが発生したら、PHPは処理を続けることができないので、処理をシャットダウンしにかかります。このとき、事前にregister_shutdown_function()
で終了処理を登録しておくことで最後のひと足掻きができます。終了処理内でエラーを拾って、ログ出力とエラーページの表示を行います。
こんなイメージ
<?php
register_shutdown_function(function(){
$lastError = error_get_last();
putLog($lastError);
if (isFatal($lastError['type'])) {
showErrorPage();
}
});
例外が誰にもキャッチされなかったら
throwした例外が誰にもcatchされなかった場合は、最終的にset_exception_handler()
で登録しておいた例外ハンドラに拾われます。なので、ここに終了処理を書いておけばOKです。
こんなイメージ
<?php
set_exception_handler(function(Throwable $throwable){
putLog($throwable);
showErrorPage();
});
エラーが発生したがFatalではなかった場合
エラーが発生した場合、事前にset_error_handler()
でエラーハンドラを登録しておけば、エラー処理を行うことができます。Fatalではないエラーの場合、エラー処理が完了すると、プログラムは元に戻って処理を続行します。エラーハンドラがfalse以外を返した場合は、PHP標準のエラー処理は完全にバイパスされます。
こんなイメージ
<?php
set_error_handler(function($severity, $message, $file, $line){
putLog($severity, $message, $file, $line);
return true;
});
エラーが発生したが@演算子でエラーが抑制されていた場合
@演算子でエラーを抑制していても、set_error_handler()
でエラーハンドラが登録されていた場合は、エラー処理が実行されます。@演算子に頼らないのが一番ですが、エラーログを吐かないようにあえて@演算子をつけたい場合もないわけではありません。
そこで、エラーハンドラ内で@演算子がついている場合はログを吐かないように制御します。これにはerror_reporting()
関数を使います。@演算子が効いている間はこの関数の戻り値が0になります。
こんなイメージ
<?php
set_error_handler(function($severity, $message, $file, $line){
if (error_reporting()) {
putLog($severity, $message, $file, $line);
}
return true;
});
エラーハンドラが宣言される前にエラーがおきた場合
error_get_last()
関数を使うことで、最後におきたエラーを取得することができます。エラーハンドラを宣言するときについでにこの関数を呼ぶことで、確実にエラーログに残せるようになります。
全部を網羅したさいきょうのエラーハンドラ
<?php
class ErrorHandler
{
private $lastMessage = '';
/**
* ErrorHandler constructor.
*/
public function __construct()
{
// エラーハンドラが宣言される前にエラーが起こっていたらログに残す
$lastError = error_get_last();
if ($lastError) {
$logMessage = self::buildMessageForPHPError($lastError);
$this->putLog($lastError['type'], $logMessage, $lastError['file'], $lastError['line']);
$this->lastMessage = $lastError['message'];
}
set_exception_handler([$this, 'exceptionHandler']);
set_error_handler([$this, 'errorHandler']);
register_shutdown_function([$this, 'onShutdown']);
}
/**
* ここに来る時は処理続行不可能なので、
* エラーログを吐いてエラーページを表示する
*
* @param Throwable $throwable
*/
public function exceptionHandler(Throwable $throwable)
{
$logMessage = self::buildMessageForThrowable($throwable);
// ここの$severityはログの出力の判定に使うイメージ
// 例外は処理続行できないので、ログ出力のレベルはE_ERRORと同等
$this->putLog(E_ERROR, $logMessage, $throwable->getFile(), $throwable->getLine());
$this->lastMessage = $throwable->getMessage();
$this->showErrorPage();
}
/**
* @param int $severity
* @param string $message
* @param string $file
* @param int $line
* @return bool
* @throws ErrorException
*/
public function errorHandler($severity, $message, $file, $line)
{
if (self::isFatal($severity)) {
// FATAL ERRORは処理の続行が不可能なので、exceptionHandler()に処理を引き継ぐ
// ただし、おそらくここを通ることはなく、
// FATAL ERRORが発生する場合は直接onShutdownに行くと思われる
throw new ErrorException($message, 0, $severity, $file, $line);
}
// @演算子でエラーを抑制している場合は error_reporting() の返り値が0になるので、エラーを出力しない
if (error_reporting()) {
$logMessage = self::buildMessageForPHPError(
['type' => $severity, 'message' => $message, 'file' => $file, 'line' => $line]
);
$this->putLog($severity, $logMessage, $file, $line);
}
// @演算子でエラーを抑制した場合でも
// Shutdownハンドラ内で error_get_last() を呼び出すとエラーが取れてしまう。
// その場合にエラーレポートしないよう、 @演算子の有無にかかわらず$messageは保存しておく
$this->lastMessage = $message;
// trueを返すとPHPのエラーハンドラはバイパスされる
return true;
}
/**
* @return void
*/
public function onShutdown()
{
$lastError = error_get_last();
if (empty($lastError)) {
return;
}
if ($this->lastMessage == $lastError['message']) {
// すでにこのエラーはログ出力済みなので何もしない
return;
}
$logMessage = self::buildMessageForPHPError($lastError);
$this->putLog($lastError['type'], $logMessage, $lastError['file'], $lastError['line']);
// 処理続行不可能なE_ERRORが発生して、errorHandler()が拾えなかった場合
// ここにくる
if (self::isFatal($lastError['type'])) {
$this->showErrorPage();
}
}
/**
* @return void
*/
private function showErrorPage()
{
//バッファをOFFにする
while (ob_get_level() > 0) {
ob_end_flush();
}
/** @noinspection PhpIncludeInspection */
require '../public/500.php';
}
/**
* @param int $severity
* @param string $message
* @param string $file
* @param int $line
* @return void
*/
private function putLog($severity, $message, $file, $line)
{
// TODO ログ出力
}
/**
* プログラムの実行が中断される重大なエラーかどうか
*
* @param int $severity
* @return bool
*/
private static function isFatal($severity)
{
return boolval($severity & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR));
}
/**
* @param array $errorDetail
* @return string
*/
public static function buildMessageForPHPError($errorDetail)
{
$message = ''; // TODO エラーメッセージを組み立てる
return $message;
}
/**
* @param Throwable $throwable
* @return string
*/
public static function buildMessageForThrowable($throwable)
{
$message = ''; // TODO エラーメッセージを組み立てる
return $message;
}
}