0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHP(Laravel)のそのコード、本当に例外であってますか?

0
Posted at

はじめに

みなさんはPHPで例外処理を書く場合、どのように書いていますか?
仕様やtry句に書いている処理による部分はあるとは思いますが、だいたいこんな感じですよね。

try {
  // 処理...
} catch (OutOfBoundsException $e) {
  logger()->error('$e', $e->getMessage());
} catch (Exception $e) {
  logger()->error('$e', $e->getMessage());
}

例外処理ではtryの中で処理が失敗したらcatchで設定しているExceptionに該当するcatchの中に入り、その処理を実行するようになっています。
今回は後述するコードで、ログにERRORが出力されているけど、Exceptionに入らず、ただただ500エラーで落ちるという現象が発生しました。
「tryで括っているのになぜ?」と調べていった結果原因が判明したので、学習内容の記録かつ「みんなも気をつけてね〜」という注意喚起で投稿します。

発生したエラー

今回実行したコードはこちら
※ここでは故意的にエラーが発生するようcombineに固定値を設定しています。

try {
  logger()->info('tryに入りました。');
  $collection = collect(['name', 'age'])->combine(['maro', 25, 'man'])->all();
  logger()->info('$collection', $collection);
} catch (\Exception $e) {
  logger()->info('catchに入りました。');
  logger()->error($e->getMessage());
}

画面に表示されたエラーはこちら
php-error-or-exception1.png
catch句に入らずに上記画面が表示されて500エラーが発生して落ちていました。

collection()->combine()で要素の数が合わないよという内容ですね。

array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements

tryの中で発生したエラーとかってcatchに入るんじゃなかったっけ?

なぜExceptionが発生しないのか

これを理解するためにまずは例外エラーの違いについて振り返りましょう。

例外とは

予測可能で回復が可能な問題。
プログラム自体の処理に起因するもの。

原因としては、以下が該当する

  • 無効な入力
  • ファイルが見つからない
  • ネットワーク接続の切断
  • 配列の範囲外アクセス
  • ゼロ除算
    など。

例外処理として、try-catchブロックを使用して補足し、適切に処理することでプログラムの実行を継続できるようにする。
つまり、HTTPステータス500エラーなどでシステムエラーになることがなくなるということ。

エラーとは

予測不可能または致命的な問題。
PHPの内部構造で発生することが多い。
エラーハンドラーで捕捉できるものもあるけど、致命的なエラーは捕捉できず、スクリプトの実行を停止させる。

Exceptionが発生しない原因

結論から言うと、PHPの仕様によるものでした。

今回発生していたエラーはValueErrorなので、分類としてはエラーになります。例外ではありません。
なのでcatchExceptionを指定していても例外として扱われないのです。

じゃあどう対策したらいいのか?
自分なりに考察してみました。
※あくまでサンプルなのでそれぞれのログレベルなどは無視してください。

パターン1

Exeption処理が走る前に直接ValueErrorの場合に入るcatchを追加します。
これをすることで、要素数が異なりますよというエラーが発生した場合はログに「ValueErrorに入りました。」とgetMessageした内容が出力されるだけです。

try {
  logger()->info('tryに入りました。');
  $collection = collect(['name', 'age'])->combine(['maro', 25, 'man'])->all();
  logger()->info('$collection', $collection);
+ } catch (\ValueError $e) {
+   logger()->info('ValueErrorに入りました。');
+   logger()->error($e->getMessage());
} catch (\Exception $e) {
  logger()->info('Exceptionに入りました。');
  logger()->error($e->getMessage());
}

こちらがコードを実行して出力されたログです。
ちゃんとValueErrorを設定したcatchの処理が実行されていますね。
php-error-or-exception2.png

パターン2

\Exceptionの後に\Errorを追加しました。
Exceptionは全ての例外の基底クラスです。
それに対しErrorは全ての内部エラーの基底クラスです。
なので、例外じゃないValueErrorが発生した場合はエラーの規定クラスであるErrorをつけたcatchを作ればいいじゃんと考えました。

try {
  logger()->info('tryに入りました。');
  $collection = collect(['name', 'age'])->combine(['maro', 25, 'man'])->all();
  logger()->info('$collection', $collection);
} catch (\Exception $e) {
  logger()->info('Exceptionに入りました。');
  logger()->error($e->getMessage());
+ } catch (\Error $e) {
+   logger()->info('Errorに入りました。');
+   logger()->error($e->getMessage());
}

どっちがいいの? そもそも考え方は正しいの?

AIに聞いてみました。

プロンプト(長いので畳みます)
# 質問
PHP(Laravel)で{# 実行コード}を実行したら{# エラー}が発生しました。
`collect()->combine()`で要素数が一致しないことが原因という部分までは理解しましたが、対応策としてどう打ち出すべきか迷っています。
PHPの書籍には「一般的にErrorクラスは実行を継続できない致命的なエラーを意味するので、例外を処理する意味はありませんし、通常のアプリが捕捉すべきではありません。それでもどうしてもErrorクラスを捕捉したい場合には、Errorをcatch句に指定してください。」というようなニュアンスの記載があったためif文で分岐させて`$collection`がfalseなどであれば処理を終わらすやスキップするなどの処理が必要なのかと考えています。
一旦自分なりの考察として{## 修正案1}と{## 修正案2}を記載したので、確認してください。

# 実行コード
'''
try {
  logger()->info('tryに入りました。');
  $collection = collect(['name', 'age'])->combine(['maro', 25, 'man'])->all();
  logger()->info('$collection', $collection);
} catch (\Exception $e) {
  logger()->info('catchに入りました。');
  logger()->error($e->getMessage());
}
'''

# エラー
'''
array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements
'''

# 修正案

## 修正案1

Exeption処理が走る前に直接`ValueError`の場合に入る`catch`を追加します。
これをすることで、要素数が異なりますよというエラーが発生した場合はログに「ValueErrorに入りました。」とgetMessageした内容が出力されるだけです。
'''
try {
  logger()->info('tryに入りました。');
  $collection = collect(['name', 'age'])->combine(['maro', 25, 'man'])->all();
  logger()->info('$collection', $collection);
} catch (\ValueError $e) {
  logger()->info('ValueErrorに入りました。');
  logger()->error($e->getMessage());
} catch (\Exception $e) {
  logger()->info('Exceptionに入りました。');
  logger()->error($e->getMessage());
}
'''


## 修正案2

`\Exception`の後に`\Error`を追加しました。
Exceptionは全ての**例外の基底クラス**です。
それに対しErrorは全ての**内部エラーの基底クラス**です。
なので、例外じゃないValueErrorが発生した場合はエラーの規定クラスである`Error`をつけたcatchを作ればいいじゃんと考えました。
'''
try {
  logger()->info('tryに入りました。');
  $collection = collect(['name', 'age'])->combine(['maro', 25, 'man'])->all();
  logger()->info('$collection', $collection);
} catch (\Exception $e) {
  logger()->info('Exceptionに入りました。');
  logger()->error($e->getMessage());
} catch (\Error $e) {
  logger()->info('Errorに入りました。');
  logger()->error($e->getMessage());
}
'''
ChatGPTからの返答(長いので畳みます)
# Laravel(PHP)で collect()->combine() 実行時に発生する ValueError の考え方と対応方針

## 結論(要約)
- 今回のエラーは **catchで握りつぶすより、事前に要素数を整える設計が基本**
- どうしても回復処理が必要な場合のみ **ValueError を狙い撃ちで catch**
- `Error` を丸ごと catch するのは原則おすすめしない

---

## なぜ catch (\Exception $e) では捕まらないのか

PHP 7以降の例外階層は以下の構造になっています。

    Throwable
    ├─ Exception   (アプリケーション例外)
    └─ Error       (PHP内部・言語仕様エラー)
        └─ ValueError

- `array_combine()` は PHP 8.0 以降、  
  **キー数と値数が一致しない場合に ValueError を throw**
- `ValueError` は `Exception` ではなく **`Error` 系**

そのため、以下では捕まらない:

    catch (\Exception $e) {
        // ValueError はここに来ない
    }

---

## 修正案1の評価(ValueError を catch)

    try {
        $collection = collect(['name', 'age'])
            ->combine(['maro', 25, 'man'])
            ->all();
    } catch (\ValueError $e) {
        logger()->info('ValueErrorに入りました。');
        logger()->error($e->getMessage());
    } catch (\Exception $e) {
        logger()->info('Exceptionに入りました。');
        logger()->error($e->getMessage());
    }

### 評価
- ✅ 今回の原因にピンポイントで対応できている
- ✅ 「このエラーだけは回復する」という意図が明確
- ⚠ ただし **本来は事前チェックの方が望ましい**

---

## 修正案2の評価(Error を catch)

    try {
        $collection = collect(['name', 'age'])
            ->combine(['maro', 25, 'man'])
            ->all();
    } catch (\Exception $e) {
        logger()->info('Exceptionに入りました。');
        logger()->error($e->getMessage());
    } catch (\Error $e) {
        logger()->info('Errorに入りました。');
        logger()->error($e->getMessage());
    }

### 評価
- ✅ ValueError も捕まる
- ⚠ `Error` は範囲が広すぎる  
  → 本当に致命的なバグまで握りつぶす可能性あり
- ❌ 「combineの入力不整合だけ回復したい」用途には過剰

---

## 推奨方針:例外を起こさない設計

### 推奨案A:要素数チェックしてスキップ(最も安全)

    $keys   = ['name', 'age'];
    $values = ['maro', 25, 'man'];

    if (count($keys) !== count($values)) {
        logger()->warning('combine skipped: count mismatch', [
            'keys_count' => count($keys),
            'values_count' => count($values),
        ]);
        return; // または continue
    }

    $collection = collect($keys)->combine($values)->all();

✔ 想定内エラーは try-catch より if 分岐が読みやすい  
✔ ロジック上の前提条件が明確になる  

---

### 推奨案B:仕様が決まっているなら整形してから combine

#### 例:values が多い場合は切り捨てる

    $keys   = ['name', 'age'];
    $values = ['maro', 25, 'man'];

    $values = array_slice($values, 0, count($keys));

    $collection = collect($keys)->combine($values)->all();

※ `null` を埋める、デフォルト値を入れる等も仕様次第

---

## try-catch を使うならこの形が実務向け

### 回復可能な ValueError だけ捕まえる

    try {
        $collection = collect(['name', 'age'])
            ->combine(['maro', 25, 'man'])
            ->all();
    } catch (\ValueError $e) {
        logger()->warning('combine failed (recoverable)', [
            'message' => $e->getMessage()
        ]);
        return;
    }

---

### 想定外もログに残したい場合(Throwable)

    try {
        $collection = collect(['name', 'age'])
            ->combine(['maro', 25, 'man'])
            ->all();
    } catch (\ValueError $e) {
        logger()->warning('combine failed (recoverable)', [
            'message' => $e->getMessage()
        ]);
    } catch (\Throwable $t) {
        logger()->error('unexpected throwable', [
            'message' => $t->getMessage()
        ]);
        throw $t; // 基本は上位に投げる
    }

---

## 判断基準まとめ

- **仕様として起こり得る?**  
  → if 分岐で事前チェック
- **起きたら即バグ?**  
  → 握りつぶさず落とす
- **catch するなら狙い撃ち**  
  → 今回は `ValueError`

対応策

例外ではないもの(Error)はtry-catchでcatchするのはおすすめしないようです。
全てのエラーをcatchで拾ってしまうと、本来拾うべきでない致命的なエラーも拾われてしまい、後続処理に影響が出る状況になりやすいと。壊れていない前提で実行する処理なのに壊れたデータが来たら致命的なことになりかねません。

復旧可能かどうかを判断し、復旧させても良いと判断した特定のエラーValueErrorなどをピンポイントでcatchするならまだ良いと思いましたが、事前チェックとしてif文でチェックしてthrowすることが一番安全そうですね。

最後に

私も保守運用とはいえ、結構長い時間プログラミングをしてきたのに、こんなの常識だろうという知識を今頃つけるとは...とショックを受けています。
とはいえ結局自分が理想とする像を実現させるためには前を向く以外方法はないので、ひたすら知識をブラッシュアップしていければと思います。

上を向いて歩こう。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?