Export::exportCsv()   F
last analyzed

Complexity

Conditions 13
Paths 1024

Size

Total Lines 99
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 13
eloc 55
c 3
b 1
f 0
nc 1024
nop 3
dl 0
loc 99
ccs 0
cts 52
cp 0
crap 182
rs 2.45

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Mvc\Controller\Traits;
13
14
use Exception;
15
use League\Csv\ByteSequence;
16
use League\Csv\CannotInsertRecord;
17
use League\Csv\CharsetConverter;
18
use League\Csv\InvalidArgument;
19
use League\Csv\Writer;
20
use Phalcon\Http\ResponseInterface;
21
use Shuchkin\SimpleXLSXGen;
22
use Spatie\ArrayToXml\ArrayToXml;
23
use Zemit\Support\Slug;
24
use Zemit\Mvc\Controller\Traits\Abstracts\AbstractModel;
25
use Zemit\Mvc\Controller\Traits\Abstracts\AbstractParams;
26
27
/**
28
 * Provides some utility methods to export data
29
 */
30
trait Export
31
{
32
    use AbstractParams;
33
    use AbstractModel;
34
    
35
    /**
36
     * Get the content type based on the given parameters.
37
     *
38
     * @param array|null $params Optional. The parameters to determine the content type. If not provided, it will use the default parameters.
39
     * @return string The content type. Possible values: "json", "csv", "xlsx".
40
     * @throws Exception When an unsupported content type is provided.
41
     */
42
    public function getContentType(?array $params = null): string
43
    {
44
        $params ??= $this->getParams();
45
        $contentType = strtolower($params['contentType'] ?? $params['content-type'] ?? $this->request->getContentType() ?? '');
46
        
47
        switch ($contentType) {
48
            case 'xml':
49
            case 'text/xml':
50
            case 'application/xml':
51
                return 'xml';
52
            
53
            case 'json':
54
            case 'text/json':
55
            case 'application/json':
56
                return 'json';
57
            
58
            case 'csv':
59
            case 'text/csv':
60
                return 'csv';
61
            
62
            case 'xlsx':
63
            case 'application/xlsx':
64
            case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
65
                return 'xlsx';
66
        }
67
        
68
        throw new Exception('`' . $contentType . '` is not supported.', 400);
69
    }
70
    
71
    /**
72
     * Returns the filename for the exported file.
73
     *
74
     * The filename is generated based on the model class name, with any
75
     * namespaces replaced by slashes, and then slugified. It is then
76
     * prepended with the current date in the 'Y-m-d' format.
77
     *
78
     * @return string The generated filename for the exported file.
79
     */
80
    public function getFilename(): string
81
    {
82
        $suffix = ' List (' . date('Y-m-d') . ')';
83
        $modelName = $this->getModelName() ?? '';
84
        return ucfirst(
85
            Slug::generate(
86
                basename(
87
                    str_replace('\\', '/', $modelName)
88
                )
89
            )
90
        ) . $suffix;
91
    }
92
    
93
    /**
94
     * Retrieves the columns from the given list of data.
95
     *
96
     * @param array $list The list of data to extract columns from.
97
     *
98
     * @return array An associative array containing the export columns as keys.
99
     */
100
    public function getExportColumns(array $list): array
101
    {
102
        $columns = [];
103
        foreach ($list as $row) {
104
            $columns += array_flip($row);
105
        }
106
        return array_keys($columns);
107
    }
108
    
109
    /**
110
     * Exports the given list to a specified file in the specified format.
111
     *
112
     * @param array $list The list of data to export.
113
     * @param string|null $filename The filename of the exported file. If not provided, the default filename will be used.
114
     * @param string|null $contentType The content type of the exported file. If not provided, the default content type will be used.
115
     * @param array|null $params Additional parameters for the export process. If not provided, the default parameters will be used.
116
     *
117
     * @return ResponseInterface Returns true if the export was successful, otherwise false.
118
     *
119
     * @throws Exception Thrown if the specified content type is not supported.
120
     */
121
    public function export(array $list = [], string $filename = null, string $contentType = null, array $params = null): ResponseInterface
122
    {
123
        $params ??= $this->getParams();
124
        $contentType ??= $this->getContentType();
125
        $filename ??= $this->getFilename();
126
        
127
        return match ($contentType) {
128
            'json' => $this->exportJson($list, $filename),
129
            'xml' => $this->exportXml($list, $filename),
130
            'csv' => $this->exportCsv($list, $filename, $params),
131
            'xlsx' => $this->exportExcel($list, $filename),
132
            default => throw new Exception('Failed to export `' . $this->getModelName() . '` using unsupported content-type `' . $contentType . '`', 400)
133
        };
134
    }
135
    
136
    /**
137
     * Exports the given list to an XML file with the specified filename.
138
     *
139
     * @param array $list The list of data to export.
140
     * @param string|null $filename The filename of the exported XML file. If not provided, a default filename will be used.
141
     *
142
     * @return ResponseInterface
143
     */
144
    public function exportXml(array $list, ?string $filename = null, ?array $params = null): ResponseInterface
145
    {
146
        $params ??= $this->getParams();
147
        $filename ??= $this->getFilename();
148
        
149
        $rootElement = $params['rootElement'] ?? '';
150
        $replaceSpacesByUnderScoresInKeyNames = $params['replaceSpacesByUnderScoresInKeyNames'] ?? true;
151
        $xmlEncoding = $params['xmlEncoding'] ?? null;
152
        $xmlVersion = $params['xmlVersion'] ?? '1.0';
153
        $domProperties = $params['domProperties'] ?? [];
154
        $xmlStandalone = $params['xmlStandalone'] ?? null;
155
        $addXmlDeclaration = $params['addXmlDeclaration'] ?? true;
156
        $options = $params['options'] ?? ['convertNullToXsiNil' => false];
157
        
158
        $result = ArrayToXml::convert(
159
            $list,
160
            $rootElement,
161
            $replaceSpacesByUnderScoresInKeyNames,
162
            $xmlEncoding,
163
            $xmlVersion,
164
            $domProperties,
165
            $xmlStandalone,
166
            $addXmlDeclaration,
167
            $options,
168
        );
169
        
170
        $this->response->setContent($result);
171
        $this->response->setContentType('application/xml');
172
        $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($filename) . '.xml"');
173
        
174
        return $this->response;
175
    }
176
    
177
    /**
178
     * Export data as JSON file for download.
179
     *
180
     * @param mixed $list The data to be exported as JSON. Can be an array, object, or any serializable data type.
181
     * @param string|null $filename The name of the exported file. If not provided, the default filename will be used.
182
     * @param int $flags Optional JSON encoding options. Default is JSON_PRETTY_PRINT.
183
     * @param int $depth Optional maximum depth of recursion. Default is 2048.
184
     *
185
     * @return ResponseInterface
186
     */
187
    public function exportJson(mixed $list, ?string $filename = null, int $flags = JSON_PRETTY_PRINT, int $depth = 2048): ResponseInterface
188
    {
189
        $filename ??= $this->getFilename();
190
191
//        $this->response->setJsonContent($list); // bug with phalcon, avoid
192
        $this->response->setContent(json_encode($list, $flags, $depth));
193
        $this->response->setContentType('application/json');
194
        $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($filename) . '.json"');
195
        
196
        return $this->response;
197
    }
198
    
199
    /**
200
     * Export data as an Excel spreadsheet
201
     *
202
     * @param array $list The data to be exported
203
     * @param string|null $filename The desired filename for the exported file (optional)
204
     *
205
     * @return ResponseInterface
206
     */
207
    public function exportExcel(array $list, ?string $filename = null, bool $forceRawValue = true): ResponseInterface
208
    {
209
        $filename ??= $this->getFilename();
210
        $columns = $this->getExportColumns($list);
211
        
212
        $export = [];
213
        $export [] = $columns;
214
        
215
        foreach ($list as $record) {
216
            $row = [];
217
            foreach ($columns as $column) {
218
                // Prefix #0 cell value to force raw value
219
                $row[$column] = ($forceRawValue? "\0" : '')  . ($record[$column] ?? '');
220
            }
221
            $export [] = array_values($row);
222
        }
223
        
224
        $xlsx = SimpleXLSXGen::fromArray($export);
225
        
226
        $this->response->setContent($xlsx);
227
        $this->response->setContentType('application/json');
228
        $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($filename) . '.json"');
229
        
230
        return $this->response;
231
//        return $xlsx->downloadAs($filename . '.xlsx');
232
    }
233
    
234
    /**
235
     * @return ResponseInterface
236
     * @throws InvalidArgument
237
     * @throws CannotInsertRecord
238
     * @throws \League\Csv\Exception
239
     */
240
    public function exportCsv(array $list, ?string $filename = null, ?array $params = null): ResponseInterface
241
    {
242
        $filename ??= $this->getFilename();
243
        $params ??= $this->getParams();
244
        $columns = $this->getExportColumns($list);
245
        
246
        // Get CSV custom request parameters
247
        $mode = $params['mode'] ?? null;
248
        $delimiter = $params['delimiter'] ?? null;
249
        $enclosure = $params['enclosure'] ?? null;
250
        $endOfLine = $params['endOfLine'] ?? null;
251
        $escape = $params['escape'] ?? null;
252
        $outputBOM = $params['outputBOM'] ?? null;
253
        $skipIncludeBOM = $params['skipIncludeBOM'] ?? false;
254
        $relaxEnclosure = $params['relaxEnclosure'] ?? false;
255
        $keepEndOfLines = $params['keepEndOfLines'] ?? false;
256
257
//            $csv = Writer::createFromFileObject(new \SplTempFileObject());
258
        $csv = Writer::createFromStream(fopen('php://memory', 'r+'));
259
        
260
        // CSV - MS Excel on MacOS
261
        if ($mode === 'mac') {
262
            $csv->setOutputBOM(ByteSequence::BOM_UTF16_LE); // utf-16
263
            $csv->setDelimiter("\t"); // tabs separated
264
            $csv->setEndOfLine("\r\n"); // end of lines
265
            CharsetConverter::addTo($csv, 'UTF-8', 'UTF-16');
266
        }
267
        
268
        // CSV - MS Excel on Windows
269
        else {
270
            $csv->setOutputBOM(ByteSequence::BOM_UTF8); // utf-8
271
            $csv->setDelimiter(','); // comma separated
272
            $csv->setEndOfLine("\r\n"); // end of lines
273
            CharsetConverter::addTo($csv, 'UTF-8', 'UTF-8');
274
        }
275
        
276
        // relax enclosure
277
        if ($relaxEnclosure) {
278
            $csv->relaxEnclosure();
279
        }
280
        // force enclosure
281
        else {
282
            $csv->forceEnclosure();
283
        }
284
        // set enclosure
285
        if (isset($enclosure)) {
286
            $csv->setEnclosure($enclosure);
287
        }
288
        // set output bom
289
        if (isset($outputBOM)) {
290
            $csv->setOutputBOM($outputBOM);
291
        }
292
        // set delimiter
293
        if (isset($delimiter)) {
294
            $csv->setDelimiter($delimiter);
295
        }
296
        // send end of line
297
        if (isset($endOfLine)) {
298
            $csv->setEndOfLine($endOfLine);
299
        }
300
        // set escape
301
        if (isset($escape)) {
302
            $csv->setEscape($escape);
303
        }
304
        // force '\\'
305
        else {
306
            $csv->setEscape('\\');
307
        }
308
        // skip include bom
309
        if ($skipIncludeBOM) {
310
            $csv->skipInputBOM();
311
        }
312
        // include bom
313
        else {
314
            $csv->includeInputBOM();
315
        }
316
        
317
        // Headers
318
        $csv->insertOne($columns);
319
        
320
        foreach ($list as $row) {
321
            $outputRow = [];
322
            foreach ($columns as $column) {
323
                $outputRow[$column] = $row[$column] ?? '';
324
                
325
                // sometimes excel can't process the cells multiple lines correctly when loading csv
326
                // this is why we remove the new lines by default, user can choose to keep them using $keepEndOfLines
327
                if (!$keepEndOfLines && is_string($outputRow[$column])) {
328
                    $outputRow[$column] = trim(preg_replace('/\s+/', ' ', $outputRow[$column]));
329
                }
330
            }
331
            $csv->insertOne($outputRow);
332
        }
333
        
334
        $this->response->setContent((string)$csv);
335
        $this->response->setContentType('text/csv');
336
        $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($filename) . '.csv"');
337
        
338
        return $this->response;
339
//        $csv->output($filename . '.csv');
340
//        return true;
341
    }
342
}
343