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

CakePHPでデータ操作〜担当者アサイン機能の実装 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第6回】マニュアル

More than 5 years have passed since last update.

:warning: 第5回でusersテーブルを追加しましたが、前回不参加の方に、最初伝え漏れていましたので追記いたしました(事前準備のところ)。すいませんでした!

:large_blue_circle: はじめに

本投稿は、2015/6/26に行われた、CakePHPでデータ操作〜担当者アサイン機能の実装 - connpassの内容についてまとめた資料です。

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

今回は、TODOに担当者を割り当てる機能を追加します。
前回ログイン機能を付けたので、複数ユーザで使用する感じにしてみます!

CakePHPの「アソシエーション」について学びます。

:large_blue_circle: CakePHPのアソシエーションとは?

前回まで、

  • users
  • todo_lists

の2テーブルを扱っていました。

しかし、テーブル1つの単純なSelectしかしていませんでした。
今回作成するアプリケーションでは、usersとtodo_listsを結合してSelectしたりする必要がありますが、CakePHPではこういった場合にSQLを直接書くわけではなく、「アソシエーション」という仕組みを使用します。
テーブルを結合したりするのを、SQLではなくコードで表現します。

今回学ぶアソシエーション

今回は下記3つのアソシエーションを学びます。

  • belongsTo
  • hasOne
  • hasMany

:warning: 「hasAndBelongsToMany」というのもありますが、今回は割愛します。

1つずつ説明します。

belongsTo

A belongsTo B、つまり、AはBに属している、というテーブル間の関係がある場合、belongsToアソシエーションを使用します。
例えば、「ユーザ(users)」テーブルに、「国籍」というフィールドがあり、「国籍マスタ(countries)」テーブルがあるとします。

  • countries(id, name)

とします。

userは一つの国籍をもちますので、

  • users(id, username, country_id)

という感じです。
users.country_id と countries.id で joinしてSelectすれば取得できますね。
SQLで書くとこんな感じです。

select users.id, users.username, countries.name as country_name
from users inner join countries on users.country_id = countries.id

こういう場合に使用するのが「belongsTo」アソシエーションです。
コードは後ほど見ていきます!

hasOne

hasOneは、相手側テーブルが1件、と言う意味でbelongsToの親戚みたいなものです。
belongsToの例では、users側にcountriesのレコードを示すcountry_idを持っていました。
例えば、ユーザのプロフィールを保持するテーブル(profiles)があるとします。

  • profiles(id, user_id, 自己紹介文, 好きな食べ物, etc...)

この場合、users側ではなく、profiles側にuser_idを持っていますので、
users.id と profiles.user_id で joinしてSelectすれば取得できますね。
こういう場合に使用するのが「hasOne」アソシエーションです。

:warning: users側にprofile_idをもつ、という実装も出来ますが、「プロフィールを未登録」の場合に

  • users.profile_idをnullにする実装にする
  • users.id = profies.user_idとなるprofilesのレコードがないだけ のどちらにするかと言う設計思想の違いですね。

SQLで書くとこうです。

select users.id, profiles.自己紹介文, profiles.好きな食べ物
from users inner join profiles on users.id = profiles.user_id

コードは後ほど見ていきます!

hasMany

hasManyは1:nの関係を表す場合に使用します。
例えば、ユーザの職歴を保持するテーブル(careers)があるとします。

  • careers(id, user_id, 入社年度, 会社名, etc...)

この場合、usersは複数の職歴(career)レコードを持てるので、
users.id と careers.user_id で joinしてSelectすれば取得できますね。
hasOneとの違いは、この時複数件のレコードがとれてくる、と言う部分のみです。

SQLで書くと、通常はこうです。

select users.id, careers.入社年度, careers.会社名
from users inner join careers on users.id = careers.user_id

:warning:実際にはCakePHPの場合、usersのselectとcareersのselectがそれぞれ一回、計2回のselectが発行される実装になっています。

コードは後ほど見ていきます!

:large_blue_circle: 今回の内容

今回は、以下の機能を実現します。

機能リスト

  • Todoの追加
    • TODOの内容に加え「そのTODOを誰が担当するか」を全ユーザのリストから選択して指定する
    • ユーザのリストは、ログインユーザがデフォルトで選択状態とする。
  • TODO一覧の表示
    • TODO追加を実行したユーザを「オーナ」、TODOを割り当てたユーザを「担当」として表示します。
    • 下記の操作制限を実装します。
      • 「削除」はオーナのみ可能。ログインユーザがオーナでないTODOの行の「削除」リンクは表示しない。
      • 「詳細」の表示はオーナまたは担当のみ可能。それ以外の場合は「詳細」リンクは表示しない。
  • TODO詳細
    • 担当者の変更を可能とする。

画面

  • TODO一覧画面

todo_lists.png

  • TODO詳細画面

todo_detail.png

:large_blue_circle: 事前準備

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

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

です。まずこれをやりましょう。
それと、前回usersテーブルを追加しましたが、前回不参加の方はそっちもやっておく必要があります。こちらを参照下さい。

今回はさらに、
todo_listsテーブルに、

  • オーナのユーザID(owner)
  • 担当のユーザID(assignee)

を追加します。

TODO一覧テーブルへの列追加

まず、phpMyAdminにログインしましょう。

URLは、http://(PublicIP)/phpMyAdmin/です。
変更していなければ、ユーザ名study、パスワードstudyでログインできます。

各列の設定
名前 データ型 長さ/値 デフォルト値 照合順序 属性 NULL インデックス A_I(AutoIncrement) コメント
owner INT 10 なし   UNSIGNED       オーナ
assignee INT 10 なし   UNSIGNED       担当

準備

では、準備を始めましょう!

  • :white_check_mark: Gitのブランチを整えて、vol/06ブランチを作成。
  • :white_check_mark: TODO一覧(todo_lists)テーブルに列を追加。

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

:large_blue_circle: Lesson1 サーバサイド

Lesson1では、サーバサイドのプログラムを実装します。
プログラムのテストには、第1回でインストールした「POSTMAN」を使用してAPIの動作確認を行います。

ポイント

今回のテーマは「アソシエーション」ですが、前述の3つのうち、実は「belongsTo」だけで今回の機能は実現できてしまいます。
それだけでは面白く無いので、「hasOne」と「hasMany」も試してみます。
まずは、belognsToを使用して、アプリケーションの実装をします。

編集するファイル一覧

編集 file 編集概要
修正 app/Controller/TodoListsController.php findのパラメータの設定
修正 app/Model/TodoList.php アソシエーションの設定
修正 app/Controller/UsersController.php ユーザ一覧の取得

TodoListsController.php

TodoListモデルのfind関数実行じのパラメータを設定します。

TodoListsController.php
〜略〜
 class TodoListsController extends AppController {

    public function index() {
-       $res = $this->TodoList->find('all');
+       $query = array (
+           'fields' => array (
+               'TodoList.id',
+               'TodoList.todo',
+               'TodoList.status',
+               'Owner.id',
+               'Owner.name',
+               'Assignee.id',
+               'Assignee.name'
+           ),
+           '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);
        $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');
    }

〜以下略〜

find関数に引数追加

  • 第二引数$queryを追加しています。$queryは配列で、下記を設定しています。
    • fields
      • 取得するテーブルの列。これを指定しないと、ユーザのパスワード等もとれてしまうため、必要な物のみ設定しています。
    • order
      • ついでにデータの並び順も指定しました。todo_listsテーブルのidの昇順です。

取得したデータの整形

  • 取得した1件のレコードについて、
    • ログインユーザがオーナである場合、owned = trueに設定、そうでない場合owned = falseに設定。
    • ログインユーザが担当である場合、assigned = trueに設定、そうでない場合assigned = falseに設定。

TodoList.php

アソシエーションの設定

TodoList.php
 class TodoList extends AppModel {
+   public $belongsTo = array (
+       'Owner' => array (
+           'className' => 'User',
+           'foreignKey' => 'owner',
+       ),
+       'Assignee' => array (
+           'className' => 'User',
+           'foreignKey' => 'assignee'
+       )
+   );
 }

belongsToアソシエーション

  • TODO一件に対する、
    • オーナのユーザ情報
    • 担当のユーザ情報

を設定しています。詳細は下記の通り。

  • モデルの変数に$belongsToという変数名で配列を設定します。
    • Owner(オーナ), Assignee(担当)という名前で2つのusersテーブルを紐つけています。
    • その下のパラメータ詳細(Owner)
      • className -> モデル名(User)
      • usersに対する外部キー(todo_lists.ownerを指定)
    • その下のパラメータ詳細(Assignee)
      • className -> モデル名(User)
      • usersに対する外部キー(todo_lists.assigneeを指定)

addメソッド

  • ownerにログインユーザのIDを設定しています。
    • これで、TODOの追加操作をした際のログインユーザがオーナとして設定されます。

UsersController.php

ログイン、ログイン済みチェック、ログアウト、ユーザ登録の実装です。

UsersController.php
 class UsersController extends AppController {

+   public function index() {
+       $res = $this->User->find('all', array (
+           'fields' => array (
+               'User.id',
+               'User.username',
+               'User.name'
+           )
+       ));
+       $this->set(compact('res'));
+       $this->set('_serialize', 'res');
+   }
+
    public function login() {
        $user = $this->Auth->user();
        $res = array();

〜以下略〜

index関数

  • TodoListController.phpと同じ容量で、find関数で取得するフィールドを絞っています。

 動作確認

POSTMANで、TODO一覧取得API(http://(PublicIP)/rest-study/todo_lists.json)をテストします。
下記のように取得できればOKです。
belongsToが効いています。

[
    {
        "TodoList": {
            "id": "62",
            "todo": "牛乳を買う",
            "status": "0",
            "owned": true,
            "assigned": true
        },
        "Owner": {
            "id": "67",
            "name": "山田太郎"
        },
        "Assignee": {
            "id": "67",
            "name": "山田太郎"
        }
    },
    {
        "TodoList": {
            "id": "78",
            "todo": "犬の散歩",
            "status": "0",
            "owned": true,
            "assigned": false
        },
        "Owner": {
            "id": "67",
            "name": "山田太郎"
        },
        "Assignee": {
            "id": "68",
            "name": "田中花子"
        }
    },
    {
        "TodoList": {
            "id": "83",
            "todo": "家に電話",
            "status": "1",
            "owned": false,
            "assigned": false
        },
        "Owner": {
            "id": "68",
            "name": "田中花子"
        },
        "Assignee": {
            "id": "68",
            "name": "田中花子"
        }
    }
]

実装

では、実際に修正して上記のテストをしてみましょう。

  • :white_check_mark: app/Controller/TodoListsController.php を上記の通り修正。
  • :white_check_mark: app/Model/TodoList.php を上記の通り修正。
  • :white_check_mark: app/Controller/UsersController.php を上記の通り修正。
  • :white_check_mark: 動作確認!
  • :white_check_mark: Gitにコミット

GitHubでのdiff表示へのリンク
第6回 Lesson1 サーバサイド · suzukishouten-study/rest-study@d4fe1a4

Lesson1.5へ!

:large_blue_circle: Lesson1.5 他のアソシエーションを試す。

今回実装するアプリケーションとしてはLesson1の実装でOKです。
Lesson2のクライアントの実装に入る前に、hasOnehasManyのアソシエーションも試してみましょう。

ソースを修正して動作確認後、元に戻してLesson2へ行きましょう。

テストでは、下記のデータの状態で実行した結果を載せています。

testdata.png

:warning:ユーザは、

  • 山田太郎
  • 田中花子
  • 佐藤一郎

が登録されていますが、佐藤一郎さんはまだTODOが1件もない(自分がオーナのものも、担当としてアサインされたものも)状態です。

hasOneをテストする

あるユーザがオーナとなっているTODOを取得するケースを考えます。
:warning:user : todo1 : nになるので本来はhasManyを使用する場面です。
ちょっと無理矢理ですが、「田中花子さん」がオーナのTODOは1件なので、田中花子さんのTODOがちゃんと取れるか確認してみましょう。

ソース修正箇所

app/Controller/UsersController.phpindex関数を修正。
app/Controller/UsersController.php
    public function index() {
        $res = $this->User->find('all');    //fieldsの指定を外す。下記はコメントアウト
//      $res = $this->User->find('all', array (
//          'fields' => array (
//              'User.id',
//              'User.username',
//              'User.name'
//          )
//      ));
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

app/Model/User.phpにアソシエーションを設定。

app/Model/User.php
class User extends AppModel {
    public $hasOne = array (
        'TodoList' => array (
            'className' => 'TodoList',
            'foreignKey' => 'owner'
        )
    );
}

さて、ユーザ取得API(http://(PublicIP)/rest-study/users.json)を叩いてみましょう!

実行結果
[
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "山田太郎"
        },
        "TodoList": {
            "id": "62",
            "todo": "牛乳を買う",
            "status": "0",
            "owner": "67",
            "assignee": "67"
        }
    },
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "山田太郎"
        },
        "TodoList": {
            "id": "78",
            "todo": "犬の散歩",
            "status": "0",
            "owner": "67",
            "assignee": "68"
        }
    },
    {
        "User": {
            "id": "68",
            "username": "tanaka",
            "password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
            "name": "田中花子"
        },
        "TodoList": {
            "id": "83",
            "todo": "家に電話",
            "status": "1",
            "owner": "68",
            "assignee": "68"
        }
    },
    {
        "User": {
            "id": "69",
            "username": "sato",
            "password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
            "name": "佐藤一郎"
        },
        "TodoList": {
            "id": null,
            "todo": null,
            "status": null,
            "owner": null,
            "assignee": null
        }
    }
]

ユーザに対して1件のTODOが紐ついていますね!
1件もTODOを持っていない場合は、各項目がnullの配列が一個出来るようです。

この時実行されたSQLは下記です。hasManyでは一回の実行でSQLが2回実行されます。

SELECT
    `User`.`id`,
    `User`.`username`, 
    `User`.`password`, 
    `User`.`name`, 
    `TodoList`.`id`, 
    `TodoList`.`todo`, 
    `TodoList`.`status`, 
    `TodoList`.`owner`, 
    `TodoList`.`assignee` 
FROM `study`.`users` AS `User` 
        LEFT JOIN `study`.`todo_lists` AS `TodoList` ON (`TodoList`.`owner` = `User`.`id`)  
WHERE 1 = 1

謎のWHERE 1 = 1がついてますがこういうSQLです。普通にJOINしてるだけですね。

hasManyをテストする

user : todo1 : nになるので本来のhasManyの使いドコロです。

ソーズ修正箇所

app/Controller/UsersController.phpはhasOneで修正した状態で置いておきましょう。

app/Model/User.phpにアソシエーションを設定。

app/Model/User.php
class User extends AppModel {
    public $hasMany = array (
        'TodoList' => array (
            'className' => 'TodoList',
            'foreignKey' => 'owner'
        )
    );
}

hasOneをhasManyに変えただけです!
では実験。

実行結果
[
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "山田太郎"
        },
        "TodoList": [
            {
                "id": "62",
                "todo": "牛乳を買う",
                "status": "0",
                "owner": "67",
                "assignee": "67"
            },
            {
                "id": "78",
                "todo": "犬の散歩",
                "status": "0",
                "owner": "67",
                "assignee": "68"
            }
        ]
    },
    {
        "User": {
            "id": "68",
            "username": "tanaka",
            "password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
            "name": "田中花子"
        },
        "TodoList": [
            {
                "id": "83",
                "todo": "家に電話",
                "status": "1",
                "owner": "68",
                "assignee": "68"
            }
        ]
    },
    {
        "User": {
            "id": "69",
            "username": "sato",
            "password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
            "name": "佐藤一郎"
        },
        "TodoList": []
    }
]

こうなりました!ちゃんとユーザに対してTODOが複数件紐ついています。
これがhasManyですね。
この時実行されたSQLは下記2つです。hasManyでは一回の実行でSQLが2回実行されます。

1回目
SELECT `User`.`id`, `User`.`username`, `User`.`password`, `User`.`name` FROM `study`.`users` AS `User`   WHERE 1 = 1
2回目
SELECT `TodoList`.`id`, `TodoList`.`todo`, `TodoList`.`status`, `TodoList`.`owner`, `TodoList`.`assignee` FROM `study`.`todo_lists` AS `TodoList`   WHERE `TodoList`.`owner` IN (67, 68, 69)

2回目は、1回目で取得したuserのidをownerに持つTodoListをselectしています。
こういう実装になってるんですね。

では、修正したところを元に戻して、lesson2、 画面を作りましょう。

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

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

ポイント

前回までの知識でほぼ開発できますが、今回、下記の「ユーザ一覧を取得して表示する」部分だけ少し解説します。

  • TODO一覧画面のユーザ一覧表示

userlist_todo_list.png

  • TODO詳細画面のユーザ一覧表示

userlist_todo_detail.png

今回の実装

Backbone、Marionetteをフル活用するなら、本来はユーザ一覧表示部分をViewにし、「Behavior」機能を使うなどして共通化するといい感じなのですが、
今回のメインは「CakePHPのアソシエーション」なので、ちょっと簡単(手抜き?)の実装になっています。
ご了承下さい。

具体的には、

  • ユーザ取得API(http://(PublicIP)/rest-study/users.json)を叩いてcollectionにユーザ一覧を取得
  • そのcollectonをループで処理し、テンプレートで用意している<select></select>タグの中に、<option value="ユーザのid">ユーザの氏名</option>というタグをJQueryを使ってはめ込んでいく。

というやり方です。

これを、TODO一覧とTODO詳細の両画面で「コピペ」しています。
(DRYではないですが今回はご勘弁!)

では、ソース解説です。

編集するファイル一覧

編集 file 編集概要
修正 app/View/Layouts/default.ctp テンプレートの修正
新規 app/webroot/js/collections/user-collection.js ユーザ一覧用collection
修正 app/webroot/js/models/todo-model.js 追加項目のparse
修正 app/webroot/js/views/todo-layout-view.js ユーザ一覧読み込み処理
修正 app/webroot/js/views/todo-composite-view.js ユーザ一覧表示
修正 app/webroot/js/views/todo-item-view.js 削除・詳細ボタンの表示制御
修正 app/webroot/js/views/todo-detail-layout-view.js ユーザ一覧読み込み処理
修正 app/webroot/js/views/todo-detail-item-view.js ユーザ一覧表示

default.ctp

テンプレートを修正。

default.ctp
〜 略 〜
  <title>シンプルTODOアプリ</title>
 </head>
 <body>
    <!-- ヘッダ -->
    <div id="header"></div>
    <!-- コンテンツ -->
    <div id="main"></div>

〜 略 〜

    <!-- TODO一覧表示のテンプレート -->
    <script type="text/template" id="todo-composite-template">
    <textarea style="width:300px;height:50px"id="new-todo" placeholder="Todo?" autofocus></textarea>
+   <select name="assignee" id="user-list">
    <input type="button" id="addTodo" value="追加">
    <hr>
    <div>
        <table border="1" width="350px">
+           <thead>
+               <tr>
+                   <td>完了</td>
+                   <td>ToDo</td>
+                   <td>オーナ</td>
+                   <td>担当</td>
+                   <td colspan="2"></td>
+               </tr>
+           </thead>
            <tbody></tbody>
        </table>
    </div>
    </script>

    <!-- TODO一行分のテンプレート(上のtbody部分に挿入される) -->
    <script type="text/template" id="todo-item-template">
    <td><input type="checkbox" class="toggle" <%- status === '1' ? 'checked' : '' %>></td>
    <td style="margin:0px">
        <span class="todo-edit" style="margin:0px"><%- todo %></span>
    </td>
    <td>
+       <span><%- Owner.name %></span>
+   </td>
+   <td>
+       <span><%- Assignee.name %></span>
+   </td>
+   <td>
        <a class="remove-link" href="#">削除</a>
+   </td>
+   <td>
        <a class="detail-link" href="#todo-lists/<%- id %>">詳細</a>
    </td>
    </script>

    <!-- 詳細画面のレイアウトテンプレート -->
    <script type="text/template" id="todo-detail-layout-template">
    <div id="todo-item"></div>
    </script>

    <!-- 詳細画面の表示内容テンプレート -->
    <script type="text/template" id="todo-detail-item-template">
    <h2>Todo #<%- id %></h2>
    <div>
    <textarea style="width:300px;height:50px" id="edit-todo" autofocus placeholder="Todo?"><%- todo %></textarea>
+   <select name="assignee" id="user-list">
+   </select>
    <input type="button" id="updateTodo" value="更新"></input>
    <input type="button" id="updateCancel" value="キャンセル"></input>
    </div>
    </script>

〜 略 〜

下記の通り修正。

  • TODO一覧画面
    • ユーザ一覧表示用<select></select>を追加
    • tableに<thead></thead>を追加(項目が増えてきたので)
    • [削除]と[詳細]を別の<td></td>に分割
  • TODO詳細画面
    • ユーザ一覧表示用<select></select>を追加

user-collection.js

user-collection.js
//User一覧表示用コレクション
define(function(require) {
    var UserModel = require('models/user-model');

    var UserCollection = Backbone.Collection.extend({
        url : '/rest-study/users.json',
        model : UserModel,

        parse : function(response) {
            return response;
        }
    });

    return UserCollection;
});

必要最低限のcollectionの実装です。
これはもう説明不要ですね!

todo-model.js

追加項目のOwnerAssigneeをパースしています。

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);
-           return response.TodoList;
+           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();
        }
    });
    return TodoModel;
 });

サーバからはCakePHP任せの下記フォーマットでTODO1件のJSONが来るので、

サーバから来た生の状態
{
    "TodoList": {
        "id": "62",
        "todo": "牛乳を買う",
        "status": "0",
        "owned": false,
        "assigned": false
    },
    "Owner": {
        "id": "67",
        "name": "山田太郎"
    },
    "Assignee": {
        "id": "67",
        "name": "山田太郎"
    }
}

こんなフォーマットに変更しています。

クライアントで処理しやすいよう整形
{
    "id": "62",
    "todo": "牛乳を買う",
    "status": "0",
    "owned": false,
    "assigned": false,
    "Owner": {
        "id": "67",
        "name": "山田太郎"
    },
    "Assignee": {
        "id": "67",
        "name": "山田太郎"
    }
}

todo-layout-view.js

ユーザ一覧読み込み処理を追加しています。

todo-layout-view.js
 define(function(require){
    var TodoCompositeView = require('views/todo-composite-view');
    var TodoCollection = require('collections/todo-collection');
+   var UserCollection = require('collections/user-collection');

    var TodoLayoutView = Marionette.LayoutView.extend({
        //テンプレート
        template: '#todo-layout-template',

        regions : {
            listRegion : '#todo-lists',
        },

-       onRender : function(){
+       onRender : function() {
+           this.userCollection = new UserCollection();
+           this.listenTo(this.userCollection, 'reset', this.onLoadUsers, this);
+           this.userCollection.fetch({reset : true});
+       },
+
+       onLoadUsers : function(userCollection) {
            var todoCollection = new TodoCollection();
            this.listenTo(todoCollection , 'reset', this.showTodoList, this);
            todoCollection.fetch({reset : true});
        },

        showTodoList : function(todoCollection){
            this.listRegion.show( new TodoCompositeView({
-               collection : todoCollection
+               collection : todoCollection,
+               userList : this.userCollection.models
            }));
        },

    });
    return TodoLayoutView;
 });
  • collections/user-collectionrequireで読み込む
  • onRender関数内で、this.userCollection.fetch()でサーバからユーザ一覧取得。
  • fetchが終わると(ListenToが効いて)onLoadUsers関数を実行、続いてshowTodoListが実行される
  • showTodoList関数内で、TodoCompositeViewの初期化時に、取得したユーザ一覧のcollectionを渡すようにしている
    • このユーザ一覧collectionをTodoCompositeViewの処理で表示するわけです。

todo-composite-view.js

ユーザ一覧を表示します。

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

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

        childView : TodoItemView,

        childViewContainer : 'tbody',

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

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

-       initialize: function(){
+       initialize: function(options){
            _.bindAll( this, 'onCreatedSuccess' );
+           this.userList = options.userList;
        },

+       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(), {
                  silent:  true ,
                  success: this.onCreatedSuccess
            });
            this.ui.newTodo.val('');
        },

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

        onCreatedSuccess : function(){
            this.collection.fetch({ reset : true });
        },

    });
    return TodoCompositeView;
 });
  • ユーザ一覧のセレクトボックスを処理するためui変数にuserListを追加。
  • initialize関数内で、todo-layout-viewから渡されたユーザ一覧collection(options.userListに入ってくる)を自身の中(this.userListに保持)
  • onRender関数内で、showUserList関数を実行してユーザ一覧の表示を行う
  • ユーザ一覧表示後、ログイン時に細んしているwindow.application.loginUserのユーザ情報からログインユーザのidを取得し、ユーザ一覧表示で自分(ログインユーザ)をデフォルトで選択された状態に設定。
  • showUserList関数では、jqueryを使用し、ui変数で定義済みの<select></select>タグの中に<option></option>タグを埋め込んでいます。
  • todoの新規追加時(newAttributes関数)は、ユーザ一覧で選択したユーザを担当者(assignee)をして設定します。

todo-item-view.js

削除・詳細ボタンの表示制御を行っています。

todo-item-view.js
 //Todo一覧の1件表示用ビュー
 define(function(){
    var TodoItemView = Marionette.ItemView.extend({
        //DOMに要素追加のタグ名
        tagName : 'tr',

        //テンプレート
        template : '#todo-item-template',

        ui : {
            checkBox : '.toggle',
-           removeLink : '.remove-link'
+           removeLink : '.remove-link',
+           detailLink : '.detail-link'
        },

        //DOMイベントハンドラ設定
        events : {
            //チェックボックスクリック時
            'click @ui.checkBox' : 'onStatusToggleClick',
            //削除ボタンクリック時
            'click @ui.removeLink' : 'onRemoveClick',
        },

        onStatusToggleClick : function() {
            this.model.toggle();
        },

        onRemoveClick : function() {
            this.model.destroy({
                wait : true
            });
        },

+       onRender : function() {
+           if (!this.model.attributes.owned) {
+               //オーナでない場合は削除リンク非表示
+               this.ui.removeLink.css({
+                   display : 'none'
+               });
+               if (!this.model.attributes.assigned) {
+                   //担当者でもない場合は、
+                   //詳細リンクも非表示
+                   this.ui.detailLink.css({
+                       display : 'none'
+                   });
+                   //チェックボックスも変更不可
+                   this.ui.checkBox.prop('disabled', true);
+               }
+           }
+       }
    });
    return TodoItemView;
 }); 
  • ui変数に削除リンク用の変数detailLinkを追加
  • onRender関数で、削除、詳細の各リンクの表示制御を下記の仕様で行っています。
    • オーナでない場合は削除リンクを非表示にする
      • さらに、担当者でもない場合は、
        • 詳細リンクも表示にする
        • チェックボックスをdisableにして変更不可とする

※表示制御の実装

  • リンクの非表示
    • jqueryのcss関数で、display : 'none'のスタイルを設定
  • チェックボックスの変更不可
    • jqueryのprop関数で、disabledtrueに設定

todo-detail-layout-view.js

ユーザ一覧読み込み処理を追加しています。
やっていることは、todo-layout-view.jsとほぼ同じです。

todo-detail-layout-view.js
 define(function(require) {
     var TodoDetailItemView = require('views/todo-detail-item-view');
     var TodoModel = require('models/todo-model');
+    var UserCollection = require('collections/user-collection');

    var TodoDetailLayoutView = Marionette.LayoutView.extend({
        //テンプレート
        template : '#todo-detail-layout-template',

        regions : {
            itemRegion : '#todo-item',
        },

        onRender : function() {
-           var todoModel = new TodoModel({
-               id : this.options.modelId
-           });
-           //モデルのサーバからのデータ取得完了時、描画を行う
-           this.listenTo(todoModel, 'sync', this.showItem, this);
-           //サーバからデータ取得
-           todoModel.fetch({
-               wait : true
+           this.userCollection = new UserCollection();
+           this.listenToOnce(this.userCollection, 'reset', this.onLoadUsers, this);
+           this.userCollection.fetch({
+               reset : true
            });
        },

-       showItem : function(todoModel) {
-           this.itemRegion.show( new TodoDetailItemView({
-               model : todoModel
-           }));
-       },
+       onLoadUsers : function(userCollection){
+           var todoModel = new TodoModel({
+               id : this.options.modelId
+           });
+           //モデルのサーバからのデータ取得完了時、描画を行う
+           this.listenToOnce(todoModel, 'sync', this.showItem, this);
+           //サーバからデータ取得
+           todoModel.fetch({
+               wait : true
+           });
+       },
+
+       showItem : function(todoModel) {
+           this.itemRegion.show(new TodoDetailItemView({
+               model : todoModel,
+               userList : this.userCollection.models
+           }));
+       },

    });
    return TodoDetailLayoutView;
 });
  • collections/user-collectionrequireで読み込む
  • onRender関数内で、this.userCollection.fetch()でサーバからユーザ一覧取得。
    • 元々onRender内で行っていた、model1件のサーバからの取得処理は、onLoadUsers内に移動しています。 - fetchが終わると(ListenToが効いて)onLoadUsers関数を実行、todoModel.fetch()でTODO1件の取得を行う。
  • showItem関数内で、TodoDetailItemViewの初期化時に、取得したユーザ一覧のcollectionを渡すようにしている
    • このユーザ一覧collectionをTodoDetailItemViewの処理で表示します。

todo-detail-item-view.js

ユーザ一覧を表示します。

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

        //テンプレート
        template: "#todo-detail-item-template",

        ui : {
            todoStatus   : '#edit-todo',
            updateButton : '#updateTodo',
-           cancelButton : '#updateCancel'
+           cancelButton : '#updateCancel',
+           userList     : '#user-list'
        },

        //DOMイベントハンドラ設定
        events : {
            //更新ボタンクリック時
            'click @ui.updateButton' : 'onUpdateClick',
            //キャンセルボタンクリック時
            'click @ui.cancelButton' : 'onCancelClick',
        },

        //初期化
-       initialize: function(){
+       initialize: function(options){
            _.bindAll( this, 'onSaveSuccess' );
+           this.userList = options.userList;
+       },
+
+       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();
+           var todoString = this.ui.todoStatus.val();  // Todo
+           var assigneeId = this.ui.userList.val();    // 担当者
            this.model.save({
-               todo : todoString
+               todo : todoString,
+               assignee : assigneeId
            }, {
                silent : true,
                success : this.onSaveSuccess,
            });
        },

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

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

        //TODOリスト画面に戻る
        backTodoLists : function() {
            Backbone.history.navigate('#todo-lists', true);
        }

    });
    return TodoDetailItemView;
 });

ユーザ一覧の表示に関しては、todo-composite-view.jsでやったことと同様です。

  • ユーザ一覧のセレクトボックスを処理するためui変数にuserListを追加。
  • initialize関数内で、todo-layout-viewから渡されたユーザ一覧collection(options.userListに入ってくる)を自身の中(this.userListに保持)
  • onRender関数内で、showUserList関数を実行してユーザ一覧の表示を行う
  • ユーザ一覧表示後、表示するTODOの担当者のユーザID(modelのassigneeから取得)を選択された状態に設定。
  • showUserList関数は、todo-composite-view.js内の同関数のコピペです!(今回はこれでご了承を!)
  • 更新ボタンクリック時の処理(onUpdateClick関数)では、ユーザ一覧で選択したユーザを担当者(assignee)をして設定しています。

実装

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

  • :white_check_mark: app/View/Layouts/default.ctpを上記の通り修正。
  • :white_check_mark: app/webroot/js/collections/user-collection.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/models/todo-model.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-layout-view.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-composite-view.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-item-view.jsを上記の通り新規作成。
  • :white_check_mark: app/webroot/js/views/todo-detail-layout-view.jsを上記の通り新規作成。
  • :white_check_mark: app/webroot/js/views/todo-detail-item-view.jsを上記の通り新規作成。
  • :white_check_mark: 動作確認!
  • :white_check_mark: Gitにコミット

GitHubでのdiff表示へのリンク

第6回 Lesson1 クライアントサイド · suzukishouten-study/rest-study@f1623dc

以上です!

次回予告

次回テーマは、サーバー・クライアントの両面からかけるバリデーション - 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
ユーザーは見つかりませんでした