Các bài học lập trình ứng dụng RESTful ~ Chuyên đề phát triển Web - [Chương 7] Thực hiện validation ở cả 2 phía Server-Client


## :large_blue_circle: Chào mừng các bạn trở lại
Bài viết này được mình dịch từ bài [サーバー・クライアントの両面からかけるバリデーション - AWS上で構築するRESTfulアプリ勉強会～Web開発ワークショップ～【第7回】マニュアル](http://qiita.com/k_shimoji/items/e52ba887e58c8f517c9a) của tác giả @k_shimoji .

## :large_blue_circle: Tại sao lại cần phải kiểm duyệt (Validate)?

Tại thời điểm hiện tại, khi chúng ta đã validate thì coi như đã validate luôn cho các thao tác trên client side rồi, vậy tại sao lại phải làm cả 2.
Lý do là chúng ta thấy có sự khác biệt ở phía client, những thao tác trên client hoàn toàn có thể giúp cải thiện UX, người dùng nhận ngay kết quả kiểm duyệt, sau khi dữ liệu hợp lệ mới truyền đến server chờ xử lý, thay vì gửi hết lên server. 

### Lý do cần thực hiện validation trên cả client lẫn server.

Có một sự thật là nếu chỉ validate ở client là chưa đủ, bởi vì "Bạn chọn từ danh sách từ phía client, nhưng chưa kiểm tra nó có hợp lệ ở trến server hay không."
Và đây là điều rất thường xuyên xảy ra.

- Về cơ bản thì những gì bạn làm trên client side bạn cũng cần phải làm nó trên server side.
	- Mình có thể xử lý nó không cần qua giao diện, thông qua API (POSTMAN!)

- Đây là những validation mà không thể chỉ làm mỗi trên client side
	- Liên quan đến DB
        - Cái có giá trị date và time
        - Cần tường tác với các hệ thống khác
        - Vân vân ...

### Lý do cần thực hiện validation từ phía server

Server side cần xử lý những input không hợp lệ, validation sẽ giúp chúng ta đạt được những mục tiêu sau.

- Bảo vệ ứng dụng
	- Để ngăn chặn những hành vi không mong muốn ( hoặc thất bại...)
        - Để tăng cường bảo mật (XSS, SQLInjection, etc ...)
- Bảo vệ dữ liệu
	- Để chắc chắn những dữ liệu không hợp lệ sẽ không được gửi lên server
		- Vượt quá Phạm vi của giá trị, format, encoding ...
		- Giúp các dữ liệu được thống nhất

### Lý do cần thực hiện validation từ phía client
Phải thừa nhận rằng sau khi bạn validate ở server xong thì bạn có thể nói validation ở client side không cần thiết cho lắm. Tuy nhiên chúng ta vẫn cần những thao tác "non-server" để người dùng làm việc trên client side, qua đó có những phản hồi tốt hơn về mặt UX, nhất là việc kiểm tra ngay lập tức các lỗi điền form.
※ Ứng dụng của chúng ta đã đủ những điều kiện và môi trường, chỉ còn sự validation chỉ mới làm 1 trong 2 phía client và server.

- Giới hạn và formart của các giá trị input
- Kiểm tra việc tổng hợp và sử dụng thông tin từ server
- Đầu tiên là bạn không được nhập những dữ liệu không hợp lệ từ UI ( nhưng khác với validation, cái này là kiểm tra thôi ...)
	- Ở phần select thêm kiểm tra đã chọn giá trị nào chưa
	- Thêm giới hạn Maxlength


## :large_blue_circle: Thực hiện

Chúng ta sẽ thực hiện validation theo server-side và client-side như hướng dẫn dưới đây, kết quả sau khi thành công như sau

:warning: Dưới đây là kết quả từ client-side. Tôi thêm một tiền tố `[Client]` ở đầu các thông báo để dễ nhận biết đâu là phản hồi từ validation ở client, và làm tương tự với `[Server]` .

![demo notice.PNG](https://qiita-image-store.s3.amazonaws.com/0/94375/84fc1e3c-4aef-59f2-eb06-740187c53baa.png)


### Server-side validation

1. Giới hạn ký tự cho TODO
	- Giới hạn từ 1~200 ký tự
	- Nếu quá hoặc chưa đủ giới hạn thì xuất thông báo "[Server] TODO must have 1~200 characters."
2. Trạng thái TODO
	- 0 (chưa hoàn thành) hoặc 1 (hoàn thành)
        - Nếu lỗi, xuất "[Server] status should be entered in the 0 (incomplete) or 1 (completed)."

3. Kiểm tra sự tồn tại của ID người được phân công
	- ID có tồn tại
        - Trường hợp lỗi, xuất thông báo "The Assignee's not found."
4. Khi không phải owner thay đổi hoặc xóa người được phân công ( nhưng vẫn ở khu vực được quyền quản lý)
- Khi người sửa không phải là owner mà là người được phân công
    - "[Server] Only Owner or Assignee can update."
- Khi người xóa TODO không phải là Owner
    - "[Server] Only the owner can delete."
5. Thêm TODO
	- Toàn bộ nội dung của TODO mới trùng với một TODO đã có
		- "TODO of the same content is already registered."

Cho việc thực hiện validation, có sẵn trong CakePHP.

- 1-2 sử dụng CakePHP's built-in validation
- 3-5 tự tạo các hàm validation mới

Bây giờ bắt đầu implement thôi.

### Client-side validation

- Giới hạn ký tự cho TODO (Giống trên server)
	- Giới hạn từ 1~200 ký tự
	- Nếu quá hoặc chưa đủ giới hạn thì xuất thông báo "TODO must have 1~200 characters."
- Thêm TODO ( cho việc xác nhận, nơi mà phần lớn các validation error chạy đúng )
	- Không được phép điền todo "hoge"
        - Nếu vi phạm "[Client] TODO can not be "hoge" only!"
        - Validation này sẽ tự xuất hiện thông báo.
Và đây là tất cả những gì mình cần làm cho phần client side. Mặc dù nó khá ít...

Những gì sẽ thực hiện ở Backbone.js,

- Để thực hiện validation, chạy hàm `validate` ở model
- Tạo các sự kiện `invalid` cho mỗi validation error



### Mục lục Workshop

- Chuẩn bị
- Bài 0: xây dựng app giống như bài trước
- Bài 1: Thực hiện ở server-side
- Bài 2: Thực hiện ở client-side

Sau khi chuẩn bị, đầu tiên mình sẽ xây dựng app như sau khi hoàn thành bài viết trước, chưa có validation!

## :large_blue_circle: Chuẩn bị

Phần chuẩn bị sẽ giống nhau ở mỗi bài, vì vậy tôi đã tổng hợp nó ở một entry riêng. Các bạn vui lòng xem ở link này [All 12 times of study sessions in do I have use of Git - RESTful application workshops will be built on AWS ~ Web development workshop ~ - Qiita](http://qiita.com/k_shimoji/items/6a66dbb93b0553fa7c7f) để chuẩn bị .

- Git: Bây giờ mình sẽ tạo branch `vol/07` và làm việc trên nó.


:warning: *** Tôi xin nhắc lại ở bài 5 và bài 6, chúng ta đã có những table bị thay đổi. ***

- Bài 5
  - Tạo Table dành cho việc đăng ký thành viên ( Thực hiện cho chức năng login )
- Bài 6
  - Thêm một cột cho table TODO list ( Thêm cột Owner và cột assignee cho chức năng phân công công việc)

Các bạn nên tham khảo lại các link sau để rõ hơn.
[Tạo Table dành cho việc đăng ký thành viên](http://qiita.com/Takumi_Kasuga/items/c97e96fb91b83ed68782#t%E1%BA%A1o-table-cho-vi%E1%BB%87c-%C4%91%C4%83ng-k%C3%BD-user)
[Thêm một cột cho table TODO list](http://qiita.com/Takumi_Kasuga/items/6d7506f8dc7de511bf14#large_blue_circle-chu%E1%BA%A9n-b%E1%BB%8B)



- :white_check_mark: Tạo một branch mới tên là `vol/07`.
- :white_check_mark: Sửa table theo các bài trước.

Nếu đã sẵn sàng, chúng ta bắt đầu bài 0

## :large_blue_circle: Bài 0: Nhìn lại ứng dụng tại thời điểm hiện tại

Hiện tại ứng dụng của chúng ta chưa được thêm validation nào.
Một số cái tối thiểu ở client-side (như MaxLength) đã được thêm, tuy nhiên bạn có thể dễ dàng phá bỏ nó bằng cách gửi request lên POSTMAN, vì vậy sẽ gây ra lỗi Internal Server Error.

### Thử với POSTMAN

Hiện tại tình trạng của chương trình như sau

- :white_check_mark: Số lượng ký tự của TODO vượt quá giới hạn
	- Sinh ra lỗi 500(Internal Server Error)
- :white_check_mark: TODO là dữ liệu rỗng
	- Sỉnh ra lỗi 500(Internal Server Error)
- :white_check_mark: Status là 9
	- Chỉ cho phép 2 giá trị là 0 và 1, nhưng vẫn được lưu giá trị
- :white_check_mark: Không tồn tại ID của user
	- Bạn muốn sinh ra thông báo lỗi, nhưng người đó vẫn thao tác được
- :white_check_mark:TODO bị trùng với TODO đã tồn tại
	- Sỉnh ra lỗi 500(Internal Server Error)
- :white_check_mark: Không phải owner sửa TODO mà là assignee
	- Chúng ta muốn có thông báo lỗi, nhưng người dùng vẫn sửa được
- :white_check_mark: Người xóa TODO không phải Owner
	- Chúng ta muốn có thông báo lỗi, nhưng người dùng vẫn xóa được

Hoàn toàn chưa bắt được các lỗi này, đó là công việc của bài 1, sau khi xong bài 1 các bạn có thể test ở POSTMAN và sẽ ra kết quả như phần **Tham khảo** dưới đây

:warning: Tham khảo

Để biết URL hay dữ liệu cần set để kiểm tra vào POSTMAN, các bạn hoàn toàn có thể sử dụng chrome developer tools.
Đầu tiên các bạn mở chrome dev tool và bấm vào tab NETWORK -> XHR, thực hiện  một thao tác nào đó ( Thêm, sửa, xóa... ) và quan sát sẽ thấy ở cột Name sẽ hiện tên thao tác mình vừa thực hiện, nhấn vào đấy để quan sát, những giá trị cần chú ý tôi đã để mũi tên màu xanh.

![chúy.PNG](https://qiita-image-store.s3.amazonaws.com/0/94375/44ed2434-b945-35c0-ddc6-cfdb261c5dae.png)

Bây giờ mình sẽ lấy URL, như trong ảnh trên tôi sẽ lấy `http://54.199.231.20/rest-study/todo_lists.json`, method là POST.
Các giá trị truyền lên sẽ gồm có todo, status, và assignee ( Lần lượt là nội dung todo, trạng thái thêm vào 0 hay 1, assignee là người được phân công ).
Trường hợp thêm nội dung vẫn xịt thì bạn nên chạy API đăng nhập trước nhé, vì phải đăng nhập xong nó mới có quyền tác động vào todo list và mới nhận được giá trị Owner.
Điền đầy đủ thông tin như trong hình dưới và nhấn SEND trong POSTMAN ( đầu tiên mình thử với giá trị hợp lệ đã )

![kq.PNG](https://qiita-image-store.s3.amazonaws.com/0/94375/6b294cbf-b2af-327b-afeb-9ae40079567c.png)
Bây giờ mình test với giá trị không hợp lệ để kiểm tra như trên, để cho nhanh thì mình test việc trùng nội dung TODO bằng cách nhấn SEND thêm 1 lần nữa.

![ec.PNG](https://qiita-image-store.s3.amazonaws.com/0/94375/21710955-5ea6-b1ff-4375-f5859ad50b1a.png)
Báo lỗi ngay và luôn.

Các bạn có thể sửa nội dung TODO thành rỗng hoặc quá 200 ký tự, hoặc sửa ID của assignee thành 1 cái ID không tồn tại người dùng trong database, hoặc sửa status thành 9 chẳng hạn để test tiếp, nếu được báo lỗi nghĩa là mình đã validate thành công trên server side.

Bây giờ mình sẽ bắt đầu bài 1

## :large_blue_circle: Bài 1: Server-side validation

Ở bài 1 này chúng ta sẽ thực hiện phần server-side của chương trình.
Sử dụng "POSTMAN" để xác nhận sự thay đổi của API sau khi tiến hành validation.

### Chú ý: Mô tả phương thức validation

Nó sẽ mô tả rule cho Model, chỉ có thể!
Sau đó, validation dựa trên các rule này sẽ được tự động thêm khi hàm `model -> save()` được thực thi, Trường hợp lỗi sẽ xuất thông báo lỗi bằng `model-> validationErrors`.

#### Ví dụ: Implement vào model

```php:ValidationRules
class TodoList extends AppModel {

〜省略〜

public $validate = array (
	//TODO check
	'todo' => array (
		//Số ký tự
		'rule1' => array (
			'rule' => array(
				'between', 1, 200
			),
			'message' => 'The characters must be in the range 1~200'
		),

		〜Chúng ta sẽ tại ra các rule mới tương tự thế này〜
```

-  Tôi đã thêm mỗi key ( trong cặp key : value ) cho mỗi `'todo'` để kiểm tra. Nó sẽ thêm các thiết lập theo các trình tự.
	- `'rule1'` là tên của rule, tốt nhất là nó sẽ được mang giá trị unique trong danh sách các key.
		- `'rule'` Tôi sẽ xác định hàm và arguments của rules để set. Ở đây thì mình xác định bằng một CakePHP built-in là `between`.
			- `'between', 1, 200` với built-in `between` của CakePHP, Hàm này được thiết lập chạy bằng cách cho các giá trị `1` và `200`. Bây giờ, số lượng ký tự của mỗi TODO sẽ được thiết lập cho thông báo lỗi nếu nó nằm ngoài khoảng từ 1 đến 200.
                        - 'message' sẽ trỏ đến câu thông báo lỗi `'Đây là thông báo khi lỗi vi phạm rule này xuất hiện'`.

Mạch chương trình như sau

- Chạy hàm `save()` của model trong controller
- Nếu xuất hiện lỗi (`save()` sẽ return giá trị `false`) xác nhận tới model rằng `validationErrors`
- Return một thông báo lỗi cho client.


```json:(ThamKhảo)model->formatOfValidationErrorMessagesThatAreSetToValidationErrors
{
    "todo": [
        "The characters must be in the range 1~200"
    ],
    "id": [
        "Only owner can update,."
    ]
}
```

### Chú ý: Chạy validation khi xóa

Vì mặc dù validation khi `save` được chạy từ Model, `delete` thình thoảng không chạy,
Cần chú ý là "Chỉ Owner được xóa TODO" , mình sẽ chạy ở Controller.
Validation logic sẽ được chuyển sang Model.


### Chú ý: Cách return error

Controller kiểm tra `validationErrors` models và return thông tin này cho client nếu có lỗi nào xuất hiện,
Mình sẽ phản hồi một HTTP status `400` để chỉ ra rằng đây là một thông báo lỗi cho client..
`400` là một `Bad Request`, phản hồi này chỉ ra rằng có vấn đề xảy ra với nội dung của request từ client.
Nếu validation error mình sẽ return `400` `.

```php:CáchSetHTTPstatus400
$this->response->statusCode(400);
```

Hay xem mình sẽ thực hiện implement trên những file nào

### Chi tiết các file cần chỉnh sửa

|Thao tác|file|Mô tả|
|---|:----------------------------------|:----------------------|
|Sửa| app/Model/TodoList.php |Thêm validation rule
|Sửa| app/Controller/TodoListsController.php | Kết quả Validation của xác nhận và sửa tin nhắn


### TodoList.php

```diff: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 check
+		'todo' => array (
+			//Số ký tự
+			'rule1' => array (
+				'rule' => array(
+					'between', 1, 200
+				),
+				'message' => '[Client] The characters must be in the range 1~200'
+			),
+			//trùng với TODO đã có
+			'rule2' => array (
+				'rule' => 'isUnique',
+				'message' => '[Client] There are the same TODO exists in the TODO list.'
+			)
+		),
+		//Kiểm tra status
+		'status' => array (
+			'rule1' => array (
+				//0 or 1 (Trường hợp custom)
+// 				'rule' => array(
+// 					'custom',
+// 					'/^[01]$/'
+// 				),
+				//0 or 1 (trường hợp hợp lệ)
+				'rule' => array(
+					'inList',
+					array(0, 1)
+				),
+				'message' => 'The status value must be 0 or 1'
+			),
+		),
+		//Kiểm tra người được phân công
+		'assignee' => array (
+			//ID đã tồn tại trong table users
+			'rule1' => array (
+				'rule' => array(
+					'existsUser'
+				),
+				'message' => '[Client] This user ID is already exists'
+			),
+		),
+		//Việc sửa/xóa
+		'id' => array (
+			//Kiểm tra người vào trang detail có phải Owner hay Assignee
+			'rule1' => array (
+				'rule' => array(
+					'isOwnerOrAssignee'
+				),
+				'message' => '[Client] Only Owner or Assigner can update this TODO'
+			),
+		)
+	);
+
+	// Owner validation rules
+       // ID đã được phân công công việc, mình sẽ xem ID có tồn tại trong table users hay không
+	public function existsUser($userId){
+		$userModel = new User();
+		$count = $userModel->find('count', array('conditions'=>array('id'=>$userId), 'recursive' => -1));
+		return $count > 0;
+	}
+
+	//Owner validation rules
+	//Tự kiểm tra xem có phải Owner hay người được phân công hay không
+	public function isOwnerOrAssignee($id){
+		if(!isset($id)){
+			//Bỏ qua người đang đăng ký
+			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;
+	}
+
+	// Owner validation rules
+       // Yourself to check whether the owner
+	public function isOwner($id){
+		if(!isset($id)){
+			//Không quan tâm đến người mới đăng ký
+			return true;
+		}
+		$me = AuthComponent::user();
+		$todo = $this->findById($id);
+		if($todo){
+			if($todo['TodoList']['owner'] === $me['id']){
+				return true;
+			}
+		}
+		return false;
+	}
 }
```

- Chúng ta áp dụng validation rules của những built-in sau của CakePHP.

| Áp dụng cho | Rules | Nội dung kiểm tra |
------+-----+--------
todo | between | Độ dài mỗi TODO có nằm trong khoảng xác định hay không
todo | isUnique | Giá trị nhập vào có unique trong database hay không
status | inList | Giá trị gửi lên có nằm trong danh sách giá trị cho sẵn không
status | custom | Bạn có thể tạo ra các tùy chỉnh giá trị theo ý mình

:warning: Ngoài những cái đã liệt kê ở trên, CakePHP còn có rất nhiều built-in validation rules khác.
Các bạn có thể xem tại đây→[Data validation - CakePHP Cookbook 2.x document](http://book.cakephp.org/2.0/en/models/data-validation.html)



- Thêm vào đó, chúng ta sẽ implement mội số hàm validation như sau.

| Áp dụng với | Hàm custom validation  | Nội dung kiểm tra |
------+-----+--------
assignee  |existsUser         | Độ dài của input trong khoảng xác định
id        |isOwnerOrAssignee  | Kiểm tra dữ liệu trước khi chỉnh sửa xem user đó đã login hay chưa và có phải owner hay người được phân công hay không

:warning: Delete when | isOwner | Kiểm tra dữ liệu trước khi chỉnh sửa xem có phải là owner hay không

:warning: Vì hàm isOwner sẽ được thực thi tại thời điểm thực hiện hành động xóa, validation rules ở model và chạy ở controller không được mô tả.

#### TodoListsController.php

Kiểm tra kết quả validation và sửa thông báo để return tới client.

:warning: Bạn chỉ có một thao tác nhỏ ở đây và không cần làm gì với giao diện này cả.
　　※Hiện tại, mình đã thêm `view()` vì mình chưa xác định được `field` ở hàm.

```diff: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);
 		// Xử lý sự kiện
 		if (count($res) > 0) {
 		    $loginUserId = $this->Auth->user()['id'];
 			foreach ( $res as $key => $row ) {
 			    // trường hợp "Login user là owner" 
 				$res[$key]['TodoList']['owned'] = $row['Owner']['id'] === $loginUserId;
 			    // trường hợp "Login user là Assignee" 
 				$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('Only the owner can detete this TODO.');
+		}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');
+	}
+
+	//Sửa các phản hồi
+	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('An error occured.');
+			}
+		}
+		return $response;
+	}
+
+	// Khi gặp validation errors set response giá trị 400
+	private function setStatusValidationError(){
+		$this->response->statusCode(400);
+	}
+
+	// Sửa các thông báo lỗi
+	private function editErrors($errors){
+		if(is_array($errors)){
+			$res['errors'] = $errors;
+		}else{
+			$res['errors']  = array('error' => array($errors));
+		}
+		return $res;
 	}
 
 }
```

- Hàm `Add` và hàm `edit`

Sau khi thực thi `save` trong model, chúng we chạy hàm `editResponse`.

- Hàm `editResponse`

Nếu giá trị trả về từ hàm `save` không phải `true`, kiểm tra giá trị `validationErrors` của model, xác nhận xem có validation error nào không.
Nếu có validation error, Set HTTP status `400`, bạn phải sửa JSON cho việc return lỗi như format sau đây.

```json:ErrorReturnJSONformat
{
    "errors": {
        "item key": [
            "error messsage"
        ],
        "item key": [
            "error messsage"
        ],
        〜 tương tự 〜
    }
}
```


```json:SampleOfErrorReturnJSON
{
    "errors": {
        "todo": [
            "length of TODO must be 1~200 characters"
        ],
        "status": [
            "Status must be 0 or 1"
        ],
        "id": [
            "Only Owner or Assignee can update this TODO"
        ]
    }
}
```


### Tiến hành implement

Tóm tắt bài học và các kiểm tra

- :white_check_mark: `app/Model/TodoList.php` sửa như trên.
- :white_check_mark: `app/Controller/TodoListsController.php` sửa như trên.
- :white_check_mark: Kiểm tra kết quả.
	- Làm theo hướng dẫn của [bài 0](http://qiita.com/Takumi_Kasuga/private/b297ee3591d4a4ff48e8#large_blue_circle-b%C3%A0i-0-x%C3%A2y-d%E1%BB%B1ng-app-nh%C6%B0-khi-ho%C3%A0n-th%C3%A0nh-b%C3%A0i-tr%C6%B0%E1%BB%9Bc-)
		- :white_check_mark: Số ký tự của TODO vượt giới hạn
			- return `The lengths of TODO must in the range of 1~200 characters`
		- :white_check_mark: TODO rỗng
			-  return `The lengths of TODO must in the range of 1~200 characters`
		- :white_check_mark: Status là 9
			- return `Status must be 0 or 1`
		- :white_check_mark: Assignee User ID không tồn tại
			- return `Assignee User ID not exist`
		- :white_check_mark: Không phải Owner, Assignee thực hiện chức năng chỉnh sửa
			- return `Only Owner can update this TODO`
		- :white_check_mark: Không phải Owner thực hiện chức năng xóa
			- `Only Owner can delete this TODO`
- :white_check_mark: Commit lên Git

**:warning: Tài liệu tham khảo dạng hiển thị diff trên Github**

[7th bài 1 server side · suzukishouten-study/rest-study@b61fdf7](https://github.com/suzukishouten-study/rest-study/commit/b61fdf71eb6f0e8a3ab8c4491c0d7cbf7e35833c)
## :large_blue_circle: Bài 2: Thực hiện ở client-side

Ở bài 2 này, chúng ta sẽ thực hiện việc implement ở client-side

### Chú ý: Mô tả các hàm validation

Chúng ta sẽ thêm một hàm check cho `validate` method của Backbone.Model
Trong nền tảng của Backbone không có các built-in để validation như trong CakePHP, Tôi sẽ tiếp tục implement logic.
Bởi vì `validate` method được thực thi tự động khi bạn chạy `set` method của model, bạn không cần phải gọi `validate` nữa.
Tại `validate` method, tổng hợp tất cả các thông báo lỗi vào một mảng, sau đó return chúng.
Mỗi lần cái gì đấy được return từ `validate` method (not-null), vì sự kiện `invalid` của model đa chạy xong , tạo thông báo lỗi và hiển thị chúng lên view.

#### Ví dụ: implementation của Model

```js:ValidationLogicInValidateMethod
	validate : function(attrs) {
		var errors = [];
		
		//Kiểm tra độ dài
		var todoLength = attrs.todo.length;
		if (todoLength < 1 || todoLength > 200) {
			errors.push('[Client]TODO length must be 1~200 characters.');
		}

		〜Mình sẽ tiếp tục thêm các validation logic với formart tương tự〜

		//return error khi sự kiện invalid của model xảy ra
		if (errors.length > 0){
			return errors;
		}else{
			return null;
		}
	}
```

- `var errors[];` Mảng sẽ chứa các thông báo lỗi
	- `//the logic of check length` Đây là phần code logic kiểm tra độ dài.
		- `Errors.push('[Client] TODO must be in the length of 1~200 characters.');` Hiển thị thông báo lỗi.
        - `Return errors` length của erros phải lớn hơn giá trị mình đã set từ đầu đó là 0, sau đó trả về tất cả với điều kiện nếu có `errors`. Nếu This của model là `invalid` thì event xảy ra.


### Chú ý: Thông báo lỗi sẽ được hiển tại tại thời điểm validation error trên server

Khi có validation error trên server side, returned HTTP status `400`.
Trên chương trình của chúng ta hiện tại, status không phải `200` khi giao tiếp với server qua `app.js` đều đã được xử lý ( handle ), sửa các tiến trình mà xảy ra status `400`.
Những thông tin chi tiết hơn, tôi sẽ giải thích ở  `app.js`.

Bây giờ chúng ta sẽ sửa chủ yếu ở View

### Các file cần thay đổi

|Thao tác|file|Mô tả|
|---|:----------------------------------|:----------------------|
|Sửa| app/webroot/js/models/todo-model.js |Implementation của client-side validation logic
|Sửa| app/webroot/js/views/todo-composite-view.js | Kết quả validation của Việc xác nhận và chỉnh sửa TODO trên client-side
|Sửa| app/webroot/js/views/todo-detail-item-view.js |Kết quả validation của Việc xác nhận và chỉnh sửa TODO trên client-side
|Sửa| app/webroot/js/app.js | Bắt lỗi và sửa các validation error của server-side

#### todo-model.js

Implement validation logic

```diff:todo-model.js
 //Model representing County Todo data 1
 define(function() {
 	var TodoModel = Backbone.Model.extend({
 		urlRoot : '/rest-study/todo_lists',
 		parse : function(response) {
 			//Model parse
 			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 = [];
+			
+			//Kiểm tra độ dài
+			var todoLength = attrs.todo.length;
+			if (todoLength < 1 || todoLength > 200) {
+				errors.push('[Client]The TODO length must be 1〜200 characters');
+			}
+
+			//Thử nghiệm chỉ cho phép nhập giá trị hoge, bỏ comment để thử chạy
+			//if(attrs.todo !== 'hoge'){
+			//	errors.push('[Client]TODO on ly "hoge" !');
+			//}
+
+			if (errors.length > 0){
+				return errors;
+			}else{
+				return null;
+			}
 		}
 	});
 	return TodoModel;
```

- `validate` method

Tôi nghĩ không cần phải miêu tả đặc biệt đối với logic.
Bởi vì giá trị input được truyền vào biến `attrs`, cần phải implement logic để check nó.
Nếu có lỗi xảy ra sẽ return vào biến `errors` variable, nếu không sẽ truyền giá trị` null`.
※ Nó sẽ chạy đúng mà không cần phải tự return chính nó

#### todo-composite-view.js

Khi thêm TODO, tại thời điểm xảy ra validation error trên model, và bắt đầu xử lý sự kiện `invalid`.

```diff:todo-composite-view.js
 //View cho TODO list
 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() {
 			//Show user list
 			this.showUserList(this.ui.userList, this.userList);
 			//Mặc định hiển thị cho những người đã logged-in
 			this.ui.userList.val(window.application.loginUser.id);
 		},
 					
 		//Show user list
 		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 });
 		},
-
+		
+		//Error display
+		renderErrorMessage : function(errors){
+			var message = '';
+			for(var key in errors.validationError){
+				message += errors.validationError[key];
+			}
+			alert(message);
+		}
 	});
 	return TodoCompositeView;
 }); 
```

- `newTodoModel : new TodoModel(),`
	- Thêm một model để xử lý cho việc validation.
        - Sau đó thêm class `require` để require các thành phần cần thiết cho model.
- Hàm `initialize`
	- Sử dụng hàm `listenTo` sẽ thêm hàm xử lý sự kiện cho sự kiện `invalid` event của model như mô tả trên kia (`this.renderErrorMessage`).
- Hàm `onCreateTodo`
	- Bạn chỉ cần sử dụng duy nhất `this.collection.create`, tuy nhiên để chạy `valiate` của model, chúng ta cần set giá trị của model ở hàm `set`. Thêm vào đó, mình sẽ xóa nội dung của model ở hàm `clear` trước đó.
- Hàm `renderErrorMessage`
	- Đây là hàm xử lý sự kiện `invalid` của model. Sửa nội dung thông báo lỗi sẽ hiển thị ở `alert`.


#### todo-detail-item-view.js

Trong khi sửa TODO, khi validation error xảy ra ở model mình sẽ chạy hàm xử lý sự kiện `invalid`.
 todo-composite-view.js mình gần như implement tương tự.

```diff:todo-detail-item-view.js
 //View trang detail
 define(function() {
 	var TodoDetailItemView = Marionette.ItemView.extend({
 
 〜 中略 〜
 
 		//Khởi tạo
 		initialize: function(options){
 			_.bindAll( this, 'onSaveSuccess' );
 			this.userList = options.userList;
+			this.listenTo(this.model, 'invalid', this.renderErrorMessage);
 		},
 
 		onRender : function() {
 			//Show user list
 			this.showUserList(this.ui.userList, this.userList);
 			//stata dành cho người được phân công
 			this.ui.userList.val(this.model.attributes.assignee);
 		},
 
 		//Show user list
 		showUserList : function($list, userList){
 			$.each(userList, function(index, userModel) {
 			$list.append(
 				"<option value='" 
 				+ userModel.attributes.id + "'>"
 				+ userModel.attributes.name + "</option>");
 			});
 		},
 
 		//Xử lý sự kiện click nút chỉnh sửa (Update) 
 		onUpdateClick : function() {
 			//Lấy các ký tự từ checkbox
 			var todoString = this.ui.todoStatus.val();  // Todo
 			var assigneeId = this.ui.userList.val();	// Assignee
-			this.model.save({
+			this.model.set({
 				todo : todoString,
 				assignee : assigneeId
-			}, {
+			});
+			this.model.save(null,{
 				silent : true,
 				success : this.onSaveSuccess,
 			});
 		},
 
 		//Xử lý sự kiện click nút Cancel 
 		onCancelClick : function() {
 			this.backTodoLists();
 		},
 
 		//Update success
 		onSaveSuccess : function() {
 			this.backTodoLists();
 		},
 
 		//Chuyển sang màn hình TODO list
 		backTodoLists : function() {
 			Backbone.history.navigate('#todo-lists', true);
+
+		},
+		
+		//Hiển thị lỗi
+		renderErrorMessage : function(errors){
+			var message = '';
+			for(var key in errors.validationError){
+				message += errors.validationError[key];
+			}
+			alert(message);
 		}
 
 	});
 	return TodoDetailItemView;
 });
```

Bởi vì View này bạn đã thừa kế từ `Marionette.ItemView`, Vì vậy bạn có thể truy cập vào model qua `this.model`, mình implement gần như giống hệt ngoại trừ todo-composite-view.js vì đây là nơi không sinh ra model.

#### app.js

Bây giờ mình sẽ xử lý sự kiện HTTP status `400`.

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

 〜 中略 〜
  
    
 		// tất cả các xử lý sự kiện bằng ajax ở đây
 		ajaxErrorHandler : function(e, xhr, options , message){
 			if( xhr.status === 401 ){
 				this.clearLoginUser();
 				// Nếu unauthenticated bỏ qua và chuyển về màn hình login
 				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){
 				//Hiển thị thông báo từ ClientError
 				alert(message);
 			}else if(xhr.status >= 500 && xhr.status < 600){
 				//Hiển thị thông báo từ ServerError
 				alert(message);
 			}
 		},
 
 	});
 	return Application;
 });
```

Trước đây mình cần xử lý riêng biệt HTTP status `400` và `500`, chúng ta đã thêm mệnh đề if để xử lý riêng biệt riêng số `400`.
Vì sẽ nhận được JSON error response sau khi được server xử lý tại `xhr.responseJSON`, sửa nó và hiển thị nó vào `alert`.

### Thực hành

Hãy kiểm tra các bạn đã thực hiện đầy đủ các bươc sau chưa

- :white_check_mark: Sửa theo mô tả trên `app/webroot/js/models/todo-model.js`.
- :white_check_mark: Sửa theo mô tả trên `app/webroot/js/views/todo-composite-view.js`.
- :white_check_mark: Sửa theo mô tả trên `app/webroot/js/views/todo-detail-item-view.js`.
- :white_check_mark: Sửa theo mô tả trên `app/webroot/js/app.js`.
- :white_check_mark: Kiểm tra kết quả
	- Hãy kiểm tra bằng các thao tác trên trình duyệt
- :white_check_mark: Commit lên Git

**:warning: Tham khảo thay đổi dạng Diff trên Github**

[Chương 7 : Bài 2 Client-side · suzukishouten-study/rest-study@c0ae3e1](https://github.com/suzukishouten-study/rest-study/commit/c0ae3e1fb86ed98466c7dd0a4b10724b21f4ccc5)

Đó là tất cả của bài học này

## Giới thiệu
Ở bài học sau chúng ta sẽ bắt đầu sử dụng Bootstrap để làm giao diện đẹp hơn và các hiệu ứng hay ho, các bạn có thể lên trang chủ bootstrap để làm quen trước.

## Rất mong nhận được phản hồi/ góp ý của các bạn

Cảm ơn các bạn đã đi cùng với series này, mình sẽ cố gắng truyền đạt tốt nhất ý đồ của tác giả tới các bạn, tuy nhiên chắc chắn vẫn còn rất nhiều sai sót và mình sẽ update thường xuyên và chỉnh sửa các bài viết cũ, rất mong nhận dược những phản hồi quý báu của các bạn để những bài viết của mình có chất lượng hơn.