[PHP] まとめて例外をスローする小技

More than 5 years have passed since last update.


基本編

簡略化のために die を用いて説明するが、例えば入力フォームでバリデーションを行うとき、

try {

if ($name === '') {
throw new Exception('名前が未入力です');
}
if ($email === '') {
throw new Exception('メールアドレスが未入力です');
}
} catch (Exception $e) {
die($e->getMessage());
}

のようなロジックがあるとする。これでは名前が未入力のときにメールアドレスのチェックを行わない。それを避けて、どちらもチェックしたいようにするときは

$errors = array();

if ($name === '') {
$errors[] = '名前が未入力です';
}
if ($email === '') {
$errors[] = 'メールアドレスが未入力です';
}
if ($errors) {
die(implode("<br />\n", $errors));
}

というように Exception を使った処理を諦めている人が多いのではないだろうか?しかしこれだとMVCフレームワークを使った開発では、モデルにバリデーション処理を吸収させづらくなり、渋々コントローラを肥大化させるはめになるのではないかと思う。そこでこれを解決する案を素人なりに考えてみた。

PHP5.3以降 であれば、 Exception::getPrevious() メソッドにより、例外をスタックとして積んでいくことができる。今回はこれを利用する。

毎回 Exception のコンストラクタを呼ぶのは面倒なので、関数を作る。この関数ではインスタンスを作るだけで、スローは行っていない

function e($message, $previous = null) {

return new Exception($message, 0, $previous);
}

更に、例外スタックから順次取り出して配列に変換する関数を作る。スタックの性質故に逆順になることを避ける為、最後に array_reverse 関数を通している。

function exception_to_array(Exception $e) {

do {
$errors[] = $e->getMessage();
} while ($e = $e->getPrevious());
return array_reverse($errors);
}

これらを使って最初のロジックはこう書き換えることが出来る。

try {

$e = null;
if ($name === '') {
$e = e('名前が未入力です', $e);
}
if ($email === '') {
$e = e('メールアドレスが未入力です', $e);
}
if ($e) {
throw $e;
}
} catch (Exception $e) {
die(implode("<br />\n", exception_to_array($e)));
}

どうだろうか?これだけでも随分MVCに基づいた設計がしやすくなった(と私は思う)。

これを利用して、初心者向けにドットインストールで開講されている講座 「ユーザー管理をするWebサービスを作ろう」 を自分の納得の行くように書き換えてみた。今回は 「フレームワークを使わないけどフレームワークっぽいことを手軽に実装する」 ことを目標としてみた。

GitHub - sns_php


応用編


$e の初期化を不要にする

参照渡しにすればNoticeを発生せずに自動的にNULLとして初期化される性質を利用する。

function e($message, &$previous = null) {

return new Exception($message, 0, $previous);
}

try {

if ($name === '') {
$e = e('名前が未入力です', $e);
}
if ($email === '') {
$e = e('メールアドレスが未入力です', $e);
}
if (!empty($e)) {
throw $e;
}
} catch (Exception $e) {
die(implode("<br />\n", exception_to_array($e)));
}


自作例外クラスを Traversable にする

このクラスを継承させると、 exception_to_array 関数を通さなくても直接 $e をforeachにかけて反復処理させることが出来る。

class TraversableException extends RuntimeException implements IteratorAggregate {

public function getIterator() {
$e = $this;
do {
$array[] = $e;
} while ($e = $e->getPrevious());
return new ArrayIterator(array_reverse($array));
}
}


RuntimeException クラスを Traversable にする

Runkit を用いた超大型反則技。あらゆる実行時例外をforeachを用いて処理できるようになる。これはやってみる価値あるんじゃないかと。

abstract class TraversableException extends Exception implements IteratorAggregate {

public function getIterator() {
$e = $this;
do {
$array[] = $e;
} while ($e = $e->getPrevious());
return new ArrayIterator(array_reverse($array));
}
}
runkit_class_adopt('RuntimeException', 'TraversableException');