はじめに
本投稿は、2015/9/25に行われた、ダウンロード・アップロード機能の実装 - connpassの内容についてまとめた資料です。
今後の予定は以下に掲載されますのでよろしくお願いします!
AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass
今回は、よくある機能、ファイルのダウンロードとアップロードを実装してみます。
なぜ、ダウンロード/アップロードを今回取り上げたのか
業務システムではよくある機能なので、実装方法を勉強しておこう、というのも目的の一つですが、Webシステムは「HTTP」の上で動いているということを改めて思い出しておこう、というのが今回の狙いの一つです。フレームワークやライブラリによって高度に抽象化された上の方のレイヤのみ扱うことの多いWebプログラミングですが、今回は"少しだけ"下のレイヤに「潜って」みましょう!
プログラムの前に、まずHTTPについて見ておきますよ!。
ダウンロードのしくみ
まず、ダウンロード。ダウンロードというと、「サーバ上から受信したコンテンツをローカルに保存する」ことですが、「ローカルに保存する」のを実際にやっているのはブラウザです。
「サーバ上から受信したコンテンツ」はどういう構造になっているのか、まず見ておきましょう。
HTTPレスポンスの構造
「サーバ上から受信したコンテンツ」は、サーバから返ってくるHTTPレスポンスのうち、「レスポンスボディ」と呼ばれる部分に含まれています。
下記はyahooのトップページを要求した際のレスポンスですが、HTTPレスポンスの構造は、
- 1行目: プロトコル/バージョン レスポンスコード ->
HTTP/1.1 200 OK
- 2行目以降: レスポンスヘッダ(複数行に渡る) ->
Server: nginx
,Date: Thu, 24 Sep 2015 09:11:53 GMT
,Content-Type: text/html; charset=UTF-8
... - 空行
- 空行の後が全てレスポンスボディ ->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML...
〜
となっています。
HTTP/1.1 200 OK
Server: nginx
Date: Thu, 24 Sep 2015 09:11:53 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
P3P: policyref="http://privacy.yahoo.co.jp/w3c/p3p_jp.xml", CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"
Cache-Control: private, no-cache, no-store, must-revalidate
Expires: -1
Pragma: no-cache
X-XRDS-Location: https://open.login.yahooapis.jp/openid20/www.yahoo.co.jp/xrds
Vary: Accept-Encoding
X-Frame-Options: SAMEORIGIN
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="content-style-type" content="text/css">
<meta http-equiv="content-script-type" content="text/javascript">
<meta name="description" content="日本最大級のポータルサイト。検索、オークション、ニュース、メール、コミュニティ、ショッピング、など80以上のサービスを展開。あなたの生活をより豊かにする「ライフ・エンジン」を目指していきます。">
<meta name="robots" content="noodp">
<title>Yahoo! JAPAN</title>
〜以下省略〜
「ダウンロード」と行ってもあくまでHTTPレスポンスボディ部にコンテンツが埋め込まれるだけで、このフォーマットは変わりません。
ダウンロードとはつまり、「サーバから受け取ったレスポンスボディ部をブラウザがローカルファイルシステムに保存すること」と言えそうです。
ブラウザにレスポンスボディを保存させる
というわけで、「ダウンロード」する為には、ブラウザに「レスポンスボディ」を保存させる、という動作をさせなければなりません。
上のyahooのトップページの例では、レスポンスボディにHTMLが返ってきています。この時、ブラウザはこれをローカルに保存(一時ファイルは除く)はせず、ブラウザウィンドウ上に描画します。
では、ブラウザは何を元に受信したレスポンスボディを「表示する」か「保存する」かを決めるのでしょうか。
ヒントになるものは、レスポンスに関するメタ情報を持つ「レスポンスヘッダ」しかありません。
そのうちブラウザが主に判断材料とするのは、Content-Type
とContent-Disposition
の2つです。
- Content-Type
- レスポンスボディがHTMLなら
text/html
、jsonならapplication/json
などとサーバ側で設定します。
ブラウザは、自分が知っているContent-Type
であれば、そのコンテンツに対する既定の動作をします。そのブラウザが「表示」が既定であれば表示し、「保存」が既定であれば保存します。
- レスポンスボディがHTMLなら
- Content-Disposition
-
attachment
またはinline
をサーバ側で設定します。保存させたい場合はattachment
, 表示させたい場合はinline
、とサーバ側で設定してブラウザにその動作を要求するわけです。ブラウザのデフォルトの挙動はブラウザによって微妙に違いますので、Content-Type
だけ設定すれば期待する動作になる場合もありますが、Content-Disposition
も指定しておくとより確実にブラウザに「ダウンロード」させることが出来ます。
-
今回のワークショップでは、
- ダウンロードするコンテンツをレスポンスボディに設定する
- レスポンスヘッダを適切に設定する
の具体的な実装方法を見ていくことになります。
アップロードの仕組み
次にアップロード。実際にどのようにファイルが送られるのでしょうか。
HTTPリクエストの構造を見ておきましょう。
HTTPリクエストの構造
HTTPリクエストの構造はHTTPレスポンスの構造と似ています。
送信するファイルは、送信されるリクエストのうち、「リクエストボディ」と呼ばれる部分に含まれて送られます。
今回作成するファイルアップロード機能で実際にファイルを送信した際のリクエストは下記のようになっています。
todolist1.txt
, todolist2.txt
の2ファイルを同時にアップロードした例です。
POST /rest-study/todo_lists/upload.json HTTP/1.1
Host: 10.0.1.206
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://10.0.1.206/rest-study/
X-Requested-With: XMLHttpRequest
Content-Length: 779
Content-Type: multipart/form-data; boundary=---------------------------1395431092341454357747073315
Cookie: CAKEPHP=oo6gj167d4fltodm3nre2lskd2
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
-----------------------------1395431092341454357747073315
Content-Disposition: form-data; name="0"; filename="todolist1.txt"
Content-Type: text/plain
todolist1.txtからアップロードしたTODO1
todolist1.txtからアップロードしたTODO2
todolist1.txtからアップロードしたTODO3
todolist1.txtからアップロードしたTODO4
todolist1.txtからアップロードしたTODO5
-----------------------------1395431092341454357747073315
Content-Disposition: form-data; name="1"; filename="todolist2.txt"
Content-Type: text/plain
todolist2.txtからアップロードしたTODO1
todolist2.txtからアップロードしたTODO2
todolist2.txtからアップロードしたTODO3
-----------------------------1395431092341454357747073315--
HTTPメソッドはPOST
で、/rest-study/todo_lists/upload.json
というURLにアクセスしています。
ポイントは、
Content-Type: multipart/form-data; boundary=---------------------------1395431092341454357747073315
Content-Length: 779
の2つのHTTPリクエストヘッダです。
まず、HTTPリクエストのフォーマットを確認しておきましょう。
- 1行目: HTTPメソッド URI プロトコル/バージョン ->
POST /rest-study/todo_lists/upload.json HTTP/1.1
- 2行目以降: リクエストヘッダ(複数行に渡る) ->
Host: 10.0.1.206
,User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0
... - 空行
- 空行の後が全てリクエストボディ ->
-----------------------------1395431092341454357747073315
〜
となっています。
multipart/form-data
Content-Type
がmultipart/form-data
でない場合、例えばapplication/json
だった場合、下記のようになります。
{"todo":"TODOを追加ボタンで登録","status":0,"assignee":"67"}
multipart/form-data以外では、送信するJSONデータはこのようにフラットにリクエストボディに格納されています。
multipart/form-dataでは、リクエストボディ部が、バウンダリ文字列とよばれる、上記の例では-----------------------------1395431092341454357747073315
のような文字列で囲まれています。
この形式が、リクエストヘッダContent-Type
で指定したmultipart/form-data
です。
その名の通り、リクエストボディ部がバウンダリ文字列を区切りとして複数のパートに分割されています。
そしてそれぞれのパートにリクエストヘッダとリクエストボディが含まれるというネストした構造になっています。
今回のワークショップでは、
- Ajaxでファイルアップロード処理を行う
- リクエストからファイルコンテンツを取り出す
- 取り出したコンテンツからTODOを作成する
の具体的な実装方法を見ていくことになります。
ですが、「リクエストからファイルコンテンツを取り出す」部分はPHP及びCakePHPがほとんどやってくれますのでカンタンです!
「Ajaxでファイルアップロード」は頑張って実装しましょう!詳しくは後述!
今回の内容
完成イメージです。
- ダウンロード用リンク
- クリックすると表示されているTODO一覧をCSV形式でダウンロードします。
- アップロードファイル選択
- クリックするとファイル選択ダイアログが開き、アップロードするファイルを選択(複数可)します。
- アップロードボタン
- クリックするとアップロードを実行します。
なお、アップロードするファイルは下記の例の通りで、一行をひとつのTODOとし、自分がオーナ、自分が担当者、未完了の状態で追加するものとします。
追加する際は、「追加ボタン」で手作業で追加する場合と同様のバリデーションを効かせます。
todolist1.txtからアップロードしたTODO1
todolist1.txtからアップロードしたTODO2
todolist1.txtからアップロードしたTODO3
todolist1.txtからアップロードしたTODO4
todolist1.txtからアップロードしたTODO5
ワークショップメニュー
- 事前準備
- Lesson1 ダウンロード
- Lesson2 アップロード
という感じですすめます。
事前準備
事前準備は毎回同じなので、別エントリにまとめています。
全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。
やることは、
- gitのブランチを整えて今回用ブランチ
vol/09
を作成する
です。まずこれをやりましょう。
それと、第5回と第6回に不参加の方は、テーブルの修正が必要です。
- 第5回
- ユーザ登録用のテーブル作成(ログイン機能実装のため)
- 第6回
- TODO一覧テーブルへの列追加(owner列とassignee列。担当者アサイン機能実装のため)
をやってますので、それぞれ下記リンク先を参照して実施して下さい。
ユーザ登録用のテーブル作成
TODO一覧テーブルへの列追加
準備ができたら、Lesson1です!
Lesson1 ダウンロード
- クライアント側
- ダウンロードリンクの追加
- サーバ側
- TODOをデータベースから読み込んでCSVファイルコンテンツとしてレスポンスボディに入れて返す
をそれぞれ実装します。
ダウンロードするファイル形式は下記のとおり、カンマ区切りで、
- id
- todo: TODOの内容
- status: 状態
- owner: オーナの名前
- assignee: 担当者の名前
を出力するものとします。一行目は項目のタイトル行とします。
id,todo,status,owner,assignee
1,牛乳を買う,1,山田太郎,山田太郎
2,家に電話,1,田中花子,山田太郎
3,犬の散歩,1,山田太郎,山田太郎
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/Config/routes.php | ダウンロード処理へのルーティング追加 |
修正 | app/View/Layouts/default.ctp | ダウンロードリンク追加 |
修正 | app/Controller/TodoListsController.php | ダウンロード処理追加 |
app/Config/routes.php
ダウンロード処理のURLを追加します。
〜略〜
/*
* API
*/
// ログイン
Router::connect('/users/login', array (
'controller' => 'users',
'action' => 'login',
'method' => array (
'POST'
)
));
// ログアウト
Router::connect('/users/logout', array (
'controller' => 'users',
'action' => 'logout',
'method' => 'POST'
));
// ログインチェック(ログイン情報取得)
Router::connect('/users/loggedin', array (
'controller' => 'users',
'action' => 'loggedIn',
'method' => 'GET'
));
// サインアップ
Router::connect('/users/signup', array (
'controller' => 'users',
'action' => 'signUp',
'method' => array (
'POST'
)
));
+// CSVファイルダウンロード
+Router::Connect('/todo_lists/download', Array(
+ 'controller' => 'todo_lists',
+ 'action' => 'download',
+ 'method' => 'GET'
+));
Router::mapResources(array (
'todo_lists',
));
〜以下略〜
-
/todo_lists/download
を追加します。 - Getメソッドでアクセス時に
TodoListController.php
のdownload
メソッドにルーティングします。
app/View/Layouts/default.ctp
ダウンロード用リンクを追加します。
〜略〜
<!-- TODO一覧表示のテンプレート -->
<script type="text/template" id="todo-composite-template">
<div class="row">
<div class="col-xs-12">
<span class="row form-inline">
<div class="input-group col-sm-6 col-xs-12">
<label for="new-todo" class="visible-xs">Todo</label>
<textarea class="form-control todo-item-text" rows="3" id="new-todo" placeholder="Todo?" autofocus></textarea>
</div>
<div class="input-group col-sm-3 col-xs-12">
<label for="user-list" class="visible-xs">担当者</label>
<select class="form-control" name="assignee" id="user-list"></select>
</div>
<div class="input-group col-sm-3 col-xs-12">
<label class="visible-xs"></label>
<input type="button" id="addTodo" class="btn btn-primary btn-md todo-action-button" value="追加">
</div>
</span>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr class="success">
<th class="col-sm-6" colspan="2">ToDo</th>
<th class="col-sm-2">オーナ</th>
<th class="col-sm-2">担当</th>
<th class="col-sm-2" colspan="2"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
+ <div class="row">
+ <div class="col-xs-6">
+ <a href="/rest-study/todo_lists/download" class="alert alert-default pull-left" id="downloadTodo">TODOリストをダウンロード</a>
+ </div>
+ </div>
</script>
〜以下略〜
-
<a>
タグの追加のみです。リンク先をapp/Config/routes.php
で追加した/rest-study/todo_lists/download
に設定しています。
app/Controller/TodoListsController.php
ダウンロード処理を行います。
〜略〜
public function index() {
$query = array (
'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 edit($id) {
$this->TodoList->id = $id;
$data = $this->request->data;
$res = $this->TodoList->save($this->request->data);
$res = !empty($res);
$response = $this->editResponse($res);
$this->set(compact('response'));
$this->set('_serialize', 'response');
}
+ public function download() {
+ //id順で一覧取得
+ $query = array (
+ 'fields' => $this->fields,
+ 'order' => "TodoList.id"
+ );
+ $res = $this->TodoList->find('all', $query);
+ // CSVファイルに整形
+ if ($res && is_array($res)) {
+ $fp = fopen('php://temp', 'w+');
+ //タイトル
+ $fields = array('id', 'todo', 'status', 'owner' ,'assignee');
+ fputcsv($fp, $fields);
+ //データ
+ foreach ( $res as $record ) {
+ $fields = array();
+ $fields[] = $record['TodoList']['id'];
+ $fields[] = $record['TodoList']['todo'];
+ $fields[] = $record['TodoList']['status'];
+ $fields[] = $record['Owner']['name'];
+ $fields[] = $record['Assignee']['name'];
+ fputcsv($fp, $fields);
+ }
+ //ポインタを先頭に
+ rewind($fp);
+ //読み込み
+ $content = stream_get_contents($fp);
+ //このままだとエンコーディングはUTF-8, 改行コードはLFとなり、
+ //Excelでひらけないので、開きたい場合は下記コメントインしてエンコーディングをSJIS-winにする
+ //$content = mb_convert_encoding($content, 'sjis-win', 'UTF-8');
+ fclose($fp);
+ //Viewを使用しない
+ $this->autoRender = false;
+ //ダウンロードファイル名を設定
+ $this->response->download('todo.csv');
+ $this->response->type('csv');
+ $this->response->body($content);
+ }
+ }
+
//レスポンスを編集
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;
}
〜以下略〜
download
関数を追加
データ取得
-
$this->TodoList->find('all', $query);
でtodoリストを取得。これは上のindex
関数でやっていることと同じです。
phpのファイルアクセス関数たち
- 下記関数たちを使用し、Todoリストのデータをいったんcsv形式で
php;//temp
ストリームに書きだします。- fopen関数で、php://temp(メモリ上にファイル出力するストリーム)を開きます。
- CSV形式の出力はfputcsv関数にまかせます。
- rewind関数でファイルポインタを先頭に戻し、
- stream_get_contents関数で出力したデータを配列に読み込み直します。
- fclose - Manual関数でファイルをクローズします。
ポイント
ここがキモです!
ブラウザにファイルを「ダウンロード」させるため、
-
Content-Type
にCSVファイルであることを設定 -
Content-Disposition
にダウンロード指定(attachment)とファイル名を指定
の2つをやる必要があります。
-
$this->response->download('todo.csv');
- CakePHPの
CakeResponse
->download
メソッドをこのように使用すると、Content-Disposition: attachment; filename="todo.csv"
が設定されます
- CakePHPの
-
$this->response->type('csv');
- CakePHPの
CakeResponse
->type
メソッドをこのように使用すると、Content-Type: text/csv; charset=UTF-8
が設定されます
- CakePHPの
-
$this->response->body($content)
- CakePHPの
CakeResponse
->body
メソッドをこのように使用すると、レスポンスボディに$content
の内容が設定されます。
- CakePHPの
以上でブラウザが「ダウンロード」してくれます。
$this->response->download('todo.csv');
と$this->response->type('csv')
をコメントアウトしたり戻したりして、いろんなブラウザで挙動をためしてみると、微妙な違いを確認できますよ。
実装
では、実際に修正してTODOをダウンロードしてみましょう。
-
app/Config/routes.php
を上記の通り修正。 -
app/View/Layouts/default.ctp
を上記の通り修正。 -
app/Controller/TodoListsController.php
を上記の通り修正。 - 動作確認!
- Gitにコミット
GitHubでのdiff表示へのリンク
第9回 Lesson1 ダウンロード · suzukishouten-study/rest-study@ccb6d14
ダウンロードできたらLesson2へ!
Lesson2 アップロード
- クライアント側
- ファイル選択とアップロードボタンの追加
- Ajaxでのファイルアップロード処理の追加
- サーバ側
- アップロードされたファイルを読み込んでTODOに追加
をそれぞれ実装します。
ダウンロードするファイル形式は下記のとおり1行1Todoとします。
todolist1.txtからアップロードしたTODO1
todolist1.txtからアップロードしたTODO2
todolist1.txtからアップロードしたTODO3
todolist1.txtからアップロードしたTODO4
todolist1.txtからアップロードしたTODO5
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/Config/routes.php | ダウンロード処理へのルーティング追加 |
修正 | app/View/Layouts/default.ctp | ファイル選択とアップロードボタン追加 |
修正 | app/webroot/js/views/todo-composite-view.js | アップロード処理追加 |
修正 | app/Controller/TodoListsController.php | アップロードされたファイルの処理追加 |
app/Config/routes.php
アップロード処理のURLを追加します。
〜略〜
// CSVファイルダウンロード
Router::Connect('/todo_lists/download', Array(
'controller' => 'todo_lists',
'action' => 'download',
'method' => 'GET'
));
+// CSVファイルアップロード
+Router::Connect('/todo_lists/upload', Array (
+ 'controller' => 'todo_lists',
+ 'action' => 'upload',
+ 'method' => 'POST'
+));
+
Router::mapResources(array (
'todo_lists',
));
〜以下略〜
app/View/Layouts/default.ctp
ファイル選択とアップロードボタンを追加します。
〜略〜
<!-- TODO一覧表示のテンプレート -->
<script type="text/template" id="todo-composite-template">
<div class="row">
<div class="col-xs-12">
<span class="row form-inline">
<div class="input-group col-sm-6 col-xs-12">
<label for="new-todo" class="visible-xs">Todo</label>
<textarea class="form-control todo-item-text" rows="3" id="new-todo" placeholder="Todo?" autofocus></textarea>
</div>
<div class="input-group col-sm-3 col-xs-12">
<label for="user-list" class="visible-xs">担当者</label>
<select class="form-control" name="assignee" id="user-list"></select>
</div>
<div class="input-group col-sm-3 col-xs-12">
<label class="visible-xs"></label>
<input type="button" id="addTodo" class="btn btn-primary btn-md todo-action-button" value="追加">
</div>
</span>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr class="success">
<th class="col-sm-6" colspan="2">ToDo</th>
<th class="col-sm-2">オーナ</th>
<th class="col-sm-2">担当</th>
<th class="col-sm-2" colspan="2"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<a href="/rest-study/todo_lists/download" class="alert alert-default pull-left" id="downloadTodo">TODOリストをダウンロード</a>
</div>
+ <div class="col-xs-6">
+ <form class="form-inline">
+ <input type="file" id="uploadFile" name="uploadFile" multiple class="form-control">
+ <a class="btn btn-info pull-right" id="uploadButton">TODOリストファイルアップロード</a>
+ </form>
+ </div>
</div>
</script>
〜以下略〜
- ダウンロードリンクの下に、
input type="file"
と、<a>
タグでボタンを追加します。-
input type="file"
はmultiple
を設定して複数ファイルアップロードに対応しておきます。
-
app/webroot/js/views/todo-composite-view.js
Ajaxによるアップロードを実装します。。
〜略〜
ui : {
addTodo : '#addTodo',
newTodo : '#new-todo',
- userList : '#user-list'
+ userList : '#user-list',
+ uploadButton : '#uploadButton',
+ uploadFile : '#uploadFile'
},
events : {
'click @ui.addTodo' : 'onCreateTodo',
+ 'click @ui.uploadButton' : 'onClickUploadButton',
},
initialize: function(options){
@@ -74,7 +77,34 @@ define(function(require) {
message += errors.validationError[key];
}
alert(message);
- }
+ },
+
+ onClickUploadButton : function() {
+ var i;
+ var form = new FormData();
+ var files = this.ui.uploadFile[0].files;
+ for ( i = 0; i < files.length; i++) {
+ form.append(i, files[i]);
+ }
+ var that = this;
+ $.ajax({
+ url : "todo_lists/upload.json",
+ type : "POST",
+ data : form,
+ processData : false,
+ contentType : false,
+ dataType : 'json'
+ }).done(function(data) {
+ alert(data);
+ }).always(function(){
+ that.collection.fetch({
+ reset : true
+ });
+ that.ui.uploadFile.attr('type', 'text');
+ that.ui.uploadFile.attr('type', 'file');
+ });
+ return false;
+ },
});
return TodoCompositeView;
});
〜以下略〜
-
ui
変数に、追加したファイル選択、アップロードボタンを追加します。 -
events
変数にアップロードボタンのクリックイベントを登録します。 -
onClickUploadButton
関数がAjaxによるアップロード処理の実装です。
Jquery.ajax()によるアップロード
今回はアップロードにはjqueryのajax関数を使用します。ポイントは以下。
- html5の
FormData
オブジェクトでファイル選択のフォームデータをラッピングします。 - FormDataに追加するのは、
this.ui.uploadFile[0].files
配列の要素。ファイルを2つ選択すると、this.ui.uploadFile[0].files[0]
と```this.ui.uploadFile[0].files[1]`に格納されるので、for文で処理しています。 -
that.ui.uploadFile.attr('type', 'text');
->that.ui.uploadFile.attr('type', 'file');
というトリッキーなことをしてますが、これはアップロード完了後、ファイルを未選択状態に戻したいため、いったんフォームをtext
にしてからfile
に戻すというハックです。ブラウザによってうまくいかない場合もあるかもしれません!
app/Controller/TodoListsController.php
アップロードされたファイルを処理してTODOを登録します。。
〜略〜
public function download() {
〜略〜
}
+ public function upload() {
+ $files = $this->request->params['form'];
+ $owner = $this->Auth->user()['id'];
+ $numTodos = 0;
+ foreach ( $files as $file ) {
+ $fileName = $file['name'];
+ $filePath = $file['tmp_name'];
+ $todos = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ $assignee = $owner;
+ $errors = array ();
+ $lineNo = 1;
+ foreach ( $todos as $todo ) {
+ $data = array ();
+ $data['todo'] = $todo;
+ $data['status'] = 0;
+ $data['owner'] = $owner;
+ $data['assignee'] = $assignee;
+ $res = $this->TodoList->save($data);
+ if ($res) {
+ $numTodos++;
+ } else {
+ if (count($this->TodoList->validationErrors) > 0) {
+ foreach ( $this->TodoList->validationErrors as $validationErrorsOfLine ) {
+ $title = 'file:' . $fileName . ' - line: ' . $lineNo . ': ';
+ foreach ( $validationErrorsOfLine as $validationError ) {
+ $errors[] = array (
+ $title . $validationError
+ );
+ }
+ }
+ }
+ }
+ $this->TodoList->create();
+ $lineNo++;
+ }
+ }
+ if (count($errors) > 0) {
+ $this->TodoList->validationErrors = $errors;
+ $response = $this->editResponse(false);
+ array_unshift($response['errors'], array (
+ '以下のエラーが発生しました。'
+ ));
+ if ($numTodos > 0) {
+ array_unshift($response['errors'], array (
+ $numTodos . '件のTODOを登録しました。'
+ ));
+ }
+ } else {
+ $response = $numTodos . '件のTODOを登録しました。';
+ }
+ $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;
}
〜以下略〜
アップロードされたファイルを取得し、読み込み、各行をTodoとして登録します。画面上で「追加」ボタンで登録したときと同じバリデーションを効かせ、エラーがあればメッセージを返すようにしています。
アップロードファイル取得
-
$this->request->params['form']
にアップロードされたファイルの情報が配列で入ってきます。ファイルが2つアップロードされたなら、$this->request->params['form'][0]
と$this->request->params['form'][1]
に入ってきます。 -
$this->request->params['form'][0]['name']
でファイル名、$this->request->params['form'][0]['tmp_name']
で、サーバ上にテンポラリファイルとして作成されたアップロードファイルのパスが格納されます。
リクエストボディにマルチパートで格納されたファイルのファイル名とファイル内容がこれで取れるわけです。phpとCakePHPがこの辺の面倒なところをやってくれています。カンタンですね!
phpのファイルアクセス関数たち
- file関数でアップロードされたファイルを全て読み込んで配列に格納します。
ポイント
やっていることは、
- アップロードされたファイルをまとめて読み込む
- すべての行に対し、一行をそのまま
todo
とし、その他の項目は、status=0
,owner=ログインユーザのid
,assignee=ログインユーザのid
を設定し、save
メソッドでデータベースに保存。これでバリデーションも動く - 登録できた件数をメッセージに埋め込んでレスポンスボディとして返す。
- 途中の行でエラーが有っても最後まで実行しますので、「n件登録できてm件エラーで登録できず」という状況が有ります。この辺はソースを頑張って読み解いてみましょう!
実装
では、実際に修正してTODOをアップロードしてみましょう。
-
app/Config/routes.php
を上記の通り修正。 -
app/View/Layouts/default.ctp
を上記の通り修正。 -
app/webroot/js/views/todo-composite-view.js
を上記の通り修正。 -
app/Controller/TodoListsController.php
を上記の通り修正。 - 動作確認!
- Gitにコミット
GitHubでのdiff表示へのリンク
第9回 Lesson2 アップロード · suzukishouten-study/rest-study@3edd59f
以上です!
次回予告
次回は「リファクタリング(サーバー編)」です。
小さなTodoアプリではありますが、少しずつコード量が増えてきました。
次回はもっとおおきな開発に備えてコードをキレイにする方法を学んでいきます。
ぜひご参加下さい!
ひょっとすると「リファクタリング(クライアント編)」も一緒にやっちゃうかも...
コメント/フィードバックお待ちしております。
参加者の方も、そうでない方もお気づきの点があればお願い致します。