WHAT'S NEW?
Loading...

CakePHP1.2で内容をカスタマイズしたCSVをダウンロードさせる【3/3】

さて、正しくCSVファイルがダウンロードされただろうか。
今回はCSVファイルのカスタマイズなので、ここまでで終わりではない。

このようなCSVファイルを
id item_id name price created
1 1 あきたこまち 1980 2010-01-01 24:59:59
2 2 ささにしき 2034 2010-01-01 24:59:59
3 3 こしひかり 1680 2010-01-01 24:59:59
4 1 あきたこまち 1980 2010-01-01 24:59:59
5 3 こしひかり 1680 2010-01-01 24:59:59

このようにしたいわけだ。

ID 商品名 単価 個数 合計額
1 あきたこまち 1980 2 3,960円
2 ささにしき 2034 1 2,034円
3 こしひかり 1680 2 3,360円

それではやってみよう。




■コントローラのカスタマイズ
まずはコントローラ側で、各商品ごとに集計させるところからはじめる。
基本的には$conditions配列に条件を書けばいいが、まずは素のSQLを指定してみる。
※MySQLを使用
select *, sum(price) as total , count(item_id) as amount
from histories as History
group by History.item_id 
このSQLをphpMyAdminなどで直接実行してみると、データベースフィールドの最後にtotalとamountフィールドが追加されたはずだ。
totalには集計された金額、amountには商品の個数が設定されている。

id item_id name price created total amount
1 1 あきたこまち 1980 2010-01-01 23:59:59 3960 2
2 2 ささにしき 2034 2010-01-01 23:59:59 2034 1
3 3 こしひかり 1680 2010-01-01 23:59:59 3360 2

この中から、item_id、name、price、total、amountを使用すればよい。
つまり、コントローラ側としては任意のフィールドを指定してfindすればよい、と言うことになる。
$conditions にはfirldsという設定があるので、これでフィールド名を設定してみる。
$conditions = array(
  'conditions' => array(
    'History.created >' => '2010-01-01 23:59:59',
  ) ,
  'fields' => array(
    'History.item_id',
    'History.name',
    'History.price',
    'count(History.item_id) as amount',
    'sum(History.price) as total',
  ),
  'group' => array(
    'History.item_id',
  ),
);
$this->set('histories', $this->History->find('all', $conditions));
これを試しにブラウザで表示させると、こうなる。

Id Name Price Amount Total
1 あきたこまち 1980 2 3960
2 ささにしき 2034 1 2034
3 こしひかり 1680 2 3360

これで取得したいデータはそろった。
次は項目名を任意の文字列に変更する。


■ビューのカスタマイズ
現在ビュースクリプトであるcsv.ctpには、細かい設定などが一切されてない状態になっている。
しかしCSVヘルパーにはいくつか便利な機能が含まれているので、それらを使ってカスタマイズすることにする。

まず、項目名はデータベースフィールドのCamelCase方式で表示されてしまっているので、これらを任意の文字列にするときのアルゴリズムを考える。

ちょっとひねった考え方をすると、データベースフィールド名と任意の文字列をセットした、連想配列にして出力すればよいと思われがちだが、もっと簡単に出来る方法がある。
  1. 項目名を非表示設定にする
  2. 1行目を任意の配列として挿入する
これでOKだ。

というわけで、まずは項目名を非表示にする設定をする。
[/Path/To/CakePHP/App/views/histories/csv.ctp]
$csv->addGrid($histories);
$csv->setFilename('支払.csv');
echo mb_convert_encoding($csv->render(), 'SJIS', 'UTF-8');
最初のメソッドであるaddGridだが、引数をいくつか指定することが可能になっている。
以下のように2番目の引数にfalse を指定すると、項目名が表示されなくなる。
$csv->addGrid($histories, false);
これで第1の条件はクリアした。

次に、任意の文字列を表示させる。
任意の文字列は配列で、以下の様な内容だ。
$columns = array(
  'ID' ,
  '商品名',
  '単価',
  '個数',
  '合計額',
);
これを全レコードの先頭に追加できれば良い。

CSVヘルパーでは、全データはaddGridが呼ばれて初めてデータがそろう仕組みになっている。
つまり、addGridを呼ぶ前に、先頭に1行だけ追加できれば良い。
そのためのメソッドが用意されている。そのまんまの名前で、addRowだ。これをaddGridの前に追加する。
$csv->addRow($columns);

最終的にcsv.ctpは以下のようになったはずだ。
[/Path/To/CakePHP/App/views/histories/csv.ctp]
$columns = array(
  'ID',
  '商品名',
  '単価',
  '個数',
  '合計額',
);
$csv->addRow($columns);
$csv->addGrid($histories, false);
$csv->setFilename('支払.csv');
echo mb_convert_encoding($csv->render(), 'SJIS', 'UTF-8');
これで、任意の文字列を項目名とした、任意のフィールドを取得するとが出来る。
しかしこれだけでは終わらない。

■コントローラのカスタマイズ
実はfindで取得したデータをダンプしてみると、非常に厄介な構造になっているのが分かると思う。
ためしにcsvアクションを、次のように編集してみると良い。
//Configure::write('debug', 0); // デバッグ情報不要  
  //$this->layout = false; // layout 不要  
  $conditions = array(
    'conditions' => array(
      'created <' => ’2010-01-01’ 23:59:59',
    ),
    'fields' => array(
      'History.item_id',
      'History.name',
      'History.price',
      'count(History.item_id) as amount',
      'sum(History.price) as total',
    ),
    'group' => array(
      'History.item_id',
    ),
  );
  $histories = $this->History->find('all', $conditions);
  pr($histories);exit(0);
  //$this->set('histories', $this->History->find('all', $conditions)); 

最初の2行、最後の1行をコメントアウトし、findした内容をprで画面にダンプする設定だ。
これでブラウザでcsvアクションの実行結果を見ることが出来る。
ためしに見てみると、
Array
(
    [0] => Array
        (
            [History] => Array
                (
                    [item_id] => 1
                    [name] => あきたこまち
                    [price] => 1980
                )
            [0] => Array
                (
                    [amount] => 2
                    [total] => 3960
                )

        )
    [1] => Array
        (
            [History] => Array
                (
                    [item_id] => 2
                    [name] => ささにしき
                    [price] => 2034
                )
            [0] => Array
                (
                    [amount] => 1
                    [total] => 2034
                )

        )
    [2] => Array
        (
            [History] => Array
                (
                    [item_id] => 3
                    [name] => こしひかり
                    [price] => 1680
                )
            [0] => Array
                (
                    [amount] => 2
                    [total] => 3360
                )
        )
)
なんと言う事か、 item_id、name、priceと兄弟要素として並んでいると思ったamountとtotalが、別の親をインデックスに持つ配列になってしまっている。この場合、[0][amount]、[0][total]だ。

これは非常に迷惑な仕様だ。

先ほどの完成形のままだと、amountとtotalがnullになってcsvファイルに吐き出されてしまう。
なので[History][amount]と[History][total]というようになってないとまずい。
どうにかしないといけない。


ここら辺はビュースクリプトでやるのはおかしいので、コントローラでどうにかして兄弟にしておくこととする。
非常に遺憾なロジックだが、一度配列に代入した全データを総なめし、配列に入れなおすことにする。
$histories= $this->History->find('all', $conditions);
for($i=0; $i<count($histories);$i++) {
  $datas[$i]['History'] = am($histories[$i]['History'], ($histories[$i][0]));
}
これで、$datasをビューに渡せば、以下の様な配列がわたることになる。
Array
(
    [0] => Array
        (
            [History] => Array
                (
                    [item_id] => 1
                    [name] => あきたこまち
                    [price] => 1980
                    [amount] => 2
                    [total] => 3960
                )
        )
    [1] => Array
        (
            [History] => Array
                (
                    [item_id] => 2
                    [name] => ささにしき
                    [price] => 2034
                    [amount] => 1
                    [total] => 2034
                )
        )
    [2] => Array
        (
            [History] => Array
                (
                    [item_id] => 3
                    [name] => こしひかり
                    [price] => 1680
                    [amount] => 2
                    [total] => 3360
                )
        )
)

最終的に出来上がったcsvアクションは以下になる。

[/Path/To/CakePHP/App/controllers/histories_controller.php]
function csv()
{
  Configure::write('debug', 0); // デバッグ情報不要
  $this->layout = false; // layout 不要
  $conditions = array(
    'conditions' => array(
      'created <' => '2010-01-01 23:59:59',
    ),
    'fields' => array(
      'History.item_id',
      'History.name',
      'History.price',
      'count(History.item_id) as amount',
      'sum(History.price) as total',
    ),
    'group' => array(
     'History.item_id',
    ),
  );

  $histories = $this->History->find('all', $conditions);
  for($i=0; $i<count($histories);$i++) {
    $datas[$i]['History'] = am($histories[$i]['History'],
      $histories[$i][0]
    );
  }

  $this->set('histories', $datas);
}


結構な面倒くささだ。
ここら辺まとめてコンポーネントにしておくと便利かもしれない。
とにかくお試しあれ。

じゃぁな。