18
9

More than 1 year has passed since last update.

$previous 引数を取らない例外クラスに強引に例外をチェーンする裏ワザ

Last updated at Posted at 2019-12-05

問題

Laravel の AuthenticationException は,引数に例外をチェーンするための $previous を取れない!

public function __construct($message = 'Unauthenticated.', array $guards = [], $redirectTo = null)
{
    parent::__construct($message);
    $this->guards = $guards;
    $this->redirectTo = $redirectTo;
}

例えば… 認証用の外部の API に Guzzle でリクエストを飛ばして, 401 エラーだったら AuthenticationException を投げる,というときにデバッグしやすいように GuzzleException の情報もチェーンして残したい!というニーズはあると思います。

Guzzle の API 認証の例
try {
    $client->post('/api/auth', ['form_params' => compact('token')]);
} catch (ClientException $e) {
    if ($e->getResponse()->getStatusCode() === 401) {
        throw new AuthenticationException(); // ← ここに $e の情報も残したい!!!
    }
}

この目的を達成するためにいくつか解法を挙げてみます。

実装方法

リフレクションを使う 😓

オーソドックス実装方法なその1。リフレクションで protected $previous に穴を開けて強引にアクセスする方法。

function setPrevious(Throwable $e, Throwable $previous): Throwable
{
    $reflection = new ReflectionClass($e);
    $prop = $reflection->getProperty('previous');
    $prop->setAccessible(true);
    $prop->setValue($e, $previous);
    return $e;
}

これで十分じゃん!という気もするが,リフレクションでは そのクラス自身に存在していないプロパティを取得できない

class SimpleException extends Exception
{
    public function __construct($message)
    {
        parent::__construct($message);
    }
}

このようなクラスの場合

$prop = $reflection->getProperty('previous');

Fatal error: Uncaught ReflectionException: Property previous does not exist

となってしまう。よってこれはボツ。

コメントに解決策を追記しました

クロージャの動的な $this を使う ☠

オーソドックス実装方法なその2。クロージャの $this を変更して protected $previous に強引にアクセスする方法。

function setPrevious(Throwable $e, Throwable $previous): Throwable
{
    return (function () use ($e, $previous) {
        $e->previous = $previous;
        return $e;
    })->call($e);
}

一見動くように見えるが,何故か $e->getPrevious() で親に存在している $e->previous を取得できない。

バグっぽいけど,動かない理由はさっきのリフレクションと同じで親に存在しているからだと思われる。よってこれもボツ。

finally の特異的な性質を利用する ✨

先日の PHP カンファレンス 2019 で @hnw さんに紹介していただいた変態的な方法です。まさかネタが実際に役立つとは思ってなかったよ…しかもこれが唯一の解決策かよ…

function setPrevious(Throwable $e, Throwable $previous): Throwable
{
    try {
        try {
            throw $previous;
        } finally {
            throw $e;
        }
    } catch (Throwable $e) {
        return $e;
    }
}

これもう分かんねえな。でもちゃんと動くんです。

この方法の特筆すべき点は

$a = new SimpleException('A');
$b = new SimpleException('B');
$c = new SimpleException('C');

setPrevious($a, $b);
setPrevious($a, $c);

と書いたときに

$a$b をチェーンして,さらにその後ろに $c をチェーン

という動きをしてくれるところがあります。上書きせずに追加してくれるのは素晴らしい。

付録

Laravel 向けの実装置いておきます。コピペでご自由にどうぞ。

<?php

namespace App\Exceptions;

use Throwable;

/**
 * Trait SetsPrevious
 *
 * $previous 引数を取らない例外クラスに例外をチェーンする裏ワザ
 *
 * @see https://www.slideshare.net/hnw/zend-vm
 * @see https://qiita.com/mpyw/items/7f5a9fe6472f38352d96
 */
trait SetsPrevious
{
    /**
     * @param Throwable $e
     * @param Throwable $previous
     * @return mixed|Throwable
     */
    public static function setPrevious(Throwable $e, Throwable $previous): Throwable
    {
        try {
            try {
                throw $previous;
            } finally {
                throw $e;
            }
        } catch (Throwable $e) {
            return $e;
        }
    }
}

これを使って 1 枚目の Advent Calendar のネタにも繋げる予定です…w

18
9
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
18
9