You are here
Home > フォーム(連載)

会員編集フォーム – Laravel Collective

今回は、前回作成した会員編集フォームを、Laravel CollectiveのForm & HTMLを使用して書き換えてみます。

このForm & HTMLは、Laravelのバージョン4.2には含まれていました。その後、Lumenの登場によりLaravelから分けて、Laravel Collectiveからの配布となりました。Laravel Collectiveではその他にも、Annotation, Remote(SSH)など有用なパッケージが含まれています。

まず、Form & HTMLのインストールから。

composer.jsonを編集します。

...
   "require": {
        "php": ">=5.5.9",
        "laravel/framework": "5.2.*",
        "barryvdh/laravel-debugbar": "^2.1",
        "laravelcollective/html": "5.2.*"
    },
...

その後以下をコマンドで実行。

$ composer update

次は、config/app.phpを編集します。

 'providers' => [
 ...
    Collective\Html\HtmlServiceProvider::class,
 ...
  ],
 
 'aliases' => [
...
      'Form' => Collective\Html\FormFacade::class,
      'Html' => Collective\Html\HtmlFacade::class,
...
  ],

これでインストール完了です。

前回のテンプレートは、以下のように変わります。ハイライトの部分が変更した部分です。

@extends('user.layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Profile</div>
                <div class="panel-body">
                {!! Form::open(['url' => url('/user/profile'), 'class' => 'form-horizontal', 'role' => 'form']) !!}

                        <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
                            <label class="col-md-4 control-label">名前</label>

                            <div class="col-md-6">
                            {!! Form::text('name', $user->name, ['class' => 'form-control']) !!}
                                @if ($errors->has('name'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('name') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
                            <label class="col-md-4 control-label">Eメール</label>

                            <div class="col-md-6">
                                {!! Form::email('email', $user->email, ['class' => 'form-control']) !!}

                                @if ($errors->has('email'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                {!! Form::submit('保存', ['class' => 'btn btn-primary']) !!}
                            </div>
                        </div>
                {!! Form::close() !!}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

デザイナーが編集できるテンプレートというより、どちらかというと開発者向け?と思われるかもしれません。しかし、例えば、前回で追加したクロスサイトリクエストフォージェリ対策の{!! csrf_field() !!}は自動的に生成されるし、タイプする量は減りますね。

また、以下のようにドロップダウンのデフォルト選択ではとても便利です。

前回のフォームでは、以下のように選択値のそれぞれでチェックする必要ありますが、

            <select name="category">
                <option value="">選択してください</option>
                <option value="1" {{ (old("category") == '1') ? "selected":"" }}>犬</option>
                <option value="2" {{ (old("category") == '2') ? "selected":"" }}>猫</option>
                <option value="3" {{ (old("category") == '3') ? "selected":"" }}>猿</option>
            </select>

これをFormで書き換えると、一行で終わってしまいます。


{!! Form::select('category', ['1' => '犬', '2' => '猫', '3' => '猿'], old('category'), ['placeholder' => '選択してください']) !!}

会員編集フォーム

ウェブのプログラミングでなんといっても難しいのはフォーム画面のプログラムです。テキスト入力、ドロップダウン、ファイルのアップロード機能、さらにjqueryやangularなどのフロントエンドのjavascriptも入れば、無限の可能性があります。

以前紹介したLaravelの付録の会員認証サンプルの会員登録画面は、php artisan make:authコマンドで自動作成されますが、ログイン後に会員がEメールアドレスや名前を編集する画面は作成されません。

今回はこの編集画面を作成してみます。

まず、新規のコントローラ作成します。

php artisan make:controller User/UserController

を実行して、それを以下のように編集し、

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\User;

use Auth;
use Validator;

class UserController extends Controller
{
    protected $user;

    public function __construct()
    {
        $this->middleware('auth:user'); // 認証

        $this->user = Auth::guard('user')->user();
    }

    public function getProfile()
    {
        return view('user/profile')->with(['user' => $this->user]);
    }

    public function postProfile(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name'  => 'required|max:255',
            'email' => 'required|email|max:255|unique:users,email,'.$this->user->id
        ]);

    // エラーチェック
        if ($validator->fails())
        {
            return back()->withInput()->withErrors($validator);
        }

    // データを更新
        $this->user->update([
            'name'  => $request->input('name'),
            'email' => $request->input('email')
        ]);

        return redirect('user/home');
    }
}

Validationでは、emailのチェックは、$this->user->idを除くusersのレコードを対象に他に同じEメールが使われていないかチェックします。

次に、以下のようなテンプレートを作成します。

@extends('user.layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Profile</div>
                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ url('/user/profile') }}">
                        {!! csrf_field() !!}

                        <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
                            <label class="col-md-4 control-label">名前</label>

                            <div class="col-md-6">
                                <input type="text" class="form-control" name="name" value="{{ old('name', $user->name) }}">

                                @if ($errors->has('name'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('name') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
                            <label class="col-md-4 control-label">Eメール</label>

                            <div class="col-md-6">
                                <input type="email" class="form-control" name="email" value="{{ old('email', $user->email) }}">

                                @if ($errors->has('email'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    保存
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

ここ、old()のLaravelの関数が使用されていますが、すでに存在する値を編集するので、デフォルトとしてDBからの値が入っていることに注意してください。

 <input type="text" class="form-control" name="name" value="{{ old('name', $user->name) }}">

コントローラでは、validationエラーが発生して、現在の画面に戻る必要があります。そのときは、withInput()がセッションに入力の値を保存して、back()で現在の画面にリダイレクトします。リダイレクト後には、old()は、デフォルトでなくセッションに保存された入力された値を表示します。

最後に、routes.phpを以下のように編集します。

Route::group(['prefix' => 'user', 'middleware' => 'web'], function () {
...
    Route::get('profile', 'User\UserController@getProfile');
    Route::post('profile', 'User\UserController@postProfile');
});

以下のように、認証のミドルウェアをコントローラから移してもOKです。

Route::group(['prefix' => 'user', 'middleware' => 'web'], function () {
...
   Route::group(['middleware' => 'auth:user' ], function () {
        Route::get('home', 'User\HomeController@index');
        Route::get('profile', 'User\UserController@getProfile');
        Route::post('profile', 'User\UserController@postProfile');
    });
});

入力画面のユニットテスト

ユニットテスト(PHPの場合は、phpunit)を使い始めて、2,3年。その重要さは理解しているものの、つい最近まで、コードの大変さによりなかなか多用はしていませんでした。

データベース絡みや入力画面絡みのテスト、書くのはやっかいです。しかし、ウェブのアプリの開発には、データベースや入力画面があって当然。ここのテストを自動化をせずにいったいどこでする?とも思う。

しかし、どうしてやっかいなのでしょう、これら?

例えば、前回の会員登録の件では、テストとして考えられるのは、

まずは、エラーを出して確認するテストたち:

  • 必須の項目に値がないときにエラーとなるか。
  • EメールにEメールでないものを入力したときにエラーとなるか。
  • すでに登録した会員のEメールを入れたときにエラーとなるか(重複チェック)。

そして、エラーがなく、会員登録成功したときの確認のテストも必要です。

前者のエラーを出して確認するテストは、コントローラーでなくモデルに入れてテストが可能かもしれません。ララベルのバリデーションはすでに一貫しているし、カスタマイズのバリデーションも同様にテスト可能です。

しかし、それらユニットテストは個々のバリデーションをテストするだけで、それら集合した入力画面に対してのファンクショナルテストやアクセプタンステストは難しいです。あたかもユーザーが入力するようなテストです。

さらに、後者のエラーが出ない成功の確認のテストは、実際にDBにレコードが作成されてしまいます。つまり1回成功テストすると、次回は重複でエラーとなり成功のテストができなくなります。もちろん、毎回レコードを削除するという手段もありますが(これはDBテストとして将来に紹介します)、違うDBを用意するとかお膳立てがかなり面倒です。

もちろん、ここで、ララベルがそのフレームワークを活かしたユニットテストの登場です(注意:ララベル5.1での前提です)。

まずは、Eメールアドレスの重複による失敗のテストです。

class SignupTest extends TestCase
{
    public function testSignupFail()
    {
        $this->visit('/signup')
            ->type('dup@gmail.com', 'email')
            ->type('testtest', 'password')
            ->type('testtest', 'password_confirmation')
            ->type('山田', 'last_name')
            ->type('太郎', 'first_name')
            ->press('保存')
            ->dontSee('会員登録完了');
    }
}
[/code]

このテストは、ユーザーが以下のような画面を経験したと同じ状況をテストします。すでにDBにdup@gmail.comが存在していると仮定です。

<a href="https://www.larajapan.com/wp-content/uploads/2015/09/fail2.png"><img class="alignnone size-full wp-image-181" src="https://www.larajapan.com/wp-content/uploads/2015/09/fail2.png" alt="fail" width="471" height="349" /></a>

16行目の "dontSee('会員登録完了');" では、エラーになるために成功時の「会員登録完了」の文字が画面には見えないよ、つまりdon't seeということです。

今度は、成功時のテストですが、


use IlluminateFoundationTestingWithoutMiddleware;
use IlluminateFoundationTestingDatabaseMigrations;
use IlluminateFoundationTestingDatabaseTransactions;

class SignupTest extends TestCase
{
    use DatabaseTransactions;

    public function testSignupSuccess()
    {
        $this->visit('/signup')
            ->type('success@gmail.com', 'email')
            ->type('testtest', 'password')
            ->type('testtest', 'password_confirmation')
            ->type('山田', 'last_name')
            ->type('太郎', 'first_name')
            ->press('保存')
            ->see('会員登録完了');
    }
}

先の失敗の例と違って、今度はdon’t seeでなくsee(‘会員登録完了’)となっています。

しかし会員登録完了なのにDBのレコードは作成されません。そう、7行目のuse DatabaseTransactionsの宣言により、レコードの作成を試みるもののDBのトランザクション機能を使用してわざと変更をロールバックさせています。これにより作成あるいは変更されたデータを手動で戻さずに、何回でもテストの実行が可能となるわけです。もちろん、トランザクション機能があるDB、mysqlならinnodbの使用でないとできないことです。

先の2つテストを合わせて、実行すると、「OK」という結果です。

$ phpunit tests/SignupTest
PHPUnit 4.8.2 by Sebastian Bergmann and contributors.

..

Time: 313 ms, Memory: 27.25Mb

OK (2 tests, 7 assertions)
Top