PHP
PHP7

ぼくのかんがえたさいきょうのPHPエラーハンドリング


やりたいこと


  • 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;
}
}