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
|
|
|
|