Laravel3、小さなクラスで更新

タグ: Laravel3  

この記事は次の記事の内容の続きとして書いています。

また上記の記事は、更に以下の記事の内容を元に書いています。

今回は、更新処理を実装しましょう。これでりんごテーブルのCRUD処理を一通り、小さなクラスと例外を使用して実現することになります。

ルート

routes.phpです。

// りんご更新
Route::get( 'apple/update/(:num)', array ( 'as' => 'update-apple', 'uses' => 'apple@update' ) );
Route::post( 'apple/update/(:num)', array ( 'as' => 'update-apple', 'before' => 'csrf', 'uses' => 'apple@update' ) );

コントローラー

    function get_update( $id )
    {
        $inputs = Input::only( array ( 'name', 'color' ) );

        if ( empty( $inputs ) )
        {
            try
            {
                $apple = $this->appleRepo->read( $id );
            }
            catch ( IdNotFoundException $e )
            {
                return Redirect::error( '404' );
            }
            Input::replace( $apple->to_array() );
        }
        return view( 'apple.create' );
    }

    function post_update( $id )
    {
        try
        {
            $this->appleRepo->update( $id, Input::only( array ( 'name', 'color' ) ) );
        }
        catch ( IdNotFoundException $e )
        {
            return Redirect::error( '404' );
        }
        catch ( ValidationFaildException $e )
        {
            return Redirect::back()
                    ->with_input()
                    ->with_errors( $e->validator );
        }
        return Redirect::back();
    }

コントローラーの更新部分です。getでフォーム表示、postでフォームの内容を処理しています。

postの更新処理を最初に見てください。追加と削除の処理をくっつけたようなコードです。とても単純です。

IDが見つからない場合と、バリデーションが失敗した場合の例外を拾い、それぞれ適切にリダイレクトしています。拾える例外は一つだけでありません。いくつも捕捉することが可能です。

続いてgetのフォーム表示を見てみましょう。今までコントローラーではif文を使用しなくても済みましたが、今回は必要です。フォームが未入力状態の場合、つまり最初にフォームを表示する場合は、テーブルから指定されたレコードを読み出し、その内容を表示しなくてはなりません。その処理を入れるため、読み込みの処理が入っています。

Laravel的な技芸コードとして読み込んだEloquentモデルをto_arrayメソッドで配列に変換し、それをInput::replaceメソッドで入力された値としてLaravelに扱わせます。

ビューは新規作成の時のビューを共用します。今回のために変更を加えました。create.bldade.phpです。

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

nameとcolorのフィールドに表示する値を2段階で扱うようにします。Input::oldでセッションの中に保存されているかチェックし、保存されている場合はその値を表示します。これはpostからエラー時にリダイレクトされた場合に対応するためです。

セッションの中に保存されていない場合、Input::getでフォームの入力値を調べます。これはInput::replaceで保存した内容を表示させるためです。

Input::replaceを使用せず、セッションに直接保存すればビューは以前のままInput::oldだけチェックすれば済みます。しかし、セッションはファイルやデータベースの場合であるかも知れず、その場合オーバーヘッドが大きくなります。しかも、配列でまとめてセッションに保存する方法がSessionクラス、Inputクラスには無く、一つ一つフィールドを指定しながら記述しなくてはなりません。そのため、配列をまとめて置き換えられ、メモリ操作だけで済む、Input::replaceを使用しました。

リポジトリー

これはほとんど新規の場合のコードと同じになります。リポクラスの全コードを紹介します。

<?php

namespace ProjectH\Repositories;

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

class AppleRepo
{
    private $apple;

    public function __construct()
    {
        $this->apple = IoC::resolve( 'Apple' );
    }

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

        $apple = $this->apple->create( $data );
        $apple->save();
    }

    public function read( $id )
    {
        $apple = $this->find( $id );

        return $apple;
    }

    public function update( $id, $data )
    {
        $apple = $this->find( $id );

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

    public function delete( $id )
    {
        $apple = $this->find( $id );
        $apple->delete();
    }

    private function find( $id )
    {
        $apple = $this->apple->find( $id );

        if ( $apple === null )
        {
            throw new IdNotFoundException( 'IDが見つかりません。' );
        }

        return $apple;
    }

}

読み込みと更新処理で両方共findメソッドを使用するため、関数として切り出しています。

おかけで更新処理はとてもすっきりです。まずレコードを読み込み、バリデーションし、最後にレコードを更新します。

バリデーションクラス

本日のメインイベントです。バリデーションクラスです。

<?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 )
    {
        $this->excute_validation( $data, $this->rules );
    }

    function validate_update( $id, $data )
    {
        $rules = $this->add_id_with_unique( $id, $this->rules );

        $this->excute_validation( $data, $rules );
    }

    private function excute_validation( $data, $rules )
    {
        $val = \Validator::make( $data, $rules );

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

    protected function add_id_with_unique( $id, $rules )
    {
        foreach ( $rules as $item => $rule )
        {
            if ( preg_match( '/unique:(\w+)(,(\w+))?/', $rule ) > 0 )
            {
                $patterns = explode( '|', $rule );
                $new = array ( );

                foreach ( $patterns as $pattern )
                {
                    if ( preg_match( '/unique:(\w+)(,(\w+))?/', $pattern, $matches ) > 0 )
                    {
                        $unique = 'unique:'.$matches[1];

                        if ( isset( $matches[3] ) )
                        {
                            $unique .= ','.$matches[3].','.$id;
                        }
                        else
                        {
                            $unique .= ','.$item.','.$id;
                        }
                        $new[] = $unique;
                    }
                    else
                    {
                        $new[] = $pattern;
                    }
                }

                $rules[$item] = implode( '|', $new );
            }
        }

        return $rules;
    }

}

以上に複雑に思えるでしょうが、add_id_with_unique関数が長いだけです。これをまず無視して見てください。とても単純ですよ。

新規の場合は指定されたバリデーションルールをそのまま使用しています。更新の場合はadd_id_with_unique関数から返された配列の値で、バリデーションを行います。

まあ実はadd_id_with_uniqueは他のプロジェクトで予め作っておいたものをコピペしたのです。バリデーションのuniqueルールは特定のレコードを無視することができます。これは更新処理のための仕様です。例えばりんご名「フジ」を変更せずそのまま「フジ」にする場合、uniqueルールをつけている場合既に存在しているため、バリデーションが通りません。かと言ってuniqueを外してしまえば、他のりんごと同じ名前を付けることができてしまいます。そのため、uniqueルールで更新処理を行う場合、無視するレコードIDを指定することができるようになっています。

この複雑なadd_id_with_unique関数はルール定義の配列の中を探しuniqueルールが存在していれば、IDをセットする仕組みになっています。

今回のチュートリアルシリーズは、目的をはっきりさせていませんが、こうしたおまけが隠されているシリーズです。;P