CakePHP流のCSVエクスポート

元記事はこちら
By jeroendenhaan

手短に調べた結果、データをCSVファイルにエクスポートする方法が見つかりませんでした。Cookbookを調べたのち、CSV形式でのデータ提供の方法を見つけました。CakePHP1.2ではカスタムレイアウト、状況に応じたビューの使用、拡張子解析、リクエストハンドリングのサポートがあり、CakePHPはこう使うべきだと考えました。素晴らしい方法ではないかもしれませんが、Cake流のやり方を掴んでもらうために初心者向けに披露するにはよいものだと思います。


先週、ウェブショップの新しい管理パネルを作成しました。いまだにCakePHP1.1で動いており、大きな手入れが必要でしたので、最初から作り直すことにしました。それでバージョン1.2について深く知るようになり、厳密にショップをきれいにしておけば1.3へのシフトも容易になるのではないかと期待しています。

アプリケーションの重要な部分は、ショップから注文をエクスポートして、帳簿をつけるために別の部門に渡すところです。当時はブラウザにヘッダを出力して、各行をエコーするカスタムコンポーネントを使っていました。

すでにウェブショップのフロントエンドでは使用していましたが、新しいバージョンのCakeに移行してから、Cake流の利用者へのデータの提示方法について深く考える必要を感じました。CSV出力用に作成されたものを見つけられなかったので、自分の発見をここで共有しようと思います。おそらく幾人かの初心者のCakePHPプログラマには有益でしょうが、プロの皆さんには目新しいものは何も無いかもしれません。

このエクササイズの目的は、デフォルトリンクからCSVエクスポートができるようにすること(たとえば、/orders/export.csv)と、適切な方法で行うこと。CakePHP1.2を念頭に置いています。

拡張子の解析

Cakeのルーティングのパワーは驚異的です。プログラマがコントローラとアクションをきれいで論理的に保っておけば、どんなタイプのリンクでも生成できます。この記事では、Routerクラスの一つのメソッドが特に重要になります。それは parseExtensions() です。このメソッドは、利用者からリクエストがあった特定のファイルタイプをどのように取り扱うかをCakeに知らせます。ここではCSVエクスポートに注力しますので、以下のコードを routes.php に追記してください。

<?php
// File: /app/config/routes.php

// CakePHPがCSVファイルのリクエストを適切に解析できるように

Router::parseExtensions('csv');

この1行で、Cakeに’.csv’で終わるリクエストを適切に処理するように伝えます。私の場合、ファイルがリクエストされたらコントローラ内で適切なアクションが呼ばれるようにしてあります。しかしその前に、データベーステーブルから読み込まれたデータはビューが表示するようにすべきです。それで、コントローラを以下のようにします。

<?php
// File: /app/controllers/orders_controller.php

class OrdersController extends AppController
{
	var $name = 'Orders';
	var $uses = array('Order');

	// RequestHandlerを含める。適切なレイアウトとビューが使用されるようにする
	
	var $components = array('RequestHandler');

	function export()
	{
		// アクションの実行時間の表示を止める
		Configure::write('debug',0);
		// 関連モデルによって再帰しないためにフィールドを指定してFindする
		$data = $this->Order->find(
			'all',
			array(
				'fields' => array('id','created','name','paid','total'),
				'order' => "Order.id ASC",
				'contain' => false
			)
		);
		// データと全く同じ配列書式で、CSVファイルのためのカラムヘッダを定義
		$headers = array(
			'Order'=>array(
				'id' => 'ID',
				'created' => 'Date',
				'name' => 'Name',
				'paid' => 'Paid?',
				'total' => 'Total'
			)
		);
		// データ配列の先頭にヘッダを追加
		array_unshift($data,$headers);
		// ビューでデータを利用できるようにする(最終的にはCSVファイルになる)
		$this->set(compact('data'));
	}

はじめに、RequestHandlerコンポーネントを読み込みます。リクエスト毎に適切なレイアウトファイルとビューファイルが表示されるようにします。次に、デバッグモードをオフにします(必須ではありませんが、CSVファイル中に不要なテキストが混入するのを防ぎます)。最後に、データベーステーブルから読み込まれたデータをフェッチし、ビューに渡します。ヘッダはオプションです。必要なければ省いて構いません。

レイアウトとビュー

次に、利用者から要求があったデータを提供する必要があります。すでにCakeには解析する拡張子を知らせてありますし、RequestHandlerも読み込んでいます。それで、CSVファイルの要求があった場合には、Cakeは自動的にCSVレイアウトファイルを参照します。たとえば、 /orders/export.csv が要求された場合は、/app/views/layouts/csv 以下の default.ctp が参照されます。

<?php
	// File: /app/views/layouts/csv/default.ctp

	// 普通のWebページの場合のように、ビューの出力をそのままechoする
	echo $content_for_layout;
?>

CSVファイルはプレーンテキストなので、ここでは特に何もする必要がありません。バリエーションとして、データ列のヘッダをここで表示する方法もありますが、データと一緒に$data変数にすでに格納済みのため、ここでは他に何も加えなくて良いです。ここで行う唯一のことは、ビューテンプレートが表示するHTMLを何であれechoするようにCakeに伝えることです。上記の例では /app/views/orders/csv/export.ctp になります。csvディレクトリが追加されることを覚えておいてください。アクションが実行される際に、関連付けられたビューファイルを探すようRequestHandlerがCakePHPに伝える場所だからです。

<?php
// File: /app/views/orders/csv/export.ctp

// データ配列をループ
foreach ($data as $row)
{
	// 各行の値をループ
	foreach ($row['Order'] as &$value)
	{
		// 各値の前後に文字列区切りを使用
		$value = "\"".$value."\"";
	}
	// すべての値をカンマ区切りでエコー
	echo implode(",",$row['Order'])."\n";
}
?>

ビューテンプレートではヘッダとデータを出力します。各行をループし、適切な文字区切りを使用し、各行をimplode()メソッドを使って出力し、改行文字で終了します。出力は新しい行で終了します。スプレッドシートでCSVファイルを開くときにはそれがよいようです。

メモ: CSVファイルのビューを表示する際、他の箇所に影響を与えないように、データ中のすべての文字が適切にエスケープされるように注意してください。考えられるのは文字列区切りや、改行文字などです。CSVの出力ファイルをひらく環境によって、最適な対応方法が変わってくるでしょう。CSVフォーマットの概要に関してはこちら(英語)を参照してください。

メモ2: ADmadのコメントが指摘しているように、データをCSVビューファイルで表示するときにヘルパーを使う方法もあります。私の最初のメモにある点をいくらか払拭できるようです。こちら(英語)を参照してください。私の知る限り、このヘルパーはとても役立つでしょう。

ヒント: 出力される内容を利用者に指定してもらうのも簡単にできます。たとえば、私のアプリケーションではフォームで期間を指定してもらうように作成しました。フォームがポストされてファイルが生成されるときに、出力日をファイル名に追加するようにしました。Formヘルパーを使用すれば簡単にできます。

<?php
echo $form->create( 'Order', array( 'url' => '/orders/export/orders_' . date("Ymd") . '.csv' ) );

おしまい

これらが役立つことを願っていますし、改善して使用するのもよいでしょう。上記で示した情報は散らばっていたり、特化されたりしてはいませんが、Cookbookに載せられています。良いコードを書きたいので、質問やコメントがあればお寄せください。

訳者注

ダウンロードする場合には、header()を出力するのがよいのかもしれません。たとえば上記の例で言えば、以下のような内容を orders/csv/export.ctp のループの前に追加しておくとか。

    header( "Content-disposition: attachment; filename=order_" . date( 'Ymd' ) . ".csv" );
    header( "Content-type: application/octet-stream; name=order_" . date( 'Ymd' ) . ".csv" );

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です