Laravel3、例外と小さなクラス達

タグ: Laravel3  

前回までに例外の使い方と、クラスの構成を説明しました。今回は、その2つの考え方を取り入れ、具体的なコードを考えましょう。

以下のようなディレクトリー構成になります。

+application
    +models
        -apple.php
    +ProjectH
        +Exceptions
            +ProgramErrorExceptions
                -ProgramErrorException.php
                -ValidatorClassNotFoundException.php
            +ExecutionErrorExceptions
                -ExecutionErrorException.php
                -ValidationFaildException.php
        +Repositories
            -AppleRepo.php
        +Services
            +Validators
                -AppleValidator
    +controllers
        -apple.php
    +migrations
    +models
        -apple.php
    +views
        +apple
            -create.blade.php
    -ioc.php
    -routes.php
    -start.php

今回作成/変更するファイルとディレクトリーのみ表示しています。設定ファイルも必要に応じ調整してください。applesテーブルを作成する必要がありますがマイグレーションで作成するか、ツールを使って作成するかはお任せします。

項目名 属性
id increments(primary key, integer, unsigned)
name string 10文字 (varchar(10))
color string 10文字 (varchar(10))

CRUD全部を作成すると、コードの量が増え理解するのに時間がかかってしまいます。まずは簡単に生成から考えて行きましょう。

IoCの活用

クラスの置き換えを簡単にできるようにするため、IoCコンテナを活用しましょう。せっかくLaravelにはお手軽なIoCクラスが用意されていますからね。

一番簡単にセットアップするには適当なファイルにコンテナへの登録をまとめ、route.phpの最後でrequire_onceすることです。分かりやすいようにapplication/ioc.phpファイルに定義を記述してみましょう。

<?php

/*
 * 依存性注入
 */

if ( !IoC::registered( 'Apple' ) )
{
    IoC::register( 'Apple', function($param1)
        {
            return new Apple($param1);
        } );
}

if ( !IoC::registered( 'AppleRepo' ) )
{
    IoC::register( 'AppleRepo', '\\ProjectH\\Repositories\\AppleRepo' );
}

if ( !IoC::registered( 'AppleValidator' ) )
{
    IoC::register( 'AppleValidator', '\\ProjectH\\Services\\Validators\\AppleValidator' );
}

Appleクラスだけ大層な書き方になっています。これはIoCコンテナでインスタンスを生成する時にパラメーターを渡したい場合の記述法です。AppleクラスはEloquentモデルです。Eloquentモデルは生成時にデーターを配列で渡してあげると、そのキーでクラスメンバを作成し、キーに対応する値をセットしてくれます。もちろん、テーブルのフィールド名に対する値を配列で渡してあげるわけです。

Apple以外のAppleRepoとAppleValidatorは単にnewするだけです。引数を渡しません。その場合は生成するクラス名を記述します。

IoCコンテナへ登録する名前は、既に登録されているものも指定でき、その場合は後から登録した内容が有効となります。登録しようとしている名前が既に登録されているかチェックし、未登録の場合のみ登録するようにしています。これは例えばテストなどで置き換える場合、その置き換えの登録が、このファイルで定義されているタイミングの前でも後でも有効にするためです。事前に登録済みであれば、このファイル中では登録済みの名前を再定義することはしません。置き換えのコードがタイミング的に後であれば、後で登録したほうが有効になりますので、置き換えられます。どちらにしても、置き換えられるわけです。置き換えのタイミングで頭を悩ますことはありません。

start.phpの最後で読み込みましょう。

require_once path( 'app' ).'ioc.php';

これで、クラスの置き換えが簡単にできるようになりました。

例外

例外の4ファイルのコードです。名前空間の定義がファイルの設置場所も示しています。

<?php

namespace ProjectH\Exceptions\ExecutionErrorExceptions;

class ExecutionErrorException extends \Exception{}
<?php

namespace ProjectH\Exceptions\ExecutionErrorExceptions;

class ValidationFaildException extends ExecutionErrorException
{
    public $validator;

    public function __construct( $validator, $message = null, $code = 0, Exception $previous = null )
    {
        parent::__construct( $message, $code, $previous );

        $this->validator = $validator;
    }

}
<?php

namespace ProjectH\Exceptions\ProgramErrorExceptions;

class ProgramErrorException extends \Exception {}
<?php

namespace ProjectH\Exceptions\ProgramErrorExceptions;

class ValidatorClassNotFoundException extends ProgramErrorException {}

バリデーション失敗時にエラーを表示するため、バリデーションクラスを渡すために工夫している他は、極めて簡単なコードです。

Eloquentモデル

Appleモデルを定義しましょう。

<?php

class Apple extends Eloquent {
}

これぞLaravelらしい、シンプルなクラスです。

ルートの定義

route.phpで定義します。以下の行を加えてください。

// りんご追加
//  フォーム表示
Route::get( 'apple/add', array ( 'as' => 'create-apple', 'uses' => 'apple@create' ) );
//  フォーム処理
Route::post( 'apple/add', array ( 'as' => 'create-apple', 'before' => 'csrf', 'uses' => 'apple@create' ) );

コントローラー

apple.phpです。

<?php

use ProjectH\Exceptions\ExecutionErrorExceptions\ValidationFaildException;

class Apple_Controller extends Base_Controller
{
    public $restful = true;

    function get_create()
    {
        return view( 'apple.create' );
    }

    function post_create()
    {
        $appleRepo = IoC::resolve( 'AppleRepo' );
        try
        {
            $appleRepo->create( Input::only( array ( 'name', 'color' ) ) );
        }
        catch ( ValidationFaildException $e )
        {
            return Redirect::back()
                    ->with_input()
                    ->with_errors( $e->validator );
        }
        return Redirect::home();
    }

}

今まで当サイトで紹介してきた方法とは異なり、だいぶかっこよくなってます。どこらへんが今までのスラスラと書く場合のコーディングと違っているか確認して行きましょう。

まずuse文です。プロジェクトのファイルはPSR-0規約で書くことにしましたので、名前空間の修飾が必要なになります。私はコード中に完全修飾名で書くよりは、use文を使用するほうが好みです。

2つ目はクラスをIoCコンテナ経由で取得していることです。このコードの場合、もしAppleRepoをIoCコンテナに登録し忘れていた場合、クラスが見つからない例外になります。もちろん、AppleRepoは必須なので見つからなくては動作しません。ですから、これでOKです。

3つめはリポクラスで新しいレコードの挿入が成功した場合以外の状況は、全部例外で通知するようにしているため、ほぼtry{}catch{}だけになっています。とてもシンプルなコントローラーになっています。例外にそれらしい名前をつければ、コードが分かりやすく、シンプルに保てます。

4つ目は他のクラスでチェックを行うため、バリデーションエラー時のメッセージを取得するため、例外クラスにバリデタークラスのインスタンスを保持しています。

リポクラス

<?php

namespace ProjectH\Repositories;

use IoC;
use ProjectH\Exceptions\ProgramErrorExceptions\ValidatorClassNotFoundException;

class AppleRepo
{

    function create( $data )
    {
        if ( IoC::registered( 'AppleValidator' ) )
        {

            $appleValidator = IoC::resolve( 'AppleValidator' );
            $appleValidator->validate_create( $data );
        }
        else
        {
            throw new ValidatorClassNotFoundException(
            'Appleモデルのバリデタークラスが存在していません。' );
        }

        $apple = IoC::resolve( 'Apple', array ( $data ) );
        $apple->save();
    }

}

リポクラスのcreateで最初にバリデータークラスのチェックを行なっています。IoCに登録されているか調べ、未登録ならば例外を発生させます。これは、バリデターの指定し忘れを防ぐためです。

いささか中途半端なのは、IoCコンテナに存在しないクラスを指定した場合、エラーとなります。これを例外で捉えることはできません。(たぶんPHPのレベルで無理なようで、Laravelのコードも例外を捉えようとしていません。)

バリデタークラスは、バリデーションが通らない場合、それを例外で知らせます。このリポクラスでは捉えません。そのためvalidation_create()より後のコードは、バリデーションに失敗した場合実行されません。コントローラーでバリデーション失敗例外を捉えていました。プログラムの実行はそちらへ移行します。

さて、バリデーションクラスがIoCコンテナに登録されていなかった場合、今回は例外を発生させました。しかしバリデーションはセキュリティー的には必要な機能ですが、クラスの機能からすると必ずしも必要ありません。つまり、このリポクラスはあるストレージとのやり取りをするクラスです。ということは、その内容の検証を行うバリデーションは副次的な機能です。そう考えるならば、バリデーションクラスが存在しない場合、バリデーションなしで処理を行うのが正しいと考えることもできます。

そう考えるなら、バリデータークラスが見つからない場合には処理を続け、りんごテーブルに新しいレコードを追加すべきでしょう。例えば、開発の最初の方ではバリデーションを後回しに、先にデータベースアクセスのコードを書くことはあります。バリデータークラスを後回しにしたい場合、そうできるように改造してみましょう。

if文に対するelseとその括弧の間の例外を投げているコードを取り去れば、バリデータークラスがIoCコンテナに未登録の場合、バリデーションを行わない仕様になります。

最後にApple Eloquent ORMモデルをIoCコンテナで取得し、レコードを保存しています。

このAppleクラスは必須です。バリデータークラスと異なり、これがないとリポクラスは成り立ちません。ですからシンプルにエラーを出し、途中で実行中止で構いません。

また、AppleクラスのIoCコンテナによるインスタンスの取得コードでは、resolveメソッドに引数を渡してインスタンスを生成する方法を使っています。

バリデタークラス

<?php

namespace ProjectH\Services\Validators;

use ProjectH\Exceptions\ExecutionErrorExceptions\ValidationFaildException;

class AppleValidator
{
    public $rules = array (
        'name' => 'required|max:10|unique:apples',
        'color' => 'required|max:10',
    );

    function validate_create( $data )
    {
        $val = \Validator::make( $data, $this->rules );

        if ( $val->fails() )
        {
            throw new ValidationFaildException( $val,
                'Appleクラスのバリデーションに失敗しました。' );
        }
    }

}

シンプルです。バリデタークラスのインスタンスを作成し、バリデーションを実行、もしバリデーションに通らなかった場合、例外を投げます。その例外には生成したバリデタークラスのインスタンスを付加しています。バリデーションエラーを渡すためです。

ビュー

どんなものでも良いのですが、簡単なビューを紹介しておきます。apple/create.blade.phpです。

{{ Form::open() }}
    {{ Form::label('name', 'りんご名') }}
    {{ Form::text('name', Input::old('name', '')) }}
    @if ( $errors->has('name') )
        {{ $errors->first('name') }}
    @endif
    <br>
    {{ Form::label('color', 'りんご色') }}
    {{ Form::text('color', Input::old('color', '')) }}
    @if ( $errors->has('color') )
        {{ $errors->first('color') }}
    @endif
    <br>
    {{ Form::submit('りんご追加') }}
    {{ Form::token() }}
{{ Form::close() }}

次の記事では、レコードの読み込み・更新を今回と同様に、例外と小さなクラスを使い実現してみましょう。新しい内容はありません。両方共今回の応用です。