Writer   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.96%

Importance

Changes 0
Metric Value
dl 0
loc 256
ccs 95
cts 96
cp 0.9896
rs 10
c 0
b 0
f 0
wmc 30
lcom 1
cbo 5

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A getFlavor() 0 4 1
A setHeaderRow() 0 12 3
A writeRow() 0 20 5
B writeRows() 0 20 6
A writeHeaderRow() 0 8 1
A prepareRow() 0 10 2
A prepareData() 0 7 1
B quoteString() 0 36 6
A escapeString() 0 10 3
1
<?php
2
3
/*
4
 * CSVelte: Slender, elegant CSV for PHP
5
 * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV
6
 * standardization efforts, CSVelte was written in an effort to take all the
7
 * suck out of working with CSV.
8
 *
9
 * @version   {version}
10
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
11
 * @author    Luke Visinoni <[email protected]>
12
 * @license   https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT)
13
 */
14
namespace CSVelte;
15
16
use ArrayIterator;
17
18
use CSVelte\Contract\Streamable;
19
use CSVelte\Exception\WriterException;
20
use CSVelte\Table\AbstractRow;
21
use CSVelte\Table\HeaderRow;
22
use CSVelte\Table\Row;
23
24
use InvalidArgumentException;
25
use Iterator;
26
27
/**
28
 * CSVelte Writer Base Class
29
 * A PHP CSV utility library (formerly PHP CSV Utilities).
30
 *
31
 * @package   CSVelte
32
 *
33
 * @copyright (c) 2016 Luke Visinoni <[email protected]>
34
 * @author    Luke Visinoni <[email protected]>
35
 *
36
 * @todo Buffer write operations so that you can call things like setHeaderRow()
37
 *     and change flavor and all that jivey divey goodness at any time.
38
 */
39
class Writer
40
{
41
    /**
42
     * The flavor (format) of CSV to write.
43
     *
44
     * @var Flavor
45
     */
46
    protected $flavor;
47
48
    /**
49
     * The output stream to write to.
50
     *
51
     * @var Contract\Streamable
52
     */
53
    protected $output;
54
55
    /**
56
     * The header row.
57
     *
58
     * @var \Iterator
59
     */
60
    protected $headers;
61
62
    /**
63
     * Number of lines written so far (not including header).
64
     *
65
     * @var int
66
     */
67
    protected $written = 0;
68
69
    /**
70
     * Class Constructor.
71
     *
72
     * @param Contract\Streamable $output An output source to write to
73
     * @param Flavor|array        $flavor A flavor or set of formatting params
74
     */
75 17
    public function __construct(Streamable $output, $flavor = null)
76
    {
77 17
        if (!($flavor instanceof Flavor)) {
78 14
            $flavor = new Flavor($flavor);
79 14
        }
80 17
        $this->flavor                             = $flavor;
81 17
        $this->output                             = $output;
82 17
    }
83
84
    /**
85
     * Get the CSV flavor (or dialect) for this writer.
86
     *
87
     * @param void
88
     *
89
     * @return Flavor
90
     */
91 16
    public function getFlavor()
92
    {
93 16
        return $this->flavor;
94
    }
95
96
    /**
97
     * Sets the header row
98
     * If any data has been written to the output, it is too late to write the
99
     * header row and an exception will be thrown. Later implementations will
100
     * likely buffer the output so that this may be called after writeRows().
101
     *
102
     * @param \Iterator|array A list of header values
103
     * @param mixed $headers
104
     *
105
     * @throws Exception\WriterException
106
     *
107
     * @return $this
108
     */
109 2
    public function setHeaderRow($headers)
110
    {
111 2
        if ($this->written) {
112 1
            throw new WriterException('Cannot set header row once data has already been written. ');
113
        }
114 1
        if (is_array($headers)) {
115 1
            $headers = new ArrayIterator($headers);
116 1
        }
117 1
        $this->headers                   = $headers;
118
119 1
        return $this;
120
    }
121
122
    /**
123
     * Write a single row.
124
     *
125
     * @param \Iterator|array $row The row to write to source
126
     *
127
     * @return int The number or bytes written
128
     */
129 14
    public function writeRow($row)
130
    {
131 14
        $eol   = $this->getFlavor()->lineTerminator;
132 14
        $delim = $this->getFlavor()->delimiter;
133 14
        if (!$this->written && $this->headers) {
134 1
            $headerRow = new HeaderRow((array) $this->headers);
135 1
            $this->writeHeaderRow($headerRow);
136 1
        }
137 14
        if (is_array($row)) {
138 10
            $row = new ArrayIterator($row);
139 10
        }
140 14
        $row                     = $this->prepareRow($row);
141 14
        if ($count = $this->output->writeLine($row->join($delim), $eol)) {
142 14
            $this->written++;
143
144 14
            return $count;
145
        }
146
147
        return 0;
148
    }
149
150
    /**
151
     * Write multiple rows.
152
     *
153
     * @param \Iterator|array $rows List of \Iterable|array
154
     *
155
     * @return int number of lines written
156
     */
157 11
    public function writeRows($rows)
158
    {
159 11
        if (is_array($rows)) {
160 8
            $rows = new ArrayIterator($rows);
161 8
        }
162 11
        if (!($rows instanceof Iterator)) {
163 1
            throw new InvalidArgumentException('First argument for ' . __METHOD__ . ' must be iterable');
164
        }
165 10
        $written = 0;
166 10
        if ($rows instanceof Reader) {
167 2
            $this->writeHeaderRow($rows->header());
168 2
        }
169 10
        foreach ($rows as $row) {
170 10
            if ($this->writeRow($row)) {
171 10
                $written++;
172 10
            }
173 10
        }
174
175 10
        return $written;
176
    }
177
178
    /**
179
     * Write the header row.
180
     *
181
     * @param HeaderRow $row
182
     *
183
     * @return int|false
184
     */
185 3
    protected function writeHeaderRow(HeaderRow $row)
186
    {
187 3
        $eol   = $this->getFlavor()->lineTerminator;
188 3
        $delim = $this->getFlavor()->delimiter;
189 3
        $row   = $this->prepareRow($row);
190
191 3
        return $this->output->writeLine($row->join($delim), $eol);
192
    }
193
194
    /**
195
     * Prepare a row of data to be written
196
     * This means taking an array of data, and converting it to a Row object.
197
     *
198
     * @param \Iterator $row of data items
199
     *
200
     * @return AbstractRow
201
     */
202 14
    protected function prepareRow(Iterator $row)
203
    {
204 14
        $items = [];
205 14
        foreach ($row as $data) {
206 14
            $items []= $this->prepareData($data);
207 14
        }
208 14
        $row = new Row($items);
209
210 14
        return $row;
211
    }
212
213
    /**
214
     * Prepare a cell of data to be written (convert to Data object).
215
     *
216
     * @param string $data A string containing cell data
217
     *
218
     * @return string quoted string data
219
     */
220 14
    protected function prepareData($data)
221
    {
222
        // @todo This can't be properly implemented until I get Data and DataType right...
223
        // it should be returning a Data object but until I get that working properly
224
        // it's just going to have to return a string
225 14
        return $this->quoteString($data);
226
    }
227
228
    /**
229
     * Enclose a string in quotes.
230
     *
231
     * Accepts a string and returns it with quotes around it.
232
     *
233
     * @param string $str The string to wrap in quotes
234
     *
235
     * @return string
236
     */
237 14
    protected function quoteString($str)
238
    {
239 14
        $flvr = $this->getFlavor();
240
        // Normally I would make this a method on the class, but I don't intend
241
        // to use it for very long, in fact, once I finish writing the Data class
242
        // it is gonezo!
243 14
        $hasSpecialChars = function ($s) use ($flvr) {
244 11
            $specialChars = preg_quote($flvr->lineTerminator . $flvr->quoteChar . $flvr->delimiter);
245 11
            $pattern      = "/[{$specialChars}]/m";
246
247 11
            return preg_match($pattern, $s);
248 14
        };
249 14
        switch ($flvr->quoteStyle) {
250 14
            case Flavor::QUOTE_ALL:
251 1
                $doQuote = true;
252 1
                break;
253 13
            case Flavor::QUOTE_NONNUMERIC:
254 1
                $doQuote = !is_numeric($str);
255 1
                break;
256 12
            case Flavor::QUOTE_MINIMAL:
257 11
                $doQuote = $hasSpecialChars($str);
258 11
                break;
259 1
            case Flavor::QUOTE_NONE:
260 1
            default:
261
                // @todo I think that if a cell is not quoted, newlines and delimiters should still be escaped by the escapeChar... no?
262 1
                $doQuote = false;
263 1
                break;
264 14
        }
265 14
        $quoteChar = ($doQuote) ? $flvr->quoteChar : '';
266
267 14
        return sprintf('%s%s%s',
268 14
            $quoteChar,
269 14
            $this->escapeString($str, $doQuote),
270
            $quoteChar
271 14
        );
272
    }
273
274
    /**
275
     * Escape a string.
276
     *
277
     * Return a string with all special characters escaped.
278
     *
279
     * @param string $str      The string to escape
280
     * @param bool   $isQuoted True if string is quoted
281
     *
282
     * @return string
283
     */
284 14
    protected function escapeString($str, $isQuoted = true)
285
    {
286 14
        $flvr                       = $this->getFlavor();
287 14
        $escapeQuote                = '';
288 14
        if ($isQuoted) {
289 4
            $escapeQuote = ($flvr->doubleQuote) ? $flvr->quoteChar : $flvr->escapeChar;
290 4
        }
291
        // @todo Not sure what else, if anything, I'm supposed to be escaping here..
292 14
        return str_replace($flvr->quoteChar, $escapeQuote . $flvr->quoteChar, $str);
293
    }
294
}
295