Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.95% |
191 / 195 |
|
91.18% |
31 / 34 |
CRAP | |
0.00% |
0 / 1 |
Csv | |
97.95% |
191 / 195 |
|
91.18% |
31 / 34 |
115 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
7 | |||
setOptions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
serialize | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
unserialize | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setString | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSerialized | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isUnserialized | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
outputToHttp | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
outputBlankFileToHttp | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
prepareHttp | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
writeToFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
appendData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
appendRow | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
writeBlankFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
4 | |||
loadFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDataFromFile | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
writeDataToFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
writeTemplateToFile | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
outputDataToHttp | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
outputTemplateToHttp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
processOptions | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
appendDataToFile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
appendRowToFile | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
8 | |||
serializeData | |
92.86% |
26 / 28 |
|
0.00% |
0 / 1 |
15.08 | |||
unserializeString | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
10 | |||
serializeRow | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
19 | |||
getFieldHeaders | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
isValid | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
__toString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (https://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@noladev.com> |
7 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
8 | * @license https://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Csv; |
15 | |
16 | /** |
17 | * CSV class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Csv |
21 | * @author Nick Sagona, III <dev@noladev.com> |
22 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
23 | * @license https://www.popphp.org/license New BSD License |
24 | * @version 4.2.0 |
25 | */ |
26 | class Csv |
27 | { |
28 | |
29 | /** |
30 | * CSV data in PHP |
31 | * @var mixed |
32 | */ |
33 | protected mixed $data = null; |
34 | |
35 | /** |
36 | * CSV string |
37 | * @var ?string |
38 | */ |
39 | protected ?string $string = null; |
40 | |
41 | /** |
42 | * CSV options |
43 | * @var array |
44 | */ |
45 | protected array $options = [ |
46 | 'exclude' => [], |
47 | 'include' => [], |
48 | 'delimiter' => ',', |
49 | 'enclosure' => '"', |
50 | 'escape' => '"', |
51 | 'fields' => true, |
52 | 'newline' => true, |
53 | 'limit' => 0, |
54 | 'map' => [], |
55 | 'columns' => [], |
56 | ]; |
57 | |
58 | /** |
59 | * Constructor |
60 | * |
61 | * Instantiate the Csv object. |
62 | * |
63 | * @param mixed $data |
64 | * @param ?array $options |
65 | */ |
66 | public function __construct(mixed $data = null, array $options = null) |
67 | { |
68 | if ($data !== null) { |
69 | // If data is a file |
70 | if (is_string($data) && (stripos($data, '.csv') !== false) && file_exists($data)) { |
71 | $this->string = file_get_contents($data); |
72 | // Else, if it's just data |
73 | } else if (!is_string($data)) { |
74 | $this->data = $data; |
75 | // Else if it's a string or stream of data |
76 | } else { |
77 | $this->string = $data; |
78 | } |
79 | } |
80 | if (!empty($options)) { |
81 | $this->setOptions($options); |
82 | } |
83 | } |
84 | |
85 | /** |
86 | * Serialize the data to a CSV string |
87 | * |
88 | * @param array $options |
89 | * @return self |
90 | */ |
91 | public function setOptions(array $options): self |
92 | { |
93 | if (!empty($options)) { |
94 | $this->options = self::processOptions($options); |
95 | } |
96 | |
97 | return $this; |
98 | } |
99 | |
100 | /** |
101 | * Serialize the data to a CSV string |
102 | * |
103 | * @param ?array $options |
104 | * @return string |
105 | */ |
106 | public function serialize(?array $options = null): string |
107 | { |
108 | if (!empty($options)) { |
109 | $this->setOptions($options); |
110 | } |
111 | $this->string = self::serializeData($this->data, $this->options); |
112 | return $this->string; |
113 | } |
114 | |
115 | /** |
116 | * Unserialize the string to data |
117 | * |
118 | * @param ?array $options |
119 | * @return mixed |
120 | */ |
121 | public function unserialize(?array $options = null): mixed |
122 | { |
123 | if (!empty($options)) { |
124 | $this->setOptions($options); |
125 | } |
126 | $this->data = self::unserializeString($this->string, $this->options); |
127 | return $this->data; |
128 | } |
129 | |
130 | /** |
131 | * Set data |
132 | * |
133 | * @param array $data |
134 | * @return Csv |
135 | */ |
136 | public function setData(array $data): Csv |
137 | { |
138 | $this->data = $data; |
139 | return $this; |
140 | } |
141 | |
142 | /** |
143 | * Get data |
144 | * |
145 | * @return array |
146 | */ |
147 | public function getData(): array |
148 | { |
149 | return $this->data; |
150 | } |
151 | |
152 | /** |
153 | * Set string |
154 | * |
155 | * @param string $string |
156 | * @return Csv |
157 | */ |
158 | public function setString(string $string): Csv |
159 | { |
160 | $this->string = $string; |
161 | return $this; |
162 | } |
163 | |
164 | /** |
165 | * Get string |
166 | * |
167 | * @return string |
168 | */ |
169 | public function getString(): string |
170 | { |
171 | return $this->string; |
172 | } |
173 | |
174 | /** |
175 | * Check if data was serialized |
176 | * |
177 | * @return bool |
178 | */ |
179 | public function isSerialized(): bool |
180 | { |
181 | return ($this->string !== null); |
182 | } |
183 | |
184 | /** |
185 | * Check if string was unserialized |
186 | * |
187 | * @return bool |
188 | */ |
189 | public function isUnserialized(): bool |
190 | { |
191 | return ($this->data !== null); |
192 | } |
193 | |
194 | /** |
195 | * Output CSV string data to HTTP |
196 | * |
197 | * @param string $filename |
198 | * @param bool $forceDownload |
199 | * @param array $headers |
200 | * @return void |
201 | */ |
202 | public function outputToHttp(string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = []): void |
203 | { |
204 | // Attempt to serialize data if it hasn't been done yet |
205 | if (($this->string === null) && ($this->data !== null)) { |
206 | $this->serialize(); |
207 | } |
208 | |
209 | $this->prepareHttp($filename, $forceDownload, $headers); |
210 | |
211 | echo $this->string; |
212 | } |
213 | |
214 | /** |
215 | * Output CSV headers only in a blank file to HTTP |
216 | * |
217 | * @param string $filename |
218 | * @param bool $forceDownload |
219 | * @param array $headers |
220 | * @param string $delimiter |
221 | * @param array $exclude |
222 | * @param array $include |
223 | * @return void |
224 | *@throws Exception |
225 | */ |
226 | public function outputBlankFileToHttp( |
227 | string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = [], |
228 | string $delimiter = ',', array $exclude = [], array $include = [] |
229 | ): void |
230 | { |
231 | // Attempt to serialize data if it hasn't been done yet |
232 | if (($this->string === null) && ($this->data !== null) && isset($this->data[0])) { |
233 | $fieldHeaders = self::getFieldHeaders($this->data[0], $delimiter, $exclude, $include); |
234 | } else { |
235 | throw new Exception('Error: The data has not been set.'); |
236 | } |
237 | |
238 | $this->prepareHttp($filename, $forceDownload, $headers); |
239 | echo $fieldHeaders; |
240 | } |
241 | |
242 | /** |
243 | * Prepare output to HTTP |
244 | * |
245 | * @param string $filename |
246 | * @param bool $forceDownload |
247 | * @param array $headers |
248 | * @return void |
249 | */ |
250 | public function prepareHttp(string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = []): void |
251 | { |
252 | if (!isset($headers['Content-Type'])) { |
253 | $headers['Content-Type'] = 'text/csv'; |
254 | } |
255 | if (!isset($headers['Content-Disposition'])) { |
256 | $headers['Content-Disposition'] = (($forceDownload) ? 'attachment; ' : null) . 'filename=' . $filename; |
257 | } |
258 | |
259 | // Send the headers and output the file |
260 | if (!headers_sent()) { |
261 | header('HTTP/1.1 200 OK'); |
262 | foreach ($headers as $name => $value) { |
263 | header($name . ': ' . $value); |
264 | } |
265 | } |
266 | } |
267 | |
268 | /** |
269 | * Output CSV data to a file |
270 | * |
271 | * @param string $to |
272 | * @return void |
273 | */ |
274 | public function writeToFile(string $to): void |
275 | { |
276 | // Attempt to serialize data if it hasn't been done yet |
277 | if (($this->string === null) && ($this->data !== null)) { |
278 | $this->serialize(); |
279 | } |
280 | |
281 | file_put_contents($to, $this->string); |
282 | } |
283 | |
284 | /** |
285 | * Append additional CSV data to a pre-existing file |
286 | * |
287 | * @param string $file |
288 | * @param array $data |
289 | * @param bool $validate |
290 | * @return self |
291 | */ |
292 | public function appendData(string $file, array $data, bool $validate = true): self |
293 | { |
294 | self::appendDataToFile($file, $data, $this->options, $validate); |
295 | return $this; |
296 | } |
297 | |
298 | /** |
299 | * Append additional CSV row of data to a pre-existing file |
300 | * |
301 | * @param string $file |
302 | * @param array $row |
303 | * @param bool $validate |
304 | * @return self |
305 | */ |
306 | public function appendRow(string $file, array $row, bool $validate = true): self |
307 | { |
308 | self::appendRowToFile($file, $row, $this->options, $validate); |
309 | return $this; |
310 | } |
311 | |
312 | /** |
313 | * Output CSV headers only to a blank file |
314 | * |
315 | * @param string $to |
316 | * @param string $delimiter |
317 | * @param array $exclude |
318 | * @param array $include |
319 | * @return void |
320 | *@throws Exception |
321 | */ |
322 | public function writeBlankFile(string $to, string $delimiter = ',', array $exclude = [], array $include = []): void |
323 | { |
324 | // Attempt to get field headers and output file |
325 | if (($this->string === null) && ($this->data !== null) && isset($this->data[0])) { |
326 | file_put_contents($to, self::getFieldHeaders($this->data[0], $delimiter, $exclude, $include)); |
327 | } else { |
328 | throw new Exception('Error: The data has not been set.'); |
329 | } |
330 | } |
331 | |
332 | /** |
333 | * Load CSV file |
334 | * |
335 | * @param string $file |
336 | * @param ?array $options |
337 | * @return Csv |
338 | */ |
339 | public static function loadFile(string $file, array $options = null): Csv |
340 | { |
341 | $csv = new self($file, $options); |
342 | $csv->unserialize(); |
343 | return $csv; |
344 | } |
345 | |
346 | /** |
347 | * Load CSV string |
348 | * |
349 | * @param string $string |
350 | * @param ?array $options |
351 | * @return Csv |
352 | */ |
353 | public static function loadString(string $string, ?array $options = null): Csv |
354 | { |
355 | $csv = new self($string, $options); |
356 | $csv->unserialize(); |
357 | return $csv; |
358 | } |
359 | |
360 | /** |
361 | * Load CSV data |
362 | * |
363 | * @param array $data |
364 | * @param ?array $options |
365 | * @return Csv |
366 | */ |
367 | public static function loadData(array $data, ?array $options = null): Csv |
368 | { |
369 | $csv = new self($data, $options); |
370 | $csv->serialize(); |
371 | return $csv; |
372 | } |
373 | |
374 | /** |
375 | * Load CSV file and get data |
376 | * |
377 | * @param string $file |
378 | * @param ?array $options |
379 | * @return array |
380 | */ |
381 | public static function getDataFromFile(string $file, ?array $options = null): array |
382 | { |
383 | $csv = new self($file, $options); |
384 | return $csv->unserialize(); |
385 | } |
386 | |
387 | /** |
388 | * Write data to file |
389 | * |
390 | * @param array $data |
391 | * @param string $to |
392 | * @param ?array $options |
393 | * @return void |
394 | */ |
395 | public static function writeDataToFile(array $data, string $to, ?array $options = null): void |
396 | { |
397 | $csv = new self($data, $options); |
398 | $csv->serialize(); |
399 | $csv->writeToFile($to); |
400 | } |
401 | |
402 | /** |
403 | * Write template to file |
404 | * |
405 | * @param array $data |
406 | * @param string $to |
407 | * @param string $delimiter |
408 | * @param array $exclude |
409 | * @param array $include |
410 | * @return void |
411 | * @throws Exception |
412 | */ |
413 | public static function writeTemplateToFile( |
414 | array $data, string $to, string $delimiter = ',', array $exclude = [], array $include = [] |
415 | ): void |
416 | { |
417 | $csv = new self($data); |
418 | $csv->writeBlankFile($to, $delimiter, $exclude, $include); |
419 | } |
420 | |
421 | /** |
422 | * Output data to HTTP |
423 | * |
424 | * @param array $data |
425 | * @param ?array $options |
426 | * @param string $filename |
427 | * @param bool $forceDownload |
428 | * @param array $headers |
429 | * @return void |
430 | */ |
431 | public static function outputDataToHttp( |
432 | array $data, ?array $options = null, string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = [] |
433 | ): void |
434 | { |
435 | $csv = new self($data, $options); |
436 | $csv->serialize(); |
437 | $csv->outputToHttp($filename, $forceDownload, $headers); |
438 | } |
439 | |
440 | /** |
441 | * Output template to HTTP |
442 | * |
443 | * @param array $data |
444 | * @param string $filename |
445 | * @param bool $forceDownload |
446 | * @param array $headers |
447 | * @param string $delimiter |
448 | * @param array $exclude |
449 | * @param array $include |
450 | * @return void |
451 | *@throws Exception |
452 | */ |
453 | public static function outputTemplateToHttp( |
454 | array $data, string $filename = 'pop-data-template.csv', bool $forceDownload = true, |
455 | array $headers = [], string $delimiter = ',', array $exclude = [], array $include = [] |
456 | ): void |
457 | { |
458 | $csv = new self($data); |
459 | $csv->outputBlankFileToHttp($filename, $forceDownload, $headers, $delimiter, $exclude, $include); |
460 | } |
461 | |
462 | /** |
463 | * Process CSV options |
464 | * |
465 | * @param array $options |
466 | * @return array |
467 | */ |
468 | public static function processOptions(array $options): array |
469 | { |
470 | $options['exclude'] ??= []; |
471 | $options['include'] ??= []; |
472 | $options['delimiter'] ??= ','; |
473 | $options['enclosure'] ??= '"'; |
474 | $options['escape'] ??= '"'; |
475 | $options['fields'] ??= true; |
476 | $options['newline'] ??= true; |
477 | $options['limit'] ??= 0; |
478 | $options['map'] ??= []; |
479 | $options['columns'] ??= []; |
480 | |
481 | return $options; |
482 | } |
483 | |
484 | /** |
485 | * Append additional CSV data to a pre-existing file |
486 | * |
487 | * @param string $file |
488 | * @param array $data |
489 | * @param array $options |
490 | * @param bool $validate |
491 | * @throws Exception |
492 | * @return void |
493 | */ |
494 | public static function appendDataToFile(string $file, array $data, array $options = [], bool $validate = true): void |
495 | { |
496 | if (!file_exists($file)) { |
497 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
498 | } |
499 | |
500 | foreach ($data as $row) { |
501 | self::appendRowToFile($file, $row, $options, $validate); |
502 | } |
503 | } |
504 | |
505 | /** |
506 | * Append additional CSV row of data to a pre-existing file |
507 | * |
508 | * @param string $file |
509 | * @param array $row |
510 | * @param array $options |
511 | * @param bool $validate |
512 | * @throws Exception |
513 | * @return void |
514 | */ |
515 | public static function appendRowToFile(string $file, array $row, array $options = [], bool $validate = true): void |
516 | { |
517 | if (!file_exists($file)) { |
518 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
519 | } |
520 | |
521 | if ($validate) { |
522 | $keys = array_keys($row); |
523 | $headers = array_map( |
524 | function($value) { return str_replace('"', '', $value); }, explode(',', trim(fgets(fopen($file, 'r')))) |
525 | ); |
526 | |
527 | if ($keys != $headers) { |
528 | throw new Exception("Error: The new data's columns do not match the CSV files columns."); |
529 | } |
530 | } |
531 | |
532 | if (isset($options['exclude'])) { |
533 | $exclude = (!is_array($options['exclude'])) ? [$options['exclude']] : $options['exclude']; |
534 | } else { |
535 | $exclude = []; |
536 | } |
537 | |
538 | if (isset($options['include'])) { |
539 | $include = (!is_array($options['include'])) ? [$options['include']] : $options['include']; |
540 | } else { |
541 | $include = []; |
542 | } |
543 | |
544 | $options = self::processOptions($options); |
545 | $csvRow = self::serializeRow( |
546 | (array)$row, $exclude, $include, $options['delimiter'], $options['enclosure'], $options['escape'], |
547 | $options['newline'], $options['limit'], $options['map'], $options['columns'] |
548 | ); |
549 | |
550 | file_put_contents($file, $csvRow, FILE_APPEND); |
551 | } |
552 | |
553 | /** |
554 | * Convert the data into CSV format. |
555 | * |
556 | * @param mixed $data |
557 | * @param array $options |
558 | * @return string |
559 | */ |
560 | public static function serializeData(mixed $data, array $options = []): string |
561 | { |
562 | $keys = array_keys($data); |
563 | $isAssoc = false; |
564 | |
565 | foreach ($keys as $key) { |
566 | if (!is_numeric($key)) { |
567 | $isAssoc = true; |
568 | } |
569 | } |
570 | |
571 | if ($isAssoc) { |
572 | $newData = []; |
573 | foreach ($data as $key => $value) { |
574 | $newData = array_merge($newData, $value); |
575 | } |
576 | $data = $newData; |
577 | } |
578 | |
579 | if (isset($options['exclude'])) { |
580 | $exclude = (!is_array($options['exclude'])) ? [$options['exclude']] : $options['exclude']; |
581 | } else { |
582 | $exclude = []; |
583 | } |
584 | |
585 | if (isset($options['include'])) { |
586 | $include = (!is_array($options['include'])) ? [$options['include']] : $options['include']; |
587 | } else { |
588 | $include = []; |
589 | } |
590 | |
591 | $options = self::processOptions($options); |
592 | $csv = ''; |
593 | $firstKey = array_keys($data)[0]; |
594 | |
595 | if (is_array($data) && isset($data[$firstKey]) && |
596 | (is_array($data[$firstKey]) || ($data[$firstKey] instanceof \ArrayObject)) && ($options['fields'])) { |
597 | $csv .= self::getFieldHeaders((array)$data[$firstKey], $options['delimiter'], $exclude, $include); |
598 | } |
599 | |
600 | // Initialize and clean the field values. |
601 | foreach ($data as $value) { |
602 | $csv .= self::serializeRow( |
603 | (array)$value, $exclude, $include, $options['delimiter'], $options['enclosure'], $options['escape'], |
604 | $options['newline'], $options['limit'], $options['map'], $options['columns'] |
605 | ); |
606 | } |
607 | |
608 | return $csv; |
609 | } |
610 | |
611 | /** |
612 | * Parse the CSV string into a PHP array |
613 | * |
614 | * @param string $string |
615 | * @param array $options |
616 | * @return array |
617 | */ |
618 | public static function unserializeString(string $string, array $options = []): array |
619 | { |
620 | $options = self::processOptions($options); |
621 | $lines = preg_split("/((\r?\n)|(\r\n?))/", $string); |
622 | $data = []; |
623 | $fieldKeys = []; |
624 | |
625 | $tempFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pop-csv-tmp-' . time() . '.csv'; |
626 | file_put_contents($tempFile, $string); |
627 | |
628 | if ($options['fields']) { |
629 | $fieldNames = str_getcsv($lines[0], $options['delimiter'], $options['enclosure'], $options['escape']); |
630 | foreach ($fieldNames as $name) { |
631 | $fieldKeys[] = trim($name); |
632 | } |
633 | } |
634 | |
635 | if (($handle = fopen($tempFile, 'r')) !== false) { |
636 | while (($dataFields = fgetcsv($handle, 1000, $options['delimiter'], $options['enclosure'], $options['escape'])) !== false) { |
637 | if (($options['fields']) && (count($dataFields) == count($fieldKeys)) && ($dataFields != $fieldKeys)) { |
638 | $d = []; |
639 | foreach ($dataFields as $i => $value) { |
640 | $d[$fieldKeys[$i]] = $value; |
641 | } |
642 | $data[] = $d; |
643 | } else if ($dataFields != $fieldKeys) { |
644 | $data[] = $dataFields; |
645 | } |
646 | } |
647 | fclose($handle); |
648 | unlink($tempFile); |
649 | } |
650 | |
651 | return $data; |
652 | } |
653 | |
654 | /** |
655 | * Serialize single row of data |
656 | * |
657 | * @param array $value |
658 | * @param array $exclude |
659 | * @param array $include |
660 | * @param string $delimiter |
661 | * @param string $enclosure |
662 | * @param string $escape |
663 | * @param bool $newline |
664 | * @param int $limit |
665 | * @param array $map |
666 | * @param array $columns |
667 | * @return string |
668 | */ |
669 | public static function serializeRow( |
670 | array $value, array $exclude = [], array $include = [], string $delimiter = ',', string $enclosure = '"', |
671 | string $escape = '"', bool $newline = true, int $limit = 0, array $map = [], array $columns = [] |
672 | ): string |
673 | { |
674 | $rowAry = []; |
675 | foreach ($value as $key => $val) { |
676 | if (!in_array($key, $exclude) && (empty($include) || in_array($key, $include))) { |
677 | if (!$newline) { |
678 | $val = str_replace(["\n", "\r"], [" ", " "], $val); |
679 | } |
680 | if ((int)$limit > 0) { |
681 | $val = substr($val, 0, (int)$limit); |
682 | } |
683 | |
684 | // Handle array map/column |
685 | if (is_array($val)) { |
686 | if (!empty($val) && isset($map[$key]) && isset($val[$map[$key]])) { |
687 | $val = $val[$map[$key]]; |
688 | } else if (!empty($val) && isset($columns[$key]) && isset($val[0]) && isset($val[0][$columns[$key]])) { |
689 | $val = implode(',', array_column($val, $columns[$key])); |
690 | } else { |
691 | $val = null; |
692 | } |
693 | } |
694 | |
695 | if (str_contains($val, $enclosure)) { |
696 | $val = str_replace($enclosure, $escape . $enclosure, $val); |
697 | } |
698 | if ((str_contains($val, $delimiter)) || (str_contains($val, "\n")) || |
699 | (str_contains($val, $escape . $enclosure))) { |
700 | $val = $enclosure . $val . $enclosure; |
701 | } |
702 | |
703 | $rowAry[] = $val; |
704 | } |
705 | } |
706 | |
707 | return implode($delimiter, $rowAry) . "\n"; |
708 | } |
709 | |
710 | /** |
711 | * Get field headers |
712 | * |
713 | * @param mixed $data |
714 | * @param string $delimiter |
715 | * @param array $exclude |
716 | * @param array $include |
717 | * @return string |
718 | */ |
719 | public static function getFieldHeaders(mixed $data, string $delimiter = ',', array $exclude = [], array $include = []): string |
720 | { |
721 | $headers = array_keys($data); |
722 | $headersAry = []; |
723 | foreach ($headers as $header) { |
724 | if (!in_array($header, $exclude) && (empty($include) || in_array($header, $include))) { |
725 | $headersAry[] = $header; |
726 | } |
727 | } |
728 | return implode($delimiter, $headersAry) . PHP_EOL; |
729 | } |
730 | |
731 | /** |
732 | * Determine if the string is valid CSV |
733 | * |
734 | * @param string $string |
735 | * @return bool |
736 | */ |
737 | public static function isValid(string $string): bool |
738 | { |
739 | $lines = preg_split("/((\r?\n)|(\r\n?))/", $string); |
740 | $fields = []; |
741 | if (isset($lines[0])) { |
742 | $fields = str_getcsv($lines[0]); |
743 | } |
744 | return (count($fields) > 0); |
745 | } |
746 | |
747 | /** |
748 | * Render CSV string data to string |
749 | * |
750 | * @return string |
751 | */ |
752 | public function __toString(): string |
753 | { |
754 | // Attempt to serialize data if it hasn't been done yet |
755 | if (($this->string === null) && ($this->data !== null)) { |
756 | $this->serialize(); |
757 | } |
758 | |
759 | return $this->string; |
760 | } |
761 | |
762 | } |