Completed
Branch releases/v0.2 (d913c4)
by Luke
02:17
created

Writer::prepareData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 9.4285
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * CSVelte: Slender, elegant CSV for PHP
4
 * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV
5
 * standardization efforts, CSVelte was written in an effort to take all the
6
 * suck out of working with CSV.
7
 *
8
 * @version   v0.2
9
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
10
 * @author    Luke Visinoni <[email protected]>
11
 * @license   https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT)
12
 */
13
namespace CSVelte;
14
15
use \Iterator;
16
use \ArrayIterator;
17
use CSVelte\Contract\Writable;
18
use CSVelte\Table\Data;
19
use CSVelte\Table\HeaderRow;
20
use CSVelte\Table\Row;
21
use CSVelte\Flavor;
22
use CSVelte\Reader;
23
24
use \InvalidArgumentException;
25
use CSVelte\Exception\WriterException;
26
27
/**
28
 * CSVelte Writer Base Class
29
 * A PHP CSV utility library (formerly PHP CSV Utilities).
30
 *
31
 * @package   CSVelte
32
 * @copyright (c) 2016, Luke Visinoni <[email protected]>
33
 * @author    Luke Visinoni <[email protected]>
34
 * @todo Buffer write operations so that you can call things like setHeaderRow()
35
 *     and change flavor and all that jivey divey goodness at any time.
36
 */
37
class Writer
38
{
39
    /**
40
     * @var \CSVelte\Flavor
41
     */
42
    protected $flavor;
43
44
    /**
45
     * @var \CSVelte\Contract\Writable
46
     */
47
    protected $output;
48
49
    /**
50
     * @var \Iterator
51
     */
52
    protected $headers;
53
54
    /**
55
     * @var int lines of data written so far (not including header)
56
     */
57
    protected $written = 0;
58
59
    /**
60
     * Class Constructor
61
     *
62
     * @param \CSVelte\Contract\Writable $output An output source to write to
63
     * @param \CSVelte\Flavor|array $flavor A flavor or set of formatting params
64
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
65
     * @access public
66
     */
67 17
    public function __construct(Writable $output, $flavor = null)
68
    {
69 17
        if (!($flavor instanceof Flavor)) $flavor = new Flavor($flavor);
70 17
        $this->flavor = $flavor;
71 17
        $this->output = $output;
72 17
    }
73
74
    /**
75
     * Get the CSV flavor (or dialect) for this writer
76
     *
77
     * @param void
78
     * @return \CSVelte\Flavor
79
     * @access public
80
     */
81 16
    public function getFlavor()
82
    {
83 16
        return $this->flavor;
84
    }
85
86
    /**
87
     * Sets the header row
88
     * If any data has been written to the output, it is too late to write the
89
     * header row and an exception will be thrown. Later implementations will
90
     * likely buffer the output so that this may be called after writeRows()
91
     *
92
     * @param \Iterator|array A list of header values
93
     * @return boolean
94
     * @throws \CSVelte\Exception\WriterException
95
     */
96 2
    public function setHeaderRow($headers)
97
    {
98 2
        if ($this->written) {
99 1
            throw new WriterException("Cannot set header row once data has already been written. ");
100
        }
101 1
        if (is_array($headers)) $headers = new ArrayIterator($headers);
102 1
        $this->headers = $headers;
103 1
    }
104
105
    /**
106
     * Write a single row
107
     *
108
     * @param \Iterator|array $row The row to write to source
109
     * @return int The number or bytes written
110
     */
111 14
    public function writeRow($row)
112
    {
113 14
        $eol = $this->getFlavor()->lineTerminator;
114 14
        $delim = $this->getFlavor()->delimiter;
115 14
        if (!$this->written && $this->headers) {
116 1
            $headerRow = new HeaderRow((array) $this->headers);
117 1
            $this->writeHeaderRow($headerRow);
118 1
        }
119 14
        if (is_array($row)) $row = new ArrayIterator($row);
120 14
        $row = $this->prepareRow($row);
121 14
        if ($count = $this->output->writeLine($row->join($delim), $eol)) {
122 14
            $this->written++;
123 14
            return $count;
124
        }
125
    }
126
127 3
    protected function writeHeaderRow(HeaderRow $row)
128
    {
129 3
        $eol = $this->getFlavor()->lineTerminator;
130 3
        $delim = $this->getFlavor()->delimiter;
131 3
        $row = $this->prepareRow($row);
132 3
        $header = new HeaderRow($row->toArray());
0 ignored issues
show
Unused Code introduced by
$header is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
133 3
        return $this->output->writeLine($row->join($delim), $eol);
134
    }
135
136
    /**
137
     * Write multiple rows
138
     *
139
     * @param \Iterable|array $rows List of \Iterable|array
140
     * @return int number of lines written
141
     * @access public
142
     */
143 11
    public function writeRows($rows)
144
    {
145 11
        if (is_array($rows)) $rows = new ArrayIterator($rows);
146 11
        if (!($rows instanceof Iterator)) {
147 1
            throw new InvalidArgumentException('First argument for ' . __METHOD__ . ' must be iterable');
148
        }
149 10
        $written = 0;
150 10
        if ($rows instanceof Reader) {
151 2
            $this->writeHeaderRow($rows->header());
152 2
        }
153 10
        foreach ($rows as $row) {
154 10
            if ($this->writeRow($row)) $written++;
155 10
        }
156 10
        return $written;
157
    }
158
159
    /**
160
     * Prepare a row of data to be written
161
     * This means taking an array of data, and converting it to a Row object
162
     *
163
     * @param \Iterator|array of data items
164
     * @return CSVelte\Table\AbstractRow
165
     * @access protected
166
     */
167 14
    protected function prepareRow(Iterator $row)
168
    {
169 14
        $items = array();
170 14
        foreach ($row as $data) {
171 14
            $items []= $this->prepareData($data);
172 14
        }
173 14
        $row = new Row($items);
174 14
        return $row;
175
    }
176
177
    /**
178
     * Prepare a cell of data to be written (convert to Data object)
179
     *
180
     * @param string $data A string containing cell data
181
     * @return string quoted string data
182
     * @access protected
183
     */
184 14
    protected function prepareData($data)
185
    {
186
        // @todo This can't be properly implemented until I get Data and DataType right...
187
        // it should be returning a Data object but until I get that working properly
188
        // it's just going to have to return a string
189 14
        return $this->quoteString($data);
190
    }
191
192 14
    protected function quoteString($str)
193
    {
194 14
        $flvr = $this->getFlavor();
195
        // Normally I would make this a method on the class, but I don't intend
196
        // to use it for very long, in fact, once I finish writing the Data class
197
        // it is gonezo!
198 14
        $hasSpecialChars = function($s) use ($flvr) {
199 11
            $specialChars = preg_quote($flvr->lineTerminator . $flvr->quoteChar . $flvr->delimiter);
200 11
            $pattern = "/[{$specialChars}]/m";
201 11
            return preg_match($pattern, $s);
202 14
        };
203 14
        switch($flvr->quoteStyle) {
204 14
            case Flavor::QUOTE_ALL:
205 1
                $doQuote = true;
206 1
                break;
207 13
            case Flavor::QUOTE_NONNUMERIC:
208 1
                $doQuote = !is_numeric($str);
209 1
                break;
210 12
            case Flavor::QUOTE_MINIMAL:
211 11
                $doQuote = $hasSpecialChars($str);
212 11
                break;
213 1
            case Flavor::QUOTE_NONE:
214 1
            default:
215
                // @todo I think that if a cell is not quoted, newlines and delimiters should still be escaped by the escapeChar... no?
216 1
                $doQuote = false;
217 1
                break;
218 14
        }
219 14
        $quoteChar = ($doQuote) ? $flvr->quoteChar : "";
220 14
        return sprintf("%s%s%s",
221 14
            $quoteChar,
222 14
            $this->escapeString($str, $doQuote),
223
            $quoteChar
224 14
        );
225
    }
226
227 14
    protected function escapeString($str, $isQuoted = true)
228
    {
229 14
        $flvr = $this->getFlavor();
230 14
        $escapeQuote = "";
231 14
        if ($isQuoted) $escapeQuote = ($flvr->doubleQuote) ? $flvr->quoteChar : $flvr->escapeChar;
232
        // @todo Not sure what else, if anything, I'm supposed to be escaping here..
233 14
        return str_replace($flvr->quoteChar, $escapeQuote . $flvr->quoteChar, $str);
234
    }
235
}
236