Laravelコア解読--異常処理

10426 ワード

異常処理はプログラミングにおいて非常に重要であるが、最も無視されやすい言語特性であり、開発者にプログラム実行時のエラーを処理するメカニズムを提供し、プログラム設計にとって正しい異常処理はプログラム自身の詳細をユーザーに漏らすことを防止し、開発者に完全なエラー遡及スタックを提供し、同時にプログラムの丈夫性を高めることができる.
この記事では、Laravelで提供されている例外処理能力を簡単に整理し、開発で例外処理を使用する実践、カスタム例外を使用する方法、Laravelの例外処理能力を拡張する方法について説明します.
異常Handlerの登録
ここではまた、Kernelが要求を処理する前のbootstrapフェーズに戻り、bootstrapフェーズのIlluminate\Foundation\Bootstrap\HandleExceptionsセクションにLaravelがシステム異常処理動作を設定し、グローバルな異常プロセッサを登録します.
class HandleExceptions
{
    public function bootstrap(Application $app)
    {
        $this->app = $app;

        error_reporting(-1);

        set_error_handler([$this, 'handleError']);

        set_exception_handler([$this, 'handleException']);

        register_shutdown_function([$this, 'handleShutdown']);

        if (! $app->environment('testing')) {
            ini_set('display_errors', 'Off');
        }
    }
    
    
    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
}
set_exception_handler([$this, 'handleException']) HandleExceptionshandleExceptionメソッドをプログラムのグローバルプロセッサメソッドとして登録します.
public function handleException($e)
{
    if (! $e instanceof Exception) {
        $e = new FatalThrowableError($e);
    }

    $this->getExceptionHandler()->report($e);

    if ($this->app->runningInConsole()) {
        $this->renderForConsole($e);
    } else {
        $this->renderHttpResponse($e);
    }
}

protected function getExceptionHandler()
{
    return $this->app->make(ExceptionHandler::class);
}

//   CLI       
protected function renderForConsole(Exception $e)
{
    $this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e);
}

//   HTTP       
protected function renderHttpResponse(Exception $e)
{
    $this->getExceptionHandler()->render($this->app['request'], $e)->send();
}

プロセッサでは主にExceptionHandlerreport方法で異常を報告し、ここではstorage/laravel.logファイルに異常を記録し、要求タイプレンダリング異常の応答に基づいてクライアントに出力する.ここでExceptionHandlerは、プロジェクトが最初にサービスコンテナに登録された\App\Exceptions\Handlerクラスの例です.
// bootstrap/app.php

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
*/

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
*/
......

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

ちなみにset_error_handler関数は、エラープロセッサ関数を登録する役割を果たしています.いくつかの古いコードやクラスライブラリではPHPの関数trigger_error関数を用いてエラーを投げ出すことが多いため、異常プロセッサはExceptionを処理するだけでErrorを処理できません.したがって、従来のクラスライブラリと互換性を持たせるために、set_error_handlerを使用してグローバルなエラープロセッサメソッドを登録し、メソッドでエラーをキャプチャした後、エラーを異常に変換して再放出することで、プロジェクト内のすべてのコードが正しく実行されていない場合に例外インスタンスを放出することができます.
/**
 * Convert PHP errors to ErrorException instances.
 *
 * @param  int  $level
 * @param  string  $message
 * @param  string  $file
 * @param  int  $line
 * @param  array  $context
 * @return void
 *
 * @throws \ErrorException
 */
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
    if (error_reporting() & $level) {
        throw new ErrorException($message, 0, $level, $file, $line);
    }
}

一般的なLaravel例外の例Laravelでは、一般的なプログラム異常に対して対応する異常例が投げ出され、開発者はこれらの実行時異常をキャプチャし、必要に応じて後続の処理を行うことができる(例えば、catchで別の救済方法を呼び出し、異常をログファイルに記録し、アラームメールを送信し、メールを送信する)
ここで私はいくつかの開発でよく異常に遭遇することを列挙し、彼らがどのような状況で投げ出されたのかを説明し、普段の符号化ではプログラムの中でこれらの異常をキャプチャして異常処理をしてこそ、プログラムをより丈夫にすることができることに注意しなければならない.
  • Illuminate\Database\QueryException LaravelでSQL文を実行中にエラーが発生した場合に放出されるこの異常は、使用率が最も高い異常であり、SQL実行エラーをキャプチャするために使用されます.例えば、Update文を実行する場合、SQL実行後に変更された行数を判断してUPDATEが成功したかどうかを判断するのが好きな人が多いですが、実行されたUPDATE文には記録値が変更されていない場合もあります.この場合、関数を修正することでUPDATEが成功したかどうかを判断することはできません.また、トランザクション実行中にQueryExceptionがキャプチャされた場合、catchコードブロックでトランザクションをロールバックできます.
  • Illuminate\Database\Eloquent\ModelNotFoundExceptionモデルのfindOrFailおよびfirstOrFailメソッドによって単一レコードが取得された場合、この例外が見つからない場合は放出されます(findおよびfirstデータが見つからない場合はNULLに戻ります).
  • Illuminate\Validation\ValidationExceptionは、LaravelのFormValidator検証に合格していないことを要求すると、この例外を放出します.
  • Illuminate\Auth\Access\AuthorizationExceptionユーザがLaravelのポリシー(Policy)検証を通過しないことを要求すると、この例外
  • が投げ出される.
  • Symfony\Component\Routing\Exception\MethodNotAllowedExceptionルーティング要求時にHTTP Methodが正しくない
  • .
  • Illuminate\Http\Exceptions\HttpResponseException Laravelの処理HTTP要求が成功しない場合にこの異常
  • を投げ出す.
    Laravelを拡張する例外プロセッサ
    Laravelは\App\Exceptions\Handlerをグローバルな例外プロセッサに登録しましたが、コードにはcatchからの例外はなく、最後に\App\Exceptions\Handlerにキャプチャされ、プロセッサはまず例外をログファイルに記録してから異常応答をレンダリングしてクライアントに応答します.しかし、独自の例外プロセッサの方法は使いにくく、メールやエラーログシステムに異常を報告したい場合が多い.次の例は、異常をSentryシステムに報告し、Sentryはエラー収集サービスで非常に使いやすい.
    public function report(Exception $exception)
    {
        if (app()->bound('sentry') && $this->shouldReport($exception)) {
            app('sentry')->captureException($exception);
        }
    
        parent::report($exception);
    }

    また、デフォルトのレンダリング方法では、フォーム検証時に応答を生成するJSONフォーマットは、私たちのプロジェクトで統一されたJOSNフォーマットとは異なり、レンダリング方法の動作をカスタマイズする必要があります.
    public function render($request, Exception $exception)
    {
        //         JSON  ,   API     Validator    ValidationException 
        //              .
        if ($exception instanceof ValidationException && $request->expectsJson()) {
            return $this->error(422, $exception->errors());
        }
    
        if ($exception instanceof ModelNotFoundException && $request->expectsJson()) {
            //                      NotFoundHttpException
            return $this->error(424, 'resource not found.');
        }
    
    
        if ($exception instanceof AuthorizationException) {
            //            AuthorizationException
            return $this->error(403, "Permission does not exist.");
        }
    
        return parent::render($request, $exception);
    }

    カスタマイズ後、要求がFormValidatorの検証を通過しない場合、ValidationExceptionが放出され、その後、異常プロセッサが異常をキャプチャすると、エラープロンプトがプロジェクト統一のJSON応答フォーマットにフォーマットされ、クライアントに出力されます.これにより,我々のコントローラでは,フォーム検証がクライアントに再出力エラーで応答しない論理であるか否かを判断することを完全に省略し,この論理を統一した異常プロセッサに渡して実行することでコントローラ手法を大幅に痩せることができる.
    カスタム例外の使用
    この部分は、Laravelフレームワークのカスタム例外ではなく、ここで説明したカスタム例外をどのプロジェクトにも適用できます.RepositoryServiceのような方法では、異なるエラーに基づいて異なる配列を返す人が多いのを見たことがあります.応答のエラーコードやエラー情報が含まれています.これはもちろん開発ニーズを満たすことができますが、異常が発生した場合のアプリケーションの実行時コンテキストを記録することはできません.エラーが発生した場合、コンテキスト情報を記録できないと、開発者の問題の位置づけに非常に不利です.
    次はカスタム例外クラスです
    namespace App\Exceptions\;
    
    use RuntimeException;
    use Throwable;
    
    class UserManageException extends RuntimeException
    {
        /**
         * The primitive arguments that triggered this exception
         *
         * @var array
         */
        public $primitives;
        /**
         * QueueManageException constructor.
         * @param array $primitives
         * @param string $message
         * @param int $code
         * @param Throwable|null $previous
         */
        public function __construct(array $primitives, $message = "", $code = 0, Throwable $previous = null)
        {
            parent::__construct($message, $code, $previous);
            $this->primitives = $primitives;
        }
    
        /**
         * get the primitive arguments that triggered this exception
         */
        public function getPrimitives()
        {
            return $this->primitives;
        }
    }

    例外クラスを定義したら、コードロジックに例外インスタンスを投げ出すことができます.
    class UserRepository
    {
      
        public function updateUserFavorites(User $user, $favoriteData)
        {
            ......
            if (!$executionOne) {
                throw new UserManageException(func_get_args(), 'Update user favorites error', '501');
            }
            
            ......
            if (!$executionTwo) {
                throw new UserManageException(func_get_args(), 'Another Error', '502');
            }
            
            return true;
        }
    }
    
    class UserController extends ...
    {
        public function updateFavorites(User $user, Request $request)
        {
            .......
            $favoriteData = $request->input('favorites');
            try {
                $this->userRepo->updateUserFavorites($user, $favoritesData);
            } catch (UserManageException $ex) {
                .......
            }
        }
    }

    上記のRepositoryに加えて、上記の汎用例外をキャプチャした後、catchコードブロックにビジネスに関連するより細分化された例外インスタンスを投げ出して開発者の位置決めを容易にする問題が多いので、上記のupdateUserFavoritesをこのようなポリシーに従って修正します.
    public function updateUserFavorites(User $user, $favoriteData)
    {
        try {
            // database execution
            
            // database execution
        } catch (QueryException $queryException) {
            throw new UserManageException(func_get_args(), 'Error Message', '501' , $queryException);
        }
    
        return true;
    }

    上記でUserMangeExceptionクラスを定義したときの4番目のパラメータ$previousは、Throwableインタフェースクラスを実装したインスタンスであり、このシナリオでは、QueryExceptionの異常インスタンスをキャプチャしたためにUserManagerExceptionのインスタンスを投げ出し、このパラメータによってQueryExceptionインスタンスをPHPの異常スタックに渡し、これは、現在投げ出されている例外インスタンスだけのコンテキスト情報ではなく、例外全体を遡る能力を提供し、エラー収集システムでは、次のようなコードを使用してすべての例外の情報を取得することができます.
    while($e instanceof \Exception) {
        echo $e->getMessage();
        $e = $e->getPrevious();
    }

    異常処理はPHPの非常に重要であるが、開発者に無視されやすい機能であり、この文章はLaravel内部の異常処理のメカニズムとLaravel異常処理を拡張する方法を簡単に説明した.もっと多くの紙面はいくつかの異常処理のプログラミング実践を重点的に分かち合って、これらはまさに私がすべての読者がすべて見て実践することができることを望んでいるいくつかのプログラミング習慣で、前に分かち合ったInterfaceの応用も同様です.