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