アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Standard PHP Libraryの例外クラスを活用しよう!


はじめに



今回はPHPでの例外の扱い方、特にSPL (Standard PHP Library)例外クラスの使い方を見ていきます。例外を投げる際には、エラー種別により例外クラスを切り替え、受け取る側での処理も分けます。Javaなどではごく当たり前です。しかし、PHPプログラマの中には、そこまで切り分けない人も意外といます。Exceptionクラスだらけのコードもしばしば見かけます。


SPLの例外クラス



アプリケーションによっては、例外クラスを独自に作成することもあります。とはいえ、いきなり例外クラスを複数定義して使いまわすことは、若干ハードルが高いかもしれません。まずは、SPL (Standard PHP Library)例外クラスを使ってみましょう。SPLでは以下の例外クラスを提供しています。

SPL 例外クラスツリー


LogicException (extends Exception)
 ├ BadFunctionCallException
 │  └ BadMethodCallException
 ├ DomainException
 ├ InvalidArgumentException
 ├ LengthException
 └ OutOfRangeException
 
RuntimeException (extends Exception)
 ├ OutOfBoundsException
 ├ OverflowException
 ├ RangeException
 ├ UnderflowException
 └ UnexpectedValueException


実際にSPLの例外を使う際には、最低でもLogicExceptionとRuntimeExceptionの切り分けはしておいた方がよいでしょう。それぞれの意味はPHPマニュアルによると、次の通りです。



LogicException:プログラムのロジック内でのエラーを表す例外です。
                この例外が出た場合は、自分が書いたコードを修正すべきです。
RuntimeException:実行時にだけ発生するようなエラーの際にスローされます。


初めて読んだ場合、分かるような分からないような感じです。具体的に見て行きましょう。


RuntimeException



RuntimeExceptionを使うのは、データベースに接続できなかった場合や、ファイルの生成を出来なかった場合など、コードを実行して始めて結果が分かる場合です(処理の流れ自体は正常な場合)。外部システムとの連携部分でのエラーもこの部類に入ります。具体的には、次のような場合です。



<?php
class SomeClass
{
    public function doSomething(...)
    {
        ...
        
        $fp = fopen($filePath, 'w');
        
        // ファイルに書き込みを必ずしなければならない場合
        if (!$fp) {
            throw new RuntimeException('...');
        } else if (!flock($fp)) {
            throw new RuntimeException('...');
        }
        
        ...
    }
}


ちなみに、PDOでデータベース接続に失敗した場合、PDOException(RuntimeExceptionのサブクラス)が投げられます。そのため、PDOの使用は次のように記述します。



<?php
try {
    $pdo = new PDO('...');
} catch (PDOException $e) {
    // エラー処理
}



LogicException



LogicExceptionを使うのは、処理の途中で絶対に入りえない(と考えられる)分岐に入った場合や、予め想定されている使い方をされなかった場合など、ロジックを確認する場合です。例えば、以下のような場合です。



<?php
class SomeClass
{
    public function doSomething($params)
    {
        if (...) {
            ...
            
            if (...) {
                ...
            } else {
                // 絶対にここにはこないはず
                throw new LogicException('...');
            }
            
            ...
        }
        
        ...
    }
    
    // オーバーライドして使わなければならないメソッド
    public function beOverrided()
    {
        throw new LogicException('...');
    }
}


beOverrided()メソッドでLogicExceptionを投げている部分は、サンプルコードでは抽象クラスと抽象メソッドに変えることも可能です。ただ、そのように出来ない場合もありえます。

コードを正しく実装していれば、LogicExceptionは投げられません。ただ、ロジックが複雑になるにつれ、制御が不安定になることは十分考えられます。また、複数人の開発の場合にも同様の問題が発生するかもしれません。そのような場合、あえてLogicExceptionを入れておくと、問題の早期発見につながります。

さらに具体的かつ簡潔なコードを見てみましょう。以下のPersonクラスではコンストラクタで引数として文字列を受け取ります。この引数に文字数制限を加えたい場合、InvalidArgumentExceptionを使ってみましょう。こうすることで、例外を受け取った側のコードでは、より高い精度でエラー種別を判別できます。



<?php
class Person
{
    const MAX_NAME_SIZE = 255;
    private $name;
    
    public function __construct($name, $age)
    {
        if (strlen($name) > self::MAX_NAME_SIZE) {
            throw new InvalidArgumentException('...');
        }
        
        $this->name = $name;
    }
}

// 受け取る側
try {
    ...
} catch (InvalidArgumentException $e) {
    ...
} catch (LogicException $e) {
    ...
}


上記のコードでInvalidArgumentExceptionを受け取るということは、事前に引数のチェックが行われていないことを意味します。すなわち、Personクラスをインスタンス化する前に、変数をチェックするようにコードを修正しなければなりません。実行時のエラーではなく、ロジックの不備によるエラーです。


例外クラスの拡張



最後に、独自例外クラスの作り方を見てみましょう。Exceptionクラスかそのサブクラスをスーパークラスとして新規例外クラスを作成します。例えば、ファイルが見つからなかった場合の例外クラスは次のように作ります。



<?php
class FileNotFoundException extends RuntimeException
{
    // メソッドをオーバーライドする必要はない
}


もちろん、SPLから離れて独自の例外クラス郡を作成することも可能です。実際、フレームワークやライブラリではそのように実装しているものもあります。


まとめ



SPLの例外クラスはとっつきにくそうに見えます。しかし、意味を理解すると、なかなか便利に使えます。何よりも、例外処理やエラー処理を適切に行えるようになると、コードの質が上がります。まだ使ったことがない人は、ぜひ試してみてください。