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

# 問題

Laravel の [`AuthenticationException`] は，引数に例外をチェーンするための `$previous` を取れない！

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

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

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

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

[`AuthenticationException`]: https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/AuthenticationException.php
[`GuzzleException`]: https://github.com/guzzle/guzzle/blob/master/src/Exception/GuzzleException.php

# 実装方法

## リフレクションを使う 😓

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

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

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

- https://3v4l.org/0ct5l

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

このようなクラスの場合

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

で

```
Fatal error: Uncaught ReflectionException: Property previous does not exist
```

となってしまう。<del>よってこれはボツ。</del>

[<ins>コメントに解決策を追記しました</ins>](https://qiita.com/mpyw/items/7f5a9fe6472f38352d96#comment-07788783bd85a3c1c57a)

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

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

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

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

- https://3v4l.org/4scuq

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

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

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

- [Zend VMにおける例外の実装](https://www.slideshare.net/hnw/zend-vm)

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

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

- https://3v4l.org/BZUJr

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

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

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

と書いたときに

 「 `$a` に `$b` をチェーンして，**さらにその後ろに `$c` をチェーン**」

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

# 付録

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

```php
<?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 のネタにも繋げる予定です…ｗ