May 25, 2013

CsvExport Behavior for CakePHP 2

This behavior is the exact opposite of the CsvImport behavior included in the Utils plugin from CakeDC. This Behavior will end on github but for now, here is the source code and instructions on how to use the Behavior.

Installation

Copy the below source code into app/Model/Behavior/CsvExportBehavior.php.

Behavior Usage

Enable the behavior inside your Model(s), if you would like csv export functionality add the behavior to AppModel.
1
2
3
public $actsAs = array(
    'CsvExport'
);
The following shows all of the available configuration options along with their default values.
1
2
3
4
5
6
7
8
public $actsAs = array(
    'CsvExport' => array(
        'delimiter'  => ';', //The delimiter for the values, default is ;
        'enclosure' => '"', //The enclosure, default is "
        'max_execution_time' => 360, //Increase for Models with lots of data, has no effect is php safemode is enabled.
        'encoding' => 'utf8' //Prefixes the return file with a BOM and attempts to utf_encode() data
    )      
);

Using the Behavior from your Controller

The following controller action will prompt to download a csv formatted file containing all of the records in your Model’s underlying database table. If you would like this export functionality for all controllers in your application add this method to AppController.
1
2
3
4
5
6
7
public function export() {
    $this->autoRender = false;
    $modelClass = $this->modelClass;
    $this->response->type('Content-Type: text/csv');
    $this->response->download( strtolower( Inflector::pluralize( $modelClass ) ) . '.csv' );
    $this->response->body( $this->$modelClass->exportCSV() );
}

CsvExportBehavior.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
<?php
/**
 * Copyright 2011, PRONIQUE Software (http://www.pronique.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2011, PRONIQUE Software (http://www.pronique.com)
 */
 
/**
 *
 *
 * Csv Export Behavior
 *
 * @subpackage models.behaviors
 */
class CsvExportBehavior extends ModelBehavior {
 
/**
 * Exportable behavior settings
 *
 * @var array
 */
    public $settings = array();
 
/**
 * List of errors generated by the export action
 *
 * @var array
 */
    public $errors = array();
 
/**
 * List of objects instances or callables to notify from events on this class
 *
 * @var array
 */
    protected $_subscribers = array();
 
    protected $tmpDir = 'csvexport';
/**
 * Initializes this behavior for the model $Model
 *
 * @param Model $Model
 * @param array $settigs list of settings to be used for this model
 * @return void
 */
    public function setup(Model &$Model, $settings = array()) {
        if (!isset($this->settings[$Model->alias])) {
            $this->settings[$Model->alias] = array(
                'encoding' => 'utf8',
                'delimiter' => ';',
                'enclosure' => '"',
                'max_execution_time' => 360
            );
        }
        $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings);
    }
 
     
/**
 * Returns a csv formatted string of every record in the model
 *
 * @param Model $Model
 * @return string the csv data
 */
    public function exportCSV(Model &$Model ) {
        if( !ini_get('safe_mode') && ini_get('max_execution_time') < $this->settings[$Model->alias]['max_execution_time']  ){
            set_time_limit($this->settings[$Model->alias]['max_execution_time']); //Extend timout to 6 minutes for large data exports.
        }
         
        $Model->recursive = -1;
        $records = $Model->find('all');
        if ( !empty($records ) ) {
            $this->ensureTmp();
             
            $tmpFilename = TMP . $this->tmpDir . DS .  strtolower( Inflector::pluralize($Model->alias) ) . '-' . date('Ymd-Hms') . '.csv';
            $fp = fopen($tmpFilename, 'w');
            if ( $this->settings[$Model->alias]['encoding'] == 'utf8' ) {
                fwrite($fp, "\xEF\xBB\xBF");
                fputcsv( $fp, $this->arrayToUtf8( array_keys($records[0][$Model->alias]) ), $this->settings[$Model->alias]['delimiter'], $this->settings[$Model->alias]['enclosure'] );
            } else {
                fputcsv( $fp, array_keys($records[0][$Model->alias]), $this->settings[$Model->alias]['delimiter'], $this->settings[$Model->alias]['enclosure'] );
            }
             
            foreach( $records as $record ) {
                if ( $this->settings[$Model->alias]['encoding'] == 'utf8' ) {
                    fputcsv($fp, $this->arrayToUtf8( $record[$Model->alias] ), $this->settings[$Model->alias]['delimiter'], $this->settings[$Model->alias]['enclosure'] );
                } else {
                    fputcsv($fp, $record[$Model->alias], $this->settings[$Model->alias]['delimiter'], $this->settings[$Model->alias]['enclosure'] );
                }
            }
            fclose( $fp );
            $data = file_get_contents( $tmpFilename );
            $this->cleanupTmp( $tmpFilename );
            return $data;
        }
        return false;
    }
 
 
/**
 * Returns the errors generated by last export
 *
 * @param Model $Model
 * @return array
 */
    public function getExportErrors(Model &$Model) {
        if (empty($this->errors[$Model->alias])) {
            return array();
        }
        return $this->errors[$Model->alias];
    }
 
/**
 * Attachs a new listener for the events generated by this class
 *
 * @param Model $Model
 * @param mixed listener instances of an object or valid php callback
 * @return void
 */
    public function attachExportListener(Model $Model, $listener) {
        $this->_subscribers[$Model->alias][] = $listener;
    }
 
/**
 * Notifies the listeners of events generated by this class
 *
 * @param Model $Model
 * @param string $action the name of the event. It will be used as method name for object listeners
 * @param mixed $data additional information to pass to the listener callback
 * @return void
 */
    protected function _notify(Model $Model, $action, $data = null) {
        if (empty($this->_subscribers[$Model->alias])) {
            return;
        }
        foreach ($this->_subscribers[$Model->alias] as $object) {
            if (method_exists($object, $action)) {
                $object->{$action}($data);
            }
            if (is_callable($object)) {
                call_user_func($object, $action, $data);
            }
        }
    }
     
    /**
    * This Behavior writes tmp files to take advantage of the built-in fputcsv function.
    *
    */
    protected function ensureTmp() {
        $tmpDir = TMP . $this->tmpDir;
        if ( !file_exists($tmpDir ) ) {
            mkdir( $tmpDir, 0777);
        }
    }
    /**
    * Delete the tmp file, only if $tmp_file lives in TMP directory
    * otherwise throw an Exception
    *
    * @param mixed $tmp_file
    */
    protected function cleanupTmp( $tmp_file='' ) {
        $realpath = realpath( $tmp_file );
         
        if ( substr( $realpath, 0, strlen( TMP ) ) != TMP ) {
            throw new Exception('I refuse to delete a file outside of ' . TMP );
        }
         
        if ( file_exists( $tmp_file ) ) {
            unlink( $tmp_file );
        }
    }
     
    /**
    * if array is not UTF-8 then convert keys and values to UTF-8
    * method is recursive
    *
    * @param mixed $in
    */
    protected function arrayToUtf8($in) {
        if (is_array($in)) {
            foreach ($in as $key => $value) {
                $out[$this->arrayToUtf8($key)] = $this->arrayToUtf8($value);
            }
        } elseif(is_string($in)) {
            if(mb_detect_encoding($in) != "UTF-8")
                return utf8_encode($in);
            else
                return $in;
        } else {
            return $in;
        }
        return $out;
    }
}

License

No comments:

Post a Comment