Help us understand the problem. What is going on with this article?

サーバー・クライアントの両面からかけるバリデーション - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第7回】マニュアル

More than 3 years have passed since last update.

:large_blue_circle: はじめに

本投稿は、2015/7/24に行われた、サーバー・クライアントの両面からかけるバリデーション - connpassの内容についてまとめた資料です。

:warning:今後の予定
AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass

今回は、「バリデーション」を実装します。
前回までは、変な入力に弱い「サンプルプログラム」の域を出ないものでしたが、今回でだいぶまともな「アプリケーション」になりますよ!

CakePHPのバリデーション機能、Backbone.jsのバリデーション機能を使って実装していきます。

:large_blue_circle: なぜバリデーションが必要なのか?

今回、サーバサイド、クライアントサイド両方でバリデーションを行いますが、なぜ両方でやる必要があるのでしょうか。
サーバ、クライアントで「バリデーションをする理由」が違うと考えています。

サーバ、クライアント両方でバリデーションする理由

重要な事は、バリデーションはクライアントだけでは完結しないということです。
「クライアントでは一覧から選択するので、サーバ側でチェックは必要ない」
というわけではありません(現実にはよくある話かもしれませんが...)。

  • クライアントでやっていることもサーバでもう一回やるのが基本
    • APIを画面以外から叩かれるかもしれない(POSTMAN!)
      • アプリの画面だけがクライアントと思わないこと!
  • サーバ側でしか出来ないバリデーションがある
    • DBを参照する
    • 日時が関係するもの
    • 他のシステムとやりとりする必要があるもの
    • etc...

サーバサイドでバリデーションする理由

サーバ側は、不正な入力を検知し、下記を達成するためにバリデーションを行います。

  • アプリケーションを守る
    • 予期しない動作(落ちたりとか...)を防ぐ
    • セキュリティを確保する(XSS、SQLInjection、etc...)
  • データを守る
    • 不正なデータが登録されないようにする
      • 値の範囲、フォーマット、エンコーディング...
      • データ間の整合性

クライアントサイドでバリデーションする理由

サーバ側できちんとバリデーションが行われているとすれば、クライアントサイドでは最悪バリデーションはしなくてもいいかもしれません。
しかし、「サーバでなくてもできること」はクライアントサイドでやることで、すぐにレスポンスがありUXの向上が期待できます。
入力すればすぐ間違いがわかるといいですよね!
クライアントが持っている情報だけでできることは、クライアントでやってしまった方が良い場合も多いでしょう。
※このへんはアプリケーションの要件や環境次第ですね。クライアントサイドのバリデーションを一切やらないのも一つの選択です。

  • 入力値の範囲、フォーマット
  • 事前にサーバから取得した情報を用いてのチェック
  • そもそも不正な値が入力できないUI(バリデーションとは違いますが...)
    • 直接入力でなく選択式にする
    • Maxlength等をちゃんと設定する

:large_blue_circle: 今回の内容

サーバサイド、クライアントサイドでそれぞれ下記のバリデーションを実装します。

こんな風にメッセージが出るようにします。
:warning: 下記はクライアントサイドでのバリデーション結果です。わかりやすいようにメッセージの頭に[Client]とつけるように今回実装しています。

message.png

サーバ側バリデーション

  1. TODOの文字数制限
    • 1〜200文字まで。
    • エラーの場合、「TODOは1〜200文字までで入力して下さい。」
  2. ステータスの制限

    • 0(未完了)か1(完了)
    • エラーの場合、「ステータスは0(未完了)か1(完了)で入力して下さい。」
  3. 担当者ID存在チェック

    • usersに存在するidであること
    • エラーの場合、「担当者に指定したユーザが存在しません。」
  4. オーナ、担当者以外で更新、削除(本来は権限管理の領域ですが)

    • オーナ、担当者以外で更新時、
      • 「オーナ、または担当者のみ更新可能です。」
    • オーナ以外で削除時、
      • 「オーナのみ削除可能です。」
  5. おまけ。

    • 完全に同じTODOの内容は登録付加
      • 「同じ内容のTODOが既に登録されています。」

よくあるバリデーションの実装については、CakePHPで用意されています。

  • 1〜2はCakePHPの組み込みバリデーションを利用
  • 3〜5はカスタムバリデーション関数を作成

して実装します。

クライアント側バリデーション

  • TODOの文字数制限(サーバと同じ)
    • 1〜200文字まで。
    • エラーの場合、「[Client]TODOは1〜200文字までで入力して下さい。」
  • おまけ(複数のバリデーションエラーがちゃんと実行される確認用)
    • todoは「hoge」しか入力できないようにする
    • エラーの場合、「[Client]TODOは"hoge"のみ!」
    • このバリデーションは最終的にコメントアウトします。

今回はクライアントサイドのみでできるのはこのくらいです。ちょっとさみしいですが...

実装は、Backbone.jsの、

  • バリデーションを実行する、modelのvalidate関数
  • バリデーションエラー時に発生するinvalidイベント

を使用します。

ワークショップメニュー

  • 事前準備
  • Lesson0 前回までのアプリをいじめてみる
  • Lesson1 サーバーサイド実装
  • Lesson2 クライアントサイド実装

という感じですすめます。
事前準備のあと、まずはバリデーションが全くない前回までのアプリをいじめてみます!

:large_blue_circle: 事前準備

事前準備は毎回同じなので、別エントリにまとめています。
全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。
やることは、

  • gitのブランチを整えて今回用ブランチvol/07を作成する

です。まずこれをやりましょう。

:warning: それと、第5回と第6回に不参加の方は、テーブルの修正が必要です。

  • 第5回
    • ユーザ登録用のテーブル作成(ログイン機能実装のため)
  • 第6回
    • TODO一覧テーブルへの列追加(owner列とassignee列。担当者アサイン機能実装のため)

をやってますので、それぞれ下記リンク先を参照して実施して下さい。
ユーザ登録用のテーブル作成
TODO一覧テーブルへの列追加

  • :white_check_mark: Gitのブランチを整えて、vol/07ブランチを作成。
  • :white_check_mark: テーブル修正がまだの方は修正する。

準備ができたら、Lesson0です!

:large_blue_circle: Lesson0 前回までのアプリをいじめてみる!

前回までのプログラムにはバリデーションを一切実装していません。
クライアント側で最低限のこと(MaxLengthを設定するとか)はしていますが、POSTMANでリクエストすれば簡単にデータを壊したり、Internal Server Errorを起こしたり出来てしまいますので、実際にやってみましょう。

POSTMANで試す

下記をやってみましょう。

  • :white_check_mark: TODOの文字数オーバ
    • 500(Internal Server Error)発生
  • :white_check_mark: TODOが空
    • 500(Internal Server Error)発生
  • :white_check_mark: ステータスを9とか
    • 仕様上0か1のみだが登録される
  • :white_check_mark: 存在しない担当者ID
    • 本来エラーにしたいが登録できてしまう
  • :white_check_mark: オーナ、担当者以外で更新
    • 本来エラーにしたいが更新できてしまう
  • :white_check_mark: オーナ以外で削除
    • 本来エラーにしたいが更新できてしまう

:warning:参考

POSTMANにURLやデータを設定する時、chromeのデベロッパーツールを使うと楽です。
下図はTODOアプリでTODOの更新を行った時のリクエスト・レスポンス等の表示画面です。
番号の通り操作すればURLや送信データがコピペ出来ますので、コピペした内容を修正して実行しましょう。

devtool.png

うまくいったら、Lesson1へ!

:large_blue_circle: Lesson1 サーバーサイド実装

Lesson1では、サーバサイドのプログラムを実装します。
「POSTMAN」を使用してバリデーション実装後のAPIの動作の変化を確認します。

ポイント:バリデーションの記述方法

Modelにルールを記載します。それだけです!
後は、model->save()の実行時に自動で設定したルールに基いてバリデーションが実行され、エラーがあればmodel->validationErrorsにエラーメッセージが設定されます。

サンプル:モデルの実装

バリデーションルール
class TodoList extends AppModel {

省略

public $validate = array (
    //TODOのチェック
    'todo' => array (
        //文字数
        'rule1' => array (
            'rule' => array(
                'between', 1, 200
            ),
            'message' => '1〜200文字までで入力して下さい。'
        ),

        以下同様のフォーマットでルールを追記していく
  • 'todo' チェックする項目のキーを設定しています。これに配列で設定を入れます。
    • 'rule1' ルールにつける名前です。項目キー内でユニークであれば何でも良いです。
      • 'rule' 設定するルールの関数や引数指定します。ここではCakePHP組み込みのbetweenを指定しています。
        • 'between', 1, 200 CakePHP組み込みのbetween関数を引数に1200を与えて実行するという設定です。これで、todoの文字数は1〜200以外はエラーという設定になります。
      • 'message' エラー時に設定するメッセージです。

プログラムの流れは下記のようになります。

  • コントローラでモデルのsave()を実行
  • エラーが発生した場合(save()の戻り値がfalse)にモデルのvalidationErrorsを確認
  • エラーメッセージをクライアントに返す、という流れになります。
(参考)model->validationErrorsに設定されるバリデーションエラーメッセージのフォーマット
{
    "todo": [
        "1〜200文字までで入力して下さい。"
    ],
    "id": [
        "オーナ、または担当者のみ更新可能です。"
    ]
}

ポイント:削除時のバリデーション実行

Modelのsave実行時にバリデーションは実行されますが、delete時には実行されないので、
「オーナ以外はTODOの削除ができない」という仕様は、Controller内で実行しています。
バリデーションロジック自体はModelに実装しています。

ポイント:エラーの返し方

コントローラで、モデルのvalidationErrorsをチェックしてエラーがあればクライアントにその情報を返すのですが、
HTTPステータス400を返すことでクライアントにエラーレスポンスであることを知らせます。
400は、Bad Requestで、クライアントからの要求内容に問題があることを知らせるレスポンスです。
バリデーションエラーの場合は400を返すのが妥当です。

HTTPステータス400の設定方法
$this->response->statusCode(400);

コントローラ側で実装しますが、細かいところは後ほど見ていきます。

では、各ファイルの修正点を確認していきましょう。

編集するファイル一覧

編集 file 編集概要
修正 app/Model/TodoList.php バリデーションルールの設定
修正 app/Controller/TodoListsController.php バリデーション結果の確認とメッセージ編集

TodoList.php

app/Model/TodoList.php
 <?php
-
 App::uses('AppModel', 'Model');
+App::uses('User', 'Model');
+App::uses('AuthComponent',  'Controller/Component');

 class TodoList extends AppModel {
    public $belongsTo = array (
        'Owner' => array (
            'className' => 'User',
            'foreignKey' => 'owner',
        ),
        'Assignee' => array (
            'className' => 'User',
            'foreignKey' => 'assignee'
        )
    );
+   public $validate = array (
+       //TODOのチェック
+       'todo' => array (
+           //文字数
+           'rule1' => array (
+               'rule' => array(
+                   'between', 1, 200
+               ),
+               'message' => '1〜200文字までで入力して下さい。'
+           ),
+           //同内容禁止
+           'rule2' => array (
+               'rule' => 'isUnique',
+               'message' => '同じ内容のTODOが既に登録されています。'
+           )
+       ),
+       //ステータスのチェック
+       'status' => array (
+           'rule1' => array (
+               //0 or 1 (正規表現)
+//                 'rule' => array(
+//                     'custom',
+//                     '/^[01]$/'
+//                 ),
+               //0 or 1 (候補値列挙)
+               'rule' => array(
+                   'inList',
+                   array(0, 1)
+               ),
+               'message' => 'ステータスは0(未完了)か1(完了)で入力して下さい。'
+           ),
+       ),
+       //担当者のチェック
+       'assignee' => array (
+           //idがusersテーブルに存在するか
+           'rule1' => array (
+               'rule' => array(
+                   'existsUser'
+               ),
+               'message' => '担当者に指定したユーザが存在しません。'
+           ),
+       ),
+       //権限
+       'id' => array (
+           //自分がオーナ、まはた担当者かチェック
+           'rule1' => array (
+               'rule' => array(
+                   'isOwnerOrAssignee'
+               ),
+               'message' => 'オーナ、または担当者のみ更新可能です。'
+           ),
+       )
+   );
+
+   //独自バリデーションルール
+   //担当者として指定されたIDがusersテーブルに存在するかチェックする
+   public function existsUser($userId){
+       $userModel = new User();
+       $count = $userModel->find('count', array('conditions'=>array('id'=>$userId), 'recursive' => -1));
+       return $count > 0;
+   }
+
+   //独自バリデーションルール
+   //自分がオーナまたは担当者かチェックする
+   public function isOwnerOrAssignee($id){
+       if(!isset($id)){
+           //新規登録の場合は関係なし
+           return true;
+       }
+       $me = AuthComponent::user();
+       $todo = $this->findById($id);
+       if($todo){
+           if($todo['TodoList']['owner'] === $me['id']
+               || $todo['TodoList']['assignee'] === $me['id']){
+               return true;
+           }
+       }
+       return false;
+   }
+
+   //独自バリデーションルール
+   //自分がオーナかチェックする
+   public function isOwner($id){
+       if(!isset($id)){
+           //新規登録の場合は関係なし
+           return true;
+       }
+       $me = AuthComponent::user();
+       $todo = $this->findById($id);
+       if($todo){
+           if($todo['TodoList']['owner'] === $me['id']){
+               return true;
+           }
+       }
+       return false;
+   }
 }
  • 下記のCakePHP組み込みのバリデーションルールを適用しています。
適用対象項目 ルール チェック内容
todo between 項目の長さが指定した範囲にあるか
todo isUnique 項目値がデータベース内でユニークか
status inList 項目値が指定した候補のいずれかであるか
status custom 項目値がしていした正規表現にマッチするか

:warning: 上記の他にも組み込みバリデーションルールはたくさん有ります。
こちらを参照→データバリデーション — CakePHP Cookbook 2.x ドキュメント

  • さらに、下記のカスタムバリデーション関数を実装しています。
適用対象項目 カスタムバリデーション関数 チェック内容
assignee existsUser 項目の長さが指定した範囲にあるか
id isOwnerOrAssignee 更新前のデータを確認し、ログイン中ユーザがオーナもしくは担当者であるか確認する
:warning:削除時 isOwner 更新前のデータを確認し、ログイン中ユーザがオーナであるか確認

:warning: isOwner関数は削除時に実行するので、モデル内のバリデーションルールには記載せずコントローラから実行します。

TodoListsController.php

バリデーション結果を確認してメッセージを編集、クライアントに返す実装です。

:warning:今回のテーマとは関係ないところをちょっと触っています。
  ※前回、view()メソッドでfieldを指定していなかったので追加しています。

TodoListsController.php
class TodoListsController extends AppController {
+   private $fields = array (
+       'TodoList.id',
+       'TodoList.todo',
+       'TodoList.status',
+       'Owner.id',
+       'Owner.name',
+       'Assignee.id',
+       'Assignee.name'
+   );

    public function index() {
        $query = array (
-           'fields' => array (
-               'TodoList.id',
-               'TodoList.todo',
-               'TodoList.status',
-               'Owner.id',
-               'Owner.name',
-               'Assignee.id',
-               'Assignee.name'
-           ),
+           'fields' => $this->fields,
            'order' => "TodoList.id"
        );
        $res = $this->TodoList->find('all', $query);
        // 整形
        if (count($res) > 0) {
            $loginUserId = $this->Auth->user()['id'];
            foreach ( $res as $key => $row ) {
                //「ログインユーザがオーナである」フラグ
                $res[$key]['TodoList']['owned'] = $row['Owner']['id'] === $loginUserId;
                //「ログインユーザが担当である」フラグ
                $res[$key]['TodoList']['assigned'] = $row['Assignee']['id'] === $loginUserId;
            }
        }
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

    public function view($id = null) {
-       $res = $this->TodoList->findById($id);
+       $res = $this->TodoList->findById($id, $this->fields);
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

    public function add() {
        $data = $this->request->data;
        $data['owner'] = $this->Auth->user()['id'];
        $res = $this->TodoList->save($data);
-       $this->set(compact('res'));
-       $this->set('_serialize', 'res');
+       $response = $this->editResponse($res);
+       $this->set(compact('response'));
+       $this->set('_serialize', 'response');
    }
    public function delete($id) {
-       $res = $this->TodoList->delete($id, false);
-       $this->set(compact('res'));
-       $this->set('_serialize', 'res');
+       //オーナかどうかチェック
+       if(!$this->TodoList->isOwner($id)){
+           $this->setStatusValidationError();
+           $response = $this->editErrors('オーナのみ削除可能です。');
+       }else{
+           $res = $this->TodoList->delete($id, false);
+           $response = $this->editResponse($res);
+       }
+       $this->set(compact('response'));
+       $this->set('_serialize', 'response');
    }

    public function edit($id) {
        $this->TodoList->id = $id;
        $data = $this->request->data;
        $res = $this->TodoList->save($this->request->data);
        $res = !empty($res);
-       $this->set(compact('res'));
-       $this->set('_serialize', 'res');
+       $response = $this->editResponse($res);
+       $this->set(compact('response'));
+       $this->set('_serialize', 'response');
+   }
+
+   //レスポンスを編集
+   private function editResponse($res){
+       if($res){
+           $response = $res;
+       }else{
+           $this->setStatusValidationError();
+           $respnse = array();
+           if(count($this->TodoList->validationErrors) > 0){
+               $response = $this->editErrors($this->TodoList->validationErrors);
+           }else{
+               $response = $this->editErrors('エラーが発生しました。');
+           }
+       }
+       return $response;
+   }
+
+   //バリデーションエラー時はレスポンスを400に設定
+   private function setStatusValidationError(){
+       $this->response->statusCode(400);
+   }
+
+   //エラーメッセージを編集
+   private function editErrors($errors){
+       if(is_array($errors)){
+           $res['errors'] = $errors;
+       }else{
+           $res['errors']  = array('error' => array($errors));
+       }
+       return $res;
    }

 }
  • add`メソッドとedit`メソッド

モデルのsaveメソッド実行後、editResponseメソッドを実行しています。

  • editResponse`メソッド

saveメソッドからの戻り値がtrueでない場合、modelのvalidationErrorsプロパティをチェックし、バリデーションエラーがあったかどうか確認しています。
バリデーションエラーがあった場合、HTTPステータス400を設定し、さらに下記のフォーマットでエラー返却用のJSONを編集しています。

エラー返却JSONフォーマット
{
    "errors": {
        "項目キー": [
            "エラーメッセージ"
        ],
        "項目キー": [
            "エラーメッセージ"
        ],
         以下同様 
    }
}
エラー返却JSONのサンプル
{
    "errors": {
        "todo": [
            "1〜200文字までで入力して下さい。"
        ],
        "status": [
            "ステータスは0(未完了)か1(完了)で入力して下さい。"
        ],
        "id": [
            "オーナ、または担当者のみ更新可能です。"
        ]
    }
}

実装

では、実際に修正して画面の動作確認をしてみましょう。

  • :white_check_mark: app/Model/TodoList.phpを上記の通り修正。
  • :white_check_mark: app/Controller/TodoListsController.phpを上記の通り修正。
  • :white_check_mark: 動作確認!
    • Lesson0でやったことと同じことをしてみましょう。
      • :white_check_mark: TODOの文字数オーバ
        • 1〜200文字までで入力して下さい。が返る
      • :white_check_mark: TODOが空
        • 1〜200文字までで入力して下さい。が返る
      • :white_check_mark: ステータスを9とか
        • ステータスは0(未完了)か1(完了)で入力して下さい。が返る
      • :white_check_mark: 存在しない担当者ID
        • 担当者に指定したユーザが存在しません。が返る
      • :white_check_mark: オーナ、担当者以外で更新
        • オーナ、または担当者のみ更新可能です。が返る
      • :white_check_mark: オーナ以外で削除
        • オーナのみ削除可能です。が返る
  • :white_check_mark: Gitにコミット

:warning: GitHubでのdiff表示へのリンク

第7回 Lesson1 サーバサイド · suzukishouten-study/rest-study@b61fdf7

:large_blue_circle: Lesson2 クライアントサイド実装

Lesson2では、クライアントサイドのプログラムを実装します。

ポイント:バリデーションの記述方法

Backbone.Modelのvalidateメソッドにチェック関数を実装します。
Backbone標準では、CakePHPの様に組み込みのバリデーション関数はありませんので、ゴリゴリとロジックを実装していきます。
validateメソッドはmodelのsetメソッドを実行すると自動で実行されますので、validateを明示的に呼び出す必要はありません。
validateメソッド内では、エラーメッセージを配列に詰め込み、それをreturnします。
validateメソッドから何かreturn(null以外)されると、modelのinvalidイベントが発火されるので、それをViewでつかまえてエラーメッセージの表示を行います。

サンプル:モデルの実装

validateメソッド内のバリデーションロジック
    validate : function(attrs) {
        var errors = [];

        //長さチェック
        var todoLength = attrs.todo.length;
        if (todoLength < 1 || todoLength > 200) {
            errors.push('[Client]TODOは1〜200文字までで入力して下さい。');
        }

        〜以下同様のフォーマットでバリデーションロジックを追記していく〜

        //errorsをreturnするとmodelのinvalidイベントが発火
        if (errors.length > 0){
            return errors;
        }else{
            return null;
        }
    }
  • var errors[]; エラーメッセージを格納する配列です。
    • //長さチェックの部分のロジック チェックロジックを自分でコーディングします。
      • errors.push('[Client]TODOは1〜200文字までで入力して下さい。'); エラーメッセージを詰め込みます。
    • return errors errorsの長さが0より大きいか判断し、そうであればerrorsをreturnします。これでmodelのinvalidイベントが発火されます。

ポイント:サーバでのバリデーションエラー時のエラーメッセージ表示

サーバ側でのバリデーションエラー時はHTTPステータス400が返ってきます。
前回までのプログラムでも、app.js内でサーバとの通信時の200以外のステータスはハンドリングしていましたので、400だった場合の処理を修正します。
詳しくはapp.jsのところで説明します。

Viewの実装も後ほど見ていきます。

では、各ファイルの修正点を確認していきましょう。

編集するファイル一覧

編集 file 編集概要
修正 app/webroot/js/models/todo-model.js クライアント側バリデーションロジックの実装
修正 app/webroot/js/views/todo-composite-view.js クライアント側バリデーション結果の確認とメッセージ編集
修正 app/webroot/js/views/todo-detail-item-view.js クライアント側バリデーション結果の確認とメッセージ編集
修正 app/webroot/js/app.js サーバ側のバリデーションエラーのハンドリング

todo-model.js

バリデーションロジックを実装します。

todo-model.js
 //Todoデータ1件を表すモデル
 define(function() {
    var TodoModel = Backbone.Model.extend({
        urlRoot : '/rest-study/todo_lists',
        parse : function(response) {
            //モデルをパース
            console.log("モデルをパース");
            console.log(response);
            var parsed = response.TodoList;
            if (response.Owner) {
                parsed.Owner = response.Owner;
                parsed.Assignee = response.Assignee;
            }
            return parsed;
        },
        toggle : function() {
            this.set('status', this.get("status") === '1' ? '0' : '1');
            this.save();
+       },
+       validate : function(attrs) {
+           var errors = [];
+           
+           //長さチェック
+           var todoLength = attrs.todo.length;
+           if (todoLength < 1 || todoLength > 200) {
+               errors.push('[Client]TODOは1〜200文字までで入力して下さい。');
+           }
+
+           //実験!
+           if(attrs.todo !== 'hoge'){
+               errors.push('[Client]TODOは"hoge"のみ!');
+           }
+
+           if (errors.length > 0){
+               return errors;
+           }else{
+               return null;
+           }
        }
    });
    return TodoModel;
  • validateメソッド

ロジックに関しては特に説明はいらないかと思います。
attrs変数に入力値が渡されるので、それをチェックするロジックを実装しています。
エラーがあればerrors変数を、なければnullをreturnしています。
※エラーがない場合return自体を書かなくても正常に動作します。

todo-composite-view.js

TODOの追加の際、モデルでのバリデーションエラー発生時に発火する、invalidイベントをハンドリングします。

todo-composite-view.js
 //Todo一覧表示用ビュー
 define(function(require) {
    var TodoItemView = require('views/todo-item-view');
+   var TodoModel = require('models/todo-model');

    var TodoCompositeView = Marionette.CompositeView.extend({
        template: '#todo-composite-template',

        childView : TodoItemView,

        childViewContainer : 'tbody',

+       newTodoModel : new TodoModel(),
+       
        ui : {
            addTodo : '#addTodo',
            newTodo : '#new-todo',
            userList : '#user-list'
        },

        events : {
            'click @ui.addTodo' : 'onCreateTodo',
        },

        initialize: function(options){
            _.bindAll( this, 'onCreatedSuccess' );
            this.userList = options.userList;
+           this.listenTo(this.newTodoModel, 'invalid', this.renderErrorMessage);
        },

        onRender : function() {
            //ユーザ一覧を表示
            this.showUserList(this.ui.userList, this.userList);
            //ログインユーザをデフォルトで選択状態にする
            this.ui.userList.val(window.application.loginUser.id);
        },

        //ユーザ一覧を表示
        showUserList : function($list, userList){
            $.each(userList, function(index, userModel) {
                $list.append(
                    "<option value='" 
                    + userModel.attributes.id + "'>"
                    + userModel.attributes.name + "</option>");
            });
        },

        onCreateTodo : function() {
-           this.collection.create(this.newAttributes(), {
+           this.newTodoModel.clear({silent : true});
+           this.newTodoModel.set(this.newAttributes());
+           this.collection.create(this.newTodoModel, {
                  silent:  true ,
                  success: this.onCreatedSuccess
            });
            this.ui.newTodo.val('');
        },

        newAttributes : function() {
            return {
                todo : this.ui.newTodo.val().trim(),
                status : 0,
                assignee : this.ui.userList.val()
            };
        },

        onCreatedSuccess : function(){
            this.collection.fetch({ reset : true });
        },
-
+       
+       //エラー表示
+       renderErrorMessage : function(errors){
+           var message = '';
+           for(var key in errors.validationError){
+               message += errors.validationError[key];
+           }
+           alert(message);
+       }
    });
    return TodoCompositeView;
 }); 
  • newTodoModel : new TodoModel(),
    • バリデーションに使用するためのmodelを生成します。
    • そのため、モデルクラスのrequireも追加しています。
  • initialize関数
    • listenTo関数を使用して、上記で定義したmodelのinvalidイベントのハンドラ関数(this.renderErrorMessage)を設定しています。
  • onCreateTodo関数
    • 元々は単にthis.collection.createのみ実行していただけですが、modelのvaliateを実行させるため、set関数でモデルの値をセットしています。また、その前にmodelの内容をclear関数でクリアしています。
  • renderErrorMessage関数
    • modelのinvalidイベントのハンドラです。メッセージを編集してalertで表示しています。

todo-detail-item-view.js

TODOの更新の際、モデルでのバリデーションエラー発生時に発火する、invalidイベントをハンドリングします。
todo-composite-view.jsとほぼ同様の実装です。

todo-detail-item-view.js
 //詳細ビュー
 define(function() {
    var TodoDetailItemView = Marionette.ItemView.extend({

 〜 中略 〜

        //初期化
        initialize: function(options){
            _.bindAll( this, 'onSaveSuccess' );
            this.userList = options.userList;
+           this.listenTo(this.model, 'invalid', this.renderErrorMessage);
        },

        onRender : function() {
            //ユーザ一覧を表示
            this.showUserList(this.ui.userList, this.userList);
            //担当者を選択状態にする
            this.ui.userList.val(this.model.attributes.assignee);
        },

        //ユーザ一覧を表示
        showUserList : function($list, userList){
            $.each(userList, function(index, userModel) {
            $list.append(
                "<option value='" 
                + userModel.attributes.id + "'>"
                + userModel.attributes.name + "</option>");
            });
        },

        //更新ボタンクリックのイベントハンドラ
        onUpdateClick : function() {
            //テキストボックスから文字を取得
            var todoString = this.ui.todoStatus.val();  // Todo
            var assigneeId = this.ui.userList.val();    // 担当者
-           this.model.save({
+           this.model.set({
                todo : todoString,
                assignee : assigneeId
-           }, {
+           });
+           this.model.save(null,{
                silent : true,
                success : this.onSaveSuccess,
            });
        },

        //キャンセルボタンクリックのイベントハンドラ
        onCancelClick : function() {
            this.backTodoLists();
        },

        //更新成功
        onSaveSuccess : function() {
            this.backTodoLists();
        },

        //TODOリスト画面に戻る
        backTodoLists : function() {
            Backbone.history.navigate('#todo-lists', true);
+
+       },
+       
+       //エラー表示
+       renderErrorMessage : function(errors){
+           var message = '';
+           for(var key in errors.validationError){
+               message += errors.validationError[key];
+           }
+           alert(message);
        }

    });
    return TodoDetailItemView;
 });

当ViewはMarionette.ItemViewを継承しているため、this.modelでmodelにアクセスできるので、modelの生成をしていないことを除きtodo-composite-view.jsとほぼ同じ実装です。

app.js

HTTPステータス400をハンドリングします。

app.js
 //Application
 console.log('load app');
 define(function(require){

 〜 中略 〜


        // ajaxのエラーを全てここでハンドリング
        ajaxErrorHandler : function(e, xhr, options , message){
            if( xhr.status === 401 ){
                this.clearLoginUser();
                // 未認証の場合ログイン画面に飛ばす
                Backbone.history.navigate('#login', {trigger : true, replace : true});
-           }else if(xhr.status >= 400 && xhr.status < 500){
+           }else if(xhr.status === 400){
+               if(xhr.responseJSON){
+                   var errors = xhr.responseJSON.errors;
+                   var msg = '';
+                   for(var key in errors){
+                       for(var idx in errors[key]){
+                           msg = msg + errors[key][idx] + '\n';
+                       }
+                   }
+                   alert(msg);
+               }else{
+                   alert(message);
+               }
+           }else if(xhr.status > 400 && xhr.status < 500){
                //ClientErrorの場合はメッセージ表示
                alert(message);
            }else if(xhr.status >= 500 && xhr.status < 600){
                //ServerErrorの場合はメッセージ表示
                alert(message);
            }
        },

    });
    return Application;
 });

元々、HTTPステータス400番台と500番台を分けてハンドリングしていましたが、400番だけを特別扱いするようにif文を追加しています。
xhr.responseJSONでサーバで編集したエラーレスポンスのJSONが取得できるので、それを編集してalertで表示しています。

実装

では、実際に修正して画面の動作確認をしてみましょう。

  • :white_check_mark: app/webroot/js/models/todo-model.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-composite-view.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-detail-item-view.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/app.jsを上記の通り修正。
  • :white_check_mark: 動作確認!
    • 画面を動かしてチェックしましょう!
  • :white_check_mark: Gitにコミット

:warning: GitHubでのdiff表示へのリンク

第7回 Lesson2 クライアントサイド · suzukishouten-study/rest-study@c0ae3e1

以上です!

次回予告

CSS・Bootstrapによるデザイン - connpassです。
見た目がきれいになってそれっぽくなりますよ!
またぜひご参加ください!

コメント/フィードバックお待ちしております。

参加者の方も、そうでない方もお気づきの点があればお願い致します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした