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