Completed
Push — master ( 748f90...5c1aea )
by Luke
08:07
created

Writer::writeRows()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 15
ccs 12
cts 12
cp 1
rs 8.8571
cc 6
eloc 10
nc 14
nop 1
crap 6
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
     */
65 17
    public function __construct(Writable $output, $flavor = null)
66
    {
67 17
        if (!($flavor instanceof Flavor)) $flavor = new Flavor($flavor);
68 17
        $this->flavor = $flavor;
69 17
        $this->output = $output;
70 17
    }
71
72
    /**
73
     * Get the CSV flavor (or dialect) for this writer
74
     *
75
     * @param void
76
     * @return \CSVelte\Flavor
77
     * @access public
78
     */
79 16
    public function getFlavor()
80
    {
81 16
        return $this->flavor;
82
    }
83
84
    /**
85
     * Sets the header row
86
     * If any data has been written to the output, it is too late to write the
87
     * header row and an exception will be thrown. Later implementations will
88
     * likely buffer the output so that this may be called after writeRows()
89
     *
90
     * @param \Iterator|array A list of header values
91
     * @return boolean
92
     * @throws \CSVelte\Exception\WriterException
93
     */
94 2
    public function setHeaderRow($headers)
95
    {
96 2
        if ($this->written) {
97 1
            throw new WriterException("Cannot set header row once data has already been written. ");
98
        }
99 1
        if (is_array($headers)) $headers = new ArrayIterator($headers);
100 1
        $this->headers = $headers;
101 1
    }
102
103
    /**
104
     * Write a single row
105
     *
106
     * @param \Iterator|array $row The row to write to source
107
     * @return int The number or bytes written
108
     */
109 14
    public function writeRow($row)
110
    {
111 14
        $eol = $this->getFlavor()->lineTerminator;
112 14
        $delim = $this->getFlavor()->delimiter;
113 14
        if (!$this->written && $this->headers) {
114 1
            $headerRow = new HeaderRow((array) $this->headers);
115 1
            $this->writeHeaderRow($headerRow);
116 1
        }
117 14
        if (is_array($row)) $row = new ArrayIterator($row);
118 14
        $row = $this->prepareRow($row);
119 14
        if ($count = $this->output->writeLine($row->join($delim), $eol)) {
120 14
            $this->written++;
121 14
            return $count;
122
        }
123
    }
124
125 3
    protected function writeHeaderRow(HeaderRow $row)
126
    {
127 3
        $eol = $this->getFlavor()->lineTerminator;
128 3
        $delim = $this->getFlavor()->delimiter;
129 3
        $row = $this->prepareRow($row);
130 3
        return $this->output->writeLine($row->join($delim), $eol);
131
    }
132
133
    /**
134
     * Write multiple rows
135
     *
136
     * @param \Iterable|array $rows List of \Iterable|array
137
     * @return int number of lines written
138
     * @access public
139
     */
140 11
    public function writeRows($rows)
141
    {
142 11
        if (is_array($rows)) $rows = new ArrayIterator($rows);
143 11
        if (!($rows instanceof Iterator)) {
144 1
            throw new InvalidArgumentException('First argument for ' . __METHOD__ . ' must be iterable');
145
        }
146 10
        $written = 0;
147 10
        if ($rows instanceof Reader) {
148 2
            $this->writeHeaderRow($rows->header());
149 2
        }
150 10
        foreach ($rows as $row) {
151 10
            if ($this->writeRow($row)) $written++;
152 10
        }
153 10
        return $written;
154
    }
155
156
    /**
157
     * Prepare a row of data to be written
158
     * This means taking an array of data, and converting it to a Row object
159
     *
160
     * @param \Iterator|array of data items
161
     * @return CSVelte\Table\AbstractRow
162
     * @access protected
163
     */
164 14
    protected function prepareRow(Iterator $row)
165
    {
166 14
        $items = array();
167 14
        foreach ($row as $data) {
168 14
            $items []= $this->prepareData($data);
169 14
        }
170 14
        $row = new Row($items);
171 14
        return $row;
172
    }
173
174
    /**
175
     * Prepare a cell of data to be written (convert to Data object)
176
     *
177
     * @param string $data A string containing cell data
178
     * @return string quoted string data
179
     * @access protected
180
     */
181 14
    protected function prepareData($data)
182
    {
183
        // @todo This can't be properly implemented until I get Data and DataType right...
184
        // it should be returning a Data object but until I get that working properly
185
        // it's just going to have to return a string
186 14
        return $this->quoteString($data);
187
    }
188
189 14
    protected function quoteString($str)
190
    {
191 14
        $flvr = $this->getFlavor();
192
        // Normally I would make this a method on the class, but I don't intend
193
        // to use it for very long, in fact, once I finish writing the Data class
194
        // it is gonezo!
195 14
        $hasSpecialChars = function($s) use ($flvr) {
196 11
            $specialChars = preg_quote($flvr->lineTerminator . $flvr->quoteChar . $flvr->delimiter);
197 11
            $pattern = "/[{$specialChars}]/m";
198 11
            return preg_match($pattern, $s);
199 14
        };
200 14
        switch($flvr->quoteStyle) {
201 14
            case Flavor::QUOTE_ALL:
202 1
                $doQuote = true;
203 1
                break;
204 13
            case Flavor::QUOTE_NONNUMERIC:
205 1
                $doQuote = !is_numeric($str);
206 1
                break;
207 12
            case Flavor::QUOTE_MINIMAL:
208 11
                $doQuote = $hasSpecialChars($str);
209 11
                break;
210 1
            case Flavor::QUOTE_NONE:
211 1
            default:
212
                // @todo I think that if a cell is not quoted, newlines and delimiters should still be escaped by the escapeChar... no?
213 1
                $doQuote = false;
214 1
                break;
215 14
        }
216 14
        $quoteChar = ($doQuote) ? $flvr->quoteChar : "";
217 14
        return sprintf("%s%s%s",
218 14
            $quoteChar,
219 14
            $this->escapeString($str, $doQuote),
220
            $quoteChar
221 14
        );
222
    }
223
224 14
    protected function escapeString($str, $isQuoted = true)
225
    {
226 14
        $flvr = $this->getFlavor();
227 14
        $escapeQuote = "";
228 14
        if ($isQuoted) $escapeQuote = ($flvr->doubleQuote) ? $flvr->quoteChar : $flvr->escapeChar;
229
        // @todo Not sure what else, if anything, I'm supposed to be escaping here..
230 14
        return str_replace($flvr->quoteChar, $escapeQuote . $flvr->quoteChar, $str);
231
    }
232
}
233