問題
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
の情報もチェーンして残したい!というニーズはあると思います。
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