19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

やりたいこと

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

19
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?