Completed
Branch refactor/142 (8a1d2c)
by Luke
02:46
created

Writer::getFlavor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
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.1
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 CSVelte\Contract\Streamable;
16
17
use \Iterator;
18
use \ArrayIterator;
19
use CSVelte\Table\Data;
20
use CSVelte\Table\HeaderRow;
21
use CSVelte\Table\Row;
22
use CSVelte\Flavor;
23
use CSVelte\Reader;
24
25
use \InvalidArgumentException;
26
use CSVelte\Exception\WriterException;
27
28
/**
29
 * CSVelte Writer Base Class
30
 * A PHP CSV utility library (formerly PHP CSV Utilities).
31
 *
32
 * @package   CSVelte
33
 * @copyright (c) 2016, Luke Visinoni <[email protected]>
34
 * @author    Luke Visinoni <[email protected]>
35
 * @todo Buffer write operations so that you can call things like setHeaderRow()
36
 *     and change flavor and all that jivey divey goodness at any time.
37
 */
38
class Writer
39
{
40
    /**
41
     * @var \CSVelte\Flavor
42
     */
43
    protected $flavor;
44
45
    /**
46
     * @var \CSVelte\Contract\Writable
47
     */
48
    protected $output;
49
50
    /**
51
     * @var \Iterator
52
     */
53
    protected $headers;
54
55
    /**
56
     * @var int lines of data written so far (not including header)
57
     */
58
    protected $written = 0;
59
60
    /**
61
     * Class Constructor
62
     *
63
     * @param \CSVelte\Contract\Writable $output An output source to write to
64
     * @param \CSVelte\Flavor|array $flavor A flavor or set of formatting params
65
     */
66 17
    public function __construct(Streamable $output, $flavor = null)
67
    {
68 17
        if (!($flavor instanceof Flavor)) $flavor = new Flavor($flavor);
69 17
        $this->flavor = $flavor;
70 17
        $this->output = $output;
0 ignored issues
show
Documentation Bug introduced by
It seems like $output of type object<CSVelte\Contract\Streamable> is incompatible with the declared type object<CSVelte\Contract\Writable> of property $output.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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