You are here
Home > 重複回避(連載)

フォームリクエスト:コントローラからバリデーションを分離

「重複回避-DB重複エラーを利用」で使用した以下のコントローラのメソッドをここでもう一度掲載します。

public function postSignup(Request $request)
{
    $rules = [
      'email'      => 'required|email|unique:member,email',
      'password'   => 'required|min:6|max:20|confirmed',
      'first_name' => 'required',
      'last_name'  => 'required'
    ];

    $messages = [
       'email.unique' => "Eメールアドレスはすでに使用されています"
    ];

    $this->validate($request, $rules, $messages);

    try 
    {
      $member = Member::create($request->all());
    }
    catch(IlluminateDatabaseQueryException $e)
    {
      $errorCode = $e->errorInfo[1];

      if($errorCode == 1062) //重複エラーをここでキャッチ
      {
        return back()->withInput()->withErrors(['email' => "Eメールアドレスはすでに使用されています"]);
      }
    }

  return "登録完了";
}  

フォームでの入力により送信された値は、このメソッドのパラメータ変数$requestに収められます。それらをバリデーションして、エラーがあれば表示、なければ新規の会員のDBレコードを作成。さらに、タイミングエラーにより逃れた重複エラーも発生するならキャッチ。まとまってとてもわかりやすいです。これはこれで十分と思います。

しかし、このコントローラのコードがより複雑になったときに、たとえば、新規会員、編集、パスワードの編集と会員関連のバリデーションを一箇所に集めて管理性を高めたいときはどうしましょう?

もちろん、1つのコントローラでpostSignup()のようにそれぞれのメソッドで対応するのも1つのやり方、しかし、会員のすべての処理を1つのコントローラで対応することはできません。例えば、ユーザーはエンドユーザーだけでなく管理者も会員処理が必要となります。その場合は少なくともユーザーと管理者に別々のコントローラを持つことになるでしょう。そうなら、モデルのMember.phpに入れるのも1つの手です。

ここでは、それらとまったく違う手法、フォームリクエストを紹介します。ララベル5.1以降が必要です。

フォームリクエストは、まずapp/Http/Requests/MemberRequest.phpのファイルを作成して、以下のように先のコントローラのバリデーション部分を移行します。

namespace AppHttpRequests;

use AppHttpRequestsRequest;

class MemberRequest extends Request {
    public function authorize()
    {
      return true;
    }

    public function rules()
    {
      return [
        'email'      => 'required|email|unique:member,email',
        'password'   => 'required|min:6|max:20|confirmed',
        'first_name' => 'required',
        'last_name'  => 'required'
      ];
    }
}

これにより先のコントローラは以下のようにシンプルになります。

public function postSignup(MemberRequest $request)
{
    try 
    {
      $member = Member::create($request->all());
    }
    catch(IlluminateDatabaseQueryException $e)
    {
      $errorCode = $e->errorInfo[1];

      if($errorCode == 1062) //重複エラーをここでキャッチ
      {
        return back()->withInput()->withErrors(['email' => "Eメールアドレスはすでに使用されています"]);
      }
    }

  return "登録完了";
}  

パラメータにおいて、$requestのタイプが、RequestからMemberRequestに変わったことに注意してください。

さて、validateの関数のコールはどこへ行ったのでしょうね?

validateの関数のコールは、MemberRequestのオブジェクトが生成されるとともに、その中で実行されます。そしてエラーがあれば画面にエラーを表示し、エラーがなければ、postSignup()の残りのコードを処理します。

今回はシンプルな例でしたが、将来はより複雑な例を紹介しましょう。

重複回避 – DB重複エラーを利用

前回においては入力バリデーションでの重複回避を紹介しました。

小規模なサイトではこれだけで重複回避は十分かもしれません。しかし、以下の状況では、どうなるでしょう?

ユーザーAさんとBさんがいたとして、どちらも、ほとんど同時にtest@gmail.comのログインで登録を試みます。もちろんそんなことは滅多に起こらないのですが、たまたまBさんは間違って自分のEメールアドレスをAさんと同じものとタイプしたとします。それから、test@gmail.comでの会員はまだDBには存在しないという仮定です。

Aさんは、

10時20分00秒:入力完了して送信(登録ボタンを押す)
10時20分05秒:入力バリデーション無事通過!
10時20分15秒:無事にDBに登録成功!

Bさんも、5秒遅れて、

10時20分05秒:入力完了して送信(登録ボタンを押す)
10時20分10秒:入力バリデーション無事通過! 

(もちろん実際はこんなに遅く物事は進行しませんが、経過を理解してもらうために)

あれあれ、まだAさんのレコードはDBにないから、入力バリデーション効きませんね。

このままだと重複のレコードになってしまいますね。どうしましょう?

通常、会員の登録のDBテーブルでは、同じID、ここではEメールの重複はないよ、ということで、プライマリーキーあるいはユニークキーというものを設定します。

例えば、以下はmysqlでのDB設定ですが、「Unique Key `member_unique` (`email)」がDBに重複のレコードが作成されるのを防ぎます。

CREATE TABLE `member` (
  `member_id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `email` varchar(100) NOT NULL DEFAULT '',
  `password` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`member_id`),
  UNIQUE KEY `member_unique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

このDBの設定のおかげで、

10時20分15秒:DBの重複エラーとなり、Bさんのレコードは作成されず重複のレコードの作成は回避されます。

ここの部分、プログラムでは前回の入力バリデーションと合わせて以下のようになります。

public function postSignup(Request $request)
{
    $rules = [
      'email'      => 'required|email|unique:member,email',
      'password'   => 'required|min:6|max:20|confirmed',
      'first_name' => 'required',
      'last_name'  => 'required'
    ];

    $messages = [
      'email.unique' => "Eメールアドレスはすでに使用されています"
    ];

    $this->validate($request, $rules, $messages);

    try 
    {
      $member = Member::create($request->all());
    }
    catch(IlluminateDatabaseQueryException $e)
    {
      $errorCode = $e->errorInfo[1];

      if($errorCode == 1062) //重複エラーをここでキャッチ
      {
        return back()->withInput()->withErrors(['email' => "Eメールアドレスはすでに使用されています"]);
      }
    }

  return "登録完了";
}  

これで重複は完全に回避されます。

これなら、入力バリデーションの重複はチェックはもう要らないのでは?

それを削除をしてもOKと思います。しかし、ルールの記述としてプログラムに残るのは良いかもしれません。将来はDBレベルの重複エラーもフレームワークが面倒みてくれるかもしれません。

重複回避-入力バリデーションで回避

GmailのEメールアドレス、Facebookのログイン、銀行の口座番号、などなど・・世の中どこに行っても必要なIDの情報。この情報に重複があったら困りますね。

このIDとなる情報を登録するプロセス、まずは入力バリデーション、次にDBでレコード作成時の重複DBエラーを利用しての2ステップで重複を回避します。

今回は、入力バリデーションを使って:

public function postSignup(Request $request)
{
  $rules = [
    'email'      => 'required|email|unique:member,email',
    'password'   => 'required|min:6|max:20|confirmed',
    'first_name' => 'required',
    'last_name'  => 'required'
  ];

  $messages = [
    'email.unique' => "Eメールアドレスはすでに使用されています"
  ];

  $this->validate($request, $rules, $messages);

  $member = Member::create($request->all());
}

重複をチェックしてくれるのは、unique:member,emailのルールです。「DBテーブル member の email には同じ情報があってはいけません」というルールです。

すでにDBに存在するEメールを入力したなら、DBに値を保存せずに、入力画面へ戻りエラー「Eメールアドレスはすでに使用されています」を表示してくれます。

それを行ってくれるのが次の1行:

$this->validate($request, $rules, $messages);

しかし、シンプルすぎて逆にわかりにくいかもしれません。そう思うなら、ララベルバージョン4のように、以下とも書くことできます。

$validator = Validator::make($request->all(), $rules, $messages)

if ($validator->fails())
{
  return back()->withErrors($validator)->$withInput();
}

さて、重複回避は、このチェックだけで十分でしょうか?

次回は、DBレベルでの重複回避の紹介をします。

Top