Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.95% covered (success)
97.95%
191 / 195
91.18% covered (success)
91.18%
31 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
Csv
97.95% covered (success)
97.95%
191 / 195
91.18% covered (success)
91.18%
31 / 34
115
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 setOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 serialize
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 unserialize
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSerialized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isUnserialized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 outputToHttp
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 outputBlankFileToHttp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 prepareHttp
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 writeToFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 appendData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 appendRow
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 writeBlankFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 loadFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDataFromFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 writeDataToFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 writeTemplateToFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 outputDataToHttp
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 outputTemplateToHttp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 processOptions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 appendDataToFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 appendRowToFile
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 serializeData
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
15.08
 unserializeString
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
10
 serializeRow
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
19
 getFieldHeaders
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 isValid
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 __toString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
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 */
14namespace 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 */
26class 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}