Kodokon kodokon.com

例外:階層、finally、グローバルハンドラー

完全なエラー戦略を組み立てましょう。連鎖させた業務例外、制御されたfinally、そして最後の防衛線としてのグローバルハンドラーです。

9 分 · 3 問

このレッスンを Kodokon で開く

PHP 7以降、投げられるものはすべてThrowableを実装し、二つの枝に分かれます。ErrorTypeErrorのような、エンジンや型付けの失敗)と、Exception(アプリケーションの失敗)です。Errorはプログラムの境界でだけ捕まえます。アプリケーションの側では、SPLがすぐに使える階層を提供します。LogicExceptionは修正すべき開発者のバグを、RuntimeExceptionは対処すべき実行時の不測の事態を知らせます。プロの反射はこうです。業務例外をこれらの土台から派生させ、見分けのつく状況ごとに一つ作るのです。

PHP
<?php

declare(strict_types=1);

abstract class AppException extends RuntimeException
{
}

final class UserNotFound extends AppException
{
    public static function withEmail(
        string $email
    ): self {
        return new self("No user for $email");
    }
}

try {
    throw UserNotFound::withEmail('lea@example.com');
} catch (UserNotFound $e) {
    echo $e->getMessage();
}
業務の階層:レベルに応じてUserNotFound、AppException、RuntimeExceptionのどれかを捕まえる

try/catchは、そのエラーをどう扱えばよいかわかっているときにだけ意味があります。さもなければ、そのまま上へ伝わらせましょう。三つの道具が制御を洗練させます。マルチキャッチのcatch (JsonException | ValueError $e)は同じ扱いをまとめます。previousパラメーターは、技術的な例外を業務例外に翻訳するときに、元の原因を連鎖させます。finallyはどんな場合でも実行されます。成功、例外、早期のreturnのときでさえも。リソースを解放するのに理想的です。

PHP
<?php

declare(strict_types=1);

final class PayloadInvalid extends RuntimeException
{
}

function decode(string $json): mixed
{
    try {
        return json_decode(
            $json,
            true,
            flags: JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $e) {
        throw new PayloadInvalid(
            'Invalid JSON payload',
            previous: $e,
        );
    } finally {
        echo "decode() finished\n";
    }
}

try {
    decode('{broken');
} catch (PayloadInvalid $e) {
    echo $e->getPrevious()?->getMessage();
}
例外の翻訳:技術的な原因はgetPrevious()を通じてアクセス可能なまま

連鎖の終わりでは、二つのグローバルハンドラーが仕上げを担います。set_exception_handlerは、処理されなかったすべてを捕まえます。構造化されたログ、汎用の500レスポンス、それもメッセージやトレースをユーザーに一切さらすことなく、です。set_error_handlerは、古い形式のエラー(警告や通知)をErrorExceptionに変換し、二つの世界を統一します。すべてが例外になり、すべてが同じ処理の道筋をたどります。

PHP
<?php

declare(strict_types=1);

set_error_handler(function (
    int $severity,
    string $message,
    string $file,
    int $line,
): bool {
    throw new ErrorException(
        $message,
        0,
        $severity,
        $file,
        $line,
    );
});

set_exception_handler(function (Throwable $e): void {
    error_log($e->getMessage());
    http_response_code(500);
    echo 'A technical error occurred.';
});

trigger_error('Legacy warning', E_USER_WARNING);
echo 'Never reached';
警告が例外になり、捕まえられないまま:グローバルハンドラーが応答し、そのあとスクリプトが止まる

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. モダンなPHPで、ErrorExceptionに共通するものは何ですか?
    • ErrorExceptionを継承している
    • どちらもThrowableインターフェースを実装している
    • どちらもPHPエンジンのために予約されている
  2. finallyブロックはいつ実行されますか?
    • 例外が投げられなかったときだけ
    • catchのあとだけ
    • どんな場合でも:成功、捕まえられたかどうかを問わない例外、早期のreturn
  3. 例外のpreviousパラメーターは何のためのものですか?
    • より高いレベルの例外に翻訳するときに、元の例外を保つため
    • 例外がコールスタックを上へ伝わるのを防ぐため
    • 次の失敗のときに投げる例外を定めるため