CakePHPで内部結合した配列のCSVダウンロード

2014-07-18

cakephp CSVダウンロードといったキーワードで検索すると、色々な方がヘルパーを作成しておられ、とても便利なもので頭が下がります。

しかし、実際の現場では複数のテーブルを結合する必要があるなど、一筋縄ではいきません。ビュー(SQLの)作れよ!ってまぁそうなんですが、そうじゃないケースも有りまして・・・

また、CSVでダウンロードする前にページ内でデータを検索→表示し、内容を確認してから、そのデータをCSVとしてダウンロードするようなケースが多いかと思います。その時点でループする配列は出来上がってるんですから、これをそのまま使えばいいだけなんじゃないか?と考えました。というか、素でPHP書いているときってそういう手順ですよね?(^-^;

配列

//cakephpでUserモデルとAccessモデルを内部結合して吐出された配列と仮定
      $array[0] = array(
           'User'   => array('name' => '山田' ,'email' => 'yamada@example.com')
          ,'Access' => array('created' => '2014-01-01 19:12' ,'modified' => '2014-07-18 12:50')
      );
      $array[1] = array(
           'User'   => array('name' => '鈴木' ,'email' => 'suzuki@example.com')
          ,'Access' => array('created' => '2014-02-01 06:12' ,'modified' => '2014-07-18 09:10')
      );
      $array[2] = array(
           'User'   => array('name' => '佐藤' ,'email' => 'sato@example.com')
          ,'Access' => array('created' => '2014-05-21 12:34' ,'modified' => '2014-07-12 15:38')
      );

CakePHPでテーブル結合した際に吐き出される配列は上のような感じです。User,Accessの両モデルが別々に書きだされます。

classを作る

私はVendorに作っていますが、まぁお好きなとこに。

Vendor/class_csv_download.php

class csvDownload{
  
  var $delimiter = ',';
  var $enclosure = '"';
  var $filename = 'export.csv';
  var $line = array();
  var $write;
  var $header;
  
  public function dl(){
    Configure::write('debug', 0); //debugは0に。運用時のdebugが0なら必要無し
    
    $fp = fopen('php://temp', 'r+b');
    
    fputcsv($fp, $this->header, $this->delimiter, $this->enclosure); //1行目を書き込む
    
    foreach ($this->line as $fields) {
      $setField = array();
      foreach( $this->write AS $element ){
          $key = key($element);  //テーブルのカラム名
          $ele = $element[$key]; //モデル名
          
          $setField[] = $fields[$key][$ele]; //$fieldsから要素を取り出して配列に
      }
        fputcsv($fp, $setField, $this->delimiter, $this->enclosure); 
    }
    rewind($fp);
    $tmp = str_replace(PHP_EOL, "\r\n", stream_get_contents($fp));
        
    header("Content-Type: application/octet-stream");
    header("Content-Disposition: attachment; filename=".$this->filename);
    echo mb_convert_encoding($tmp, 'SJIS-win', 'UTF-8');

    exit;
  }

}

コントローラー側

App::import( "Vendor", "class_csv_download" ); //vendorに作った場合はインポート忘れずに

      $obj =  new csvDownload;
      $obj -> line = $array; //cakeで実行して吐き出した配列
    //1行目
      $obj -> header = array('名前','メールアドレス','登録日','最終更新');
   //CSVに書き出したい項目を設定 
      $obj -> write = array(
           array('User'=>'name')
          ,array('User'=>'email')
          ,array('Access'=>'created')
          ,array('Access'=>'modified')
      );
      $obj -> dl();

まとめ

例えば管理ページで顧客のアクセスログを一覧表示するページが有り、日付等の条件を指定して絞込検索をするような機能があるとします。その時点でSQLが発行されデータ抽出を終えているのですから、そのデータを使おうって考え方です。

App::import( "Vendor", "class_csv_download" );

class AdminController extends AppController  {

  public $layout = 'LAYOUT'; 
  public $uses = array(
          'User'
        , 'Access'
      );
  
  public function userLog() {

    /*
     * 処理
     * ..
     * ......
     * ........
     * ..........
     * 
     */
    
      $array = $this->Model->find('all', $conditions);
      
    //URLパラメータに type=csvがあったら
      if( isset($this->params['url']['type']) AND $this->params['url']['type'] === 'csv'){
        
            $obj =  new csvDownload;
            $obj -> line = $array;
            $obj -> header = array('名前','メールアドレス','登録日','最終更新'); //1行目
         //CSVに書き出したい項目を設定 
            $obj -> write = array(
                 array('User'=>'name')
                ,array('User'=>'email')
                ,array('Access'=>'created')
                ,array('Access'=>'modified')
            );
            $obj -> dl();
      }

  }

}

この方法ならSQLで吐き出した配列に限らず、規則に沿った配列を作れば、どんなものでもCSVに書き出す事が出来るはずです。いちいちそれ用のsql作らなくていいですし、書き出したい項目も自由に変更出来ます。
ただ、foreachの中で必要なデータを取り出す為にforeachしてますので、データ量が多いとレスポンス悪いかな・・とは思います。