はじめに
本投稿は、2015/7/24に行われた、サーバー・クライアントの両面からかけるバリデーション - connpassの内容についてまとめた資料です。
今後の予定
AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass
今回は、「バリデーション」を実装します。
前回までは、変な入力に弱い「サンプルプログラム」の域を出ないものでしたが、今回でだいぶまともな「アプリケーション」になりますよ!
CakePHPのバリデーション機能、Backbone.jsのバリデーション機能を使って実装していきます。
なぜバリデーションが必要なのか?
今回、サーバサイド、クライアントサイド両方でバリデーションを行いますが、なぜ両方でやる必要があるのでしょうか。
サーバ、クライアントで「バリデーションをする理由」が違うと考えています。
サーバ、クライアント両方でバリデーションする理由
重要な事は、バリデーションはクライアントだけでは完結しないということです。
「クライアントでは一覧から選択するので、サーバ側でチェックは必要ない」
というわけではありません(現実にはよくある話かもしれませんが...)。
- クライアントでやっていることもサーバでもう一回やるのが基本
- APIを画面以外から叩かれるかもしれない(POSTMAN!)
- アプリの画面だけがクライアントと思わないこと!
- APIを画面以外から叩かれるかもしれない(POSTMAN!)
- サーバ側でしか出来ないバリデーションがある
- DBを参照する
- 日時が関係するもの
- 他のシステムとやりとりする必要があるもの
- etc...
サーバサイドでバリデーションする理由
サーバ側は、不正な入力を検知し、下記を達成するためにバリデーションを行います。
- アプリケーションを守る
- 予期しない動作(落ちたりとか...)を防ぐ
- セキュリティを確保する(XSS、SQLInjection、etc...)
- データを守る
- 不正なデータが登録されないようにする
- 値の範囲、フォーマット、エンコーディング...
- データ間の整合性
- 不正なデータが登録されないようにする
クライアントサイドでバリデーションする理由
サーバ側できちんとバリデーションが行われているとすれば、クライアントサイドでは最悪バリデーションはしなくてもいいかもしれません。
しかし、「サーバでなくてもできること」はクライアントサイドでやることで、すぐにレスポンスがありUXの向上が期待できます。
入力すればすぐ間違いがわかるといいですよね!
クライアントが持っている情報だけでできることは、クライアントでやってしまった方が良い場合も多いでしょう。
※このへんはアプリケーションの要件や環境次第ですね。クライアントサイドのバリデーションを一切やらないのも一つの選択です。
- 入力値の範囲、フォーマット
- 事前にサーバから取得した情報を用いてのチェック
- そもそも不正な値が入力できないUI(バリデーションとは違いますが...)
- 直接入力でなく選択式にする
- Maxlength等をちゃんと設定する
今回の内容
サーバサイド、クライアントサイドでそれぞれ下記のバリデーションを実装します。
こんな風にメッセージが出るようにします。
下記はクライアントサイドでのバリデーション結果です。わかりやすいようにメッセージの頭に[Client]
とつけるように今回実装しています。
サーバ側バリデーション
-
TODOの文字数制限
- 1〜200文字まで。
- エラーの場合、「TODOは1〜200文字までで入力して下さい。」
-
ステータスの制限
- 0(未完了)か1(完了)
- エラーの場合、「ステータスは0(未完了)か1(完了)で入力して下さい。」
-
担当者ID存在チェック
- usersに存在するidであること
- エラーの場合、「担当者に指定したユーザが存在しません。」
-
オーナ、担当者以外で更新、削除(本来は権限管理の領域ですが)
- オーナ、担当者以外で更新時、
- 「オーナ、または担当者のみ更新可能です。」
- オーナ以外で削除時、
- 「オーナのみ削除可能です。」
- オーナ、担当者以外で更新時、
-
おまけ。
- 完全に同じTODOの内容は登録付加
- 「同じ内容の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 クライアントサイド実装
という感じですすめます。
事前準備のあと、まずはバリデーションが全くない前回までのアプリをいじめてみます!
事前準備
事前準備は毎回同じなので、別エントリにまとめています。
全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。
やることは、
- gitのブランチを整えて今回用ブランチ
vol/07
を作成する
です。まずこれをやりましょう。
それと、第5回と第6回に不参加の方は、テーブルの修正が必要です。
- 第5回
- ユーザ登録用のテーブル作成(ログイン機能実装のため)
- 第6回
- TODO一覧テーブルへの列追加(owner列とassignee列。担当者アサイン機能実装のため)
をやってますので、それぞれ下記リンク先を参照して実施して下さい。
ユーザ登録用のテーブル作成
TODO一覧テーブルへの列追加
-
Gitのブランチを整えて、
vol/07
ブランチを作成。 - テーブル修正がまだの方は修正する。
準備ができたら、Lesson0です!
Lesson0 前回までのアプリをいじめてみる!
前回までのプログラムにはバリデーションを一切実装していません。
クライアント側で最低限のこと(MaxLengthを設定するとか)はしていますが、POSTMANでリクエストすれば簡単にデータを壊したり、Internal Server Errorを起こしたり出来てしまいますので、実際にやってみましょう。
POSTMANで試す
下記をやってみましょう。
-
TODOの文字数オーバ
- 500(Internal Server Error)発生
-
TODOが空
- 500(Internal Server Error)発生
-
ステータスを9とか
- 仕様上0か1のみだが登録される
-
存在しない担当者ID
- 本来エラーにしたいが登録できてしまう
-
オーナ、担当者以外で更新
- 本来エラーにしたいが更新できてしまう
-
オーナ以外で削除
- 本来エラーにしたいが更新できてしまう
参考
POSTMANにURLやデータを設定する時、chromeのデベロッパーツールを使うと楽です。
下図はTODOアプリでTODOの更新を行った時のリクエスト・レスポンス等の表示画面です。
番号の通り操作すればURLや送信データがコピペ出来ますので、コピペした内容を修正して実行しましょう。
うまくいったら、Lesson1へ!
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
関数を引数に1
と200
を与えて実行するという設定です。これで、todoの文字数は1〜200以外はエラーという設定になります。
-
-
'message'
エラー時に設定するメッセージです。
-
-
プログラムの流れは下記のようになります。
- コントローラでモデルの
save()
を実行 - エラーが発生した場合(
save()
の戻り値がfalse
)にモデルのvalidationErrors
を確認 - エラーメッセージをクライアントに返す、という流れになります。
{
"todo": [
"1〜200文字までで入力して下さい。"
],
"id": [
"オーナ、または担当者のみ更新可能です。"
]
}
ポイント:削除時のバリデーション実行
Modelのsave
実行時にバリデーションは実行されますが、delete
時には実行されないので、
「オーナ以外はTODOの削除ができない」という仕様は、Controller内で実行しています。
バリデーションロジック自体はModelに実装しています。
ポイント:エラーの返し方
コントローラで、モデルのvalidationErrors
をチェックしてエラーがあればクライアントにその情報を返すのですが、
HTTPステータス400
を返すことでクライアントにエラーレスポンスであることを知らせます。
400
は、Bad Request
で、クライアントからの要求内容に問題があることを知らせるレスポンスです。
バリデーションエラーの場合は400
を返すのが妥当です。
$this->response->statusCode(400);
コントローラ側で実装しますが、細かいところは後ほど見ていきます。
では、各ファイルの修正点を確認していきましょう。
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/Model/TodoList.php | バリデーションルールの設定 |
修正 | app/Controller/TodoListsController.php | バリデーション結果の確認とメッセージ編集 |
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 |項目値がしていした正規表現にマッチするか
上記の他にも組み込みバリデーションルールはたくさん有ります。
こちらを参照→データバリデーション — CakePHP Cookbook 2.x ドキュメント
- さらに、下記のカスタムバリデーション関数を実装しています。
|適用対象項目|カスタムバリデーション関数|チェック内容|
------+-----+--------
assignee |existsUser |項目の長さが指定した範囲にあるか
id |isOwnerOrAssignee |更新前のデータを確認し、ログイン中ユーザがオーナもしくは担当者であるか確認する
削除時 |isOwner |更新前のデータを確認し、ログイン中ユーザがオーナであるか確認
isOwner関数は削除時に実行するので、モデル内のバリデーションルールには記載せずコントローラから実行します。
TodoListsController.php
バリデーション結果を確認してメッセージを編集、クライアントに返す実装です。
今回のテーマとは関係ないところをちょっと触っています。
※前回、view()
メソッドでfield
を指定していなかったので追加しています。
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を編集しています。
{
"errors": {
"項目キー": [
"エラーメッセージ"
],
"項目キー": [
"エラーメッセージ"
],
〜 以下同様 〜
}
}
{
"errors": {
"todo": [
"1〜200文字までで入力して下さい。"
],
"status": [
"ステータスは0(未完了)か1(完了)で入力して下さい。"
],
"id": [
"オーナ、または担当者のみ更新可能です。"
]
}
}
実装
では、実際に修正して画面の動作確認をしてみましょう。
-
app/Model/TodoList.php
を上記の通り修正。 -
app/Controller/TodoListsController.php
を上記の通り修正。 -
動作確認!
- Lesson0でやったことと同じことをしてみましょう。
-
TODOの文字数オーバ
-
1〜200文字までで入力して下さい。
が返る
-
-
TODOが空
-
1〜200文字までで入力して下さい。
が返る
-
-
ステータスを9とか
-
ステータスは0(未完了)か1(完了)で入力して下さい。
が返る
-
-
存在しない担当者ID
-
担当者に指定したユーザが存在しません。
が返る
-
-
オーナ、担当者以外で更新
-
オーナ、または担当者のみ更新可能です。
が返る
-
-
オーナ以外で削除
-
オーナのみ削除可能です。
が返る
-
-
TODOの文字数オーバ
- Lesson0でやったことと同じことをしてみましょう。
- Gitにコミット
GitHubでのdiff表示へのリンク
第7回 Lesson1 サーバサイド · suzukishouten-study/rest-study@b61fdf7
Lesson2 クライアントサイド実装
Lesson2では、クライアントサイドのプログラムを実装します。
ポイント:バリデーションの記述方法
Backbone.Modelのvalidate
メソッドにチェック関数を実装します。
Backbone標準では、CakePHPの様に組み込みのバリデーション関数はありませんので、ゴリゴリとロジックを実装していきます。
validate
メソッドはmodelのset
メソッドを実行すると自動で実行されますので、validate
を明示的に呼び出す必要はありません。
validate
メソッド内では、エラーメッセージを配列に詰め込み、それをreturnします。
validate
メソッドから何かreturn(null以外)されると、modelのinvalid
イベントが発火されるので、それをViewでつかまえてエラーメッセージの表示を行います。
サンプル:モデルの実装
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データ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一覧表示用ビュー
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
で表示しています。
- modelの
todo-detail-item-view.js
TODOの更新の際、モデルでのバリデーションエラー発生時に発火する、invalid
イベントをハンドリングします。
todo-composite-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
をハンドリングします。
//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
で表示しています。
実装
では、実際に修正して画面の動作確認をしてみましょう。
-
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
を上記の通り修正。 -
動作確認!
- 画面を動かしてチェックしましょう!
- Gitにコミット
GitHubでのdiff表示へのリンク
第7回 Lesson2 クライアントサイド · suzukishouten-study/rest-study@c0ae3e1
以上です!
次回予告
CSS・Bootstrapによるデザイン - connpassです。
見た目がきれいになってそれっぽくなりますよ!
またぜひご参加ください!
コメント/フィードバックお待ちしております。
参加者の方も、そうでない方もお気づきの点があればお願い致します。