Passed
Pull Request — master (#183)
by Luke
03:12
created

Reader::readLine()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nc 18
nop 0
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
1
<?php
2
/**
3
 * CSVelte: Slender, elegant CSV for PHP
4
 *
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
 * @copyright Copyright (c) 2018 Luke Visinoni
10
 * @author    Luke Visinoni <[email protected]>
11
 * @license   See LICENSE file (MIT license)
12
 */
13
namespace CSVelte;
14
15
use Iterator;
16
use Countable;
17
use CSVelte\Contract\Streamable;
18
use Noz\Collection\Collection;
19
use Stringy\Stringy;
20
21
use function Noz\collect;
22
use function Stringy\create as s;
23
24
class Reader implements Iterator, Countable
25
{
26
    /** @var Streamable The input stream to read from */
27
    protected $input;
28
29
    /** @var Dialect The *dialect* of CSV to read */
30
    protected $dialect;
31
32
    /** @var Collection The line currently sitting in memory */
33
    protected $current;
34
35
    /** @var Collection The header row */
36
    protected $header;
37
38
    /** @var int The current line number */
39
    protected $lineNo;
40
41
    /** @var bool Determines whether we've reached the end of the data */
42
    protected $valid;
43
44
    /**
45
     * Reader constructor.
46
     *
47
     * Although this is the constructor, I don't envision it being used much in userland. I think much more common
48
     * methods of creating readers will be available within CSVelte base class such as CSVelte::fromPath(),
49
     * CSVelte::fromString(), CSVelte::fromSplFileObject, CSVelte::toSplFileObject, CSVelte::toPath(), etc.
50
     *
51
     * @param Streamable $input The source being read from
52
     * @param Dialect $dialect The dialect being read
53
     */
54 15
    public function __construct(Streamable $input, Dialect $dialect = null)
55
    {
56 15
        if (is_null($dialect)) {
57 8
            $dialect = new Dialect;
58 8
        }
59 15
        $this->setInputStream($input)
60 15
            ->setDialect($dialect);
61 15
    }
62
63
    /**
64
     * Get csv data as a two-dimensional array
65
     *
66
     * @return array
67
     */
68 4
    public function toArray()
69
    {
70 4
        return iterator_to_array($this);
71
    }
72
73
    /**
74
     * Set the CSV dialect
75
     *
76
     * @param Dialect $dialect The *dialect* of CSV to read
77
     *
78
     * @return self
79
     */
80 15
    public function setDialect(Dialect $dialect)
81
    {
82 15
        $this->dialect = $dialect;
83
        // call rewind because new dialect needs to be used to re-read
84 15
        return $this->rewind();
85
    }
86
87
    /**
88
     * Get dialect
89
     *
90
     * @return Dialect
91
     */
92 15
    public function getDialect()
93
    {
94 15
        return $this->dialect;
95
    }
96
97
    /**
98
     * Fetch a single row
99
     *
100
     * Fetch the next row from the CSV data. If no more data available, returns false.
101
     *
102
     * @return array|false
103
     */
104 2
    public function fetchRow()
105
    {
106 2
        if (!$this->valid() || $this->input->eof()) {
107 1
            return false;
108
        }
109 1
        $line = $this->current();
110 1
        $this->next();
111 1
        return $line;
112
    }
113
114
    /**
115
     * Set input stream
116
     *
117
     * @param Streamable $stream The input stream to read from
118
     *
119
     * @return self
120
     */
121 15
    protected function setInputStream(Streamable $stream)
122
    {
123 15
        $this->input = $stream;
124 15
        return $this;
125
    }
126
127
    /**
128
     * Loads next line into memory
129
     *
130
     * Reads from input one character at a time until a newline is reached that isn't within quotes. Once a completed
131
     * line has been loaded, it is assigned to the `$this->current` property. Subsequent calls will continue to load
132
     * successive lines until the end of the input source is reached.
133
     *
134
     * @return self
135
     */
136 15
    protected function loadLine()
137
    {
138 15
        $d = $this->getDialect();
139 15
        $line = '';
140 15
        while ($str = $this->input->readLine($d->getLineTerminator())) {
141 15
            $line .= $str;
142 15
            if (count(s($line)->split($d->getQuoteChar())) % 2) {
143 15
                break;
144
            }
145 8
        }
146 15
        $this->current = $this->parseLine($line);
147 15
        return $this;
148
    }
149
150
    /**
151
     * Parse a line of CSV into individual fields
152
     *
153
     * Accepts a line (string) of CSV data that it then splits at the delimiter character. The method is smart, in that
154
     * it knows not to split at delimiters within quotes. Ultimately, fields are placed into a collection and returned.
155
     *
156
     * @param string $line A single line of CSV data to parse into individual fields
157
     *
158
     * @return Collection
159
     */
160 15
    protected function parseLine($line)
161
    {
162 15
        $d = $this->getDialect();
163 15
        $fields = collect(s($line)
164 15
            ->trimRight($d->getLineTerminator())
165 15
            ->split(" *{$d->getDelimiter()} *(?=([^\"]*\"[^\"]*\")*[^\"]*$)"));
166 15
        if (!is_null($this->header)) {
167
            // @todo there may be cases where this gives a false positive...
168 10
            if (count($fields) == count($this->header)) {
169 10
                $fields = $fields->rekey($this->header);
170 10
            }
171 10
        }
172 15
        return $fields->map(function(Stringy $field, $pos) use ($d) {
0 ignored issues
show
Unused Code introduced by
The parameter $pos is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
173 15
            if ($d->isDoubleQuote()) {
174 15
                $field = $field->replace('""', '"');
175 15
            }
176 15
            return (string) $field->trim($d->getQuoteChar());
177 15
        });
178
    }
179
180
    /** == BEGIN: SPL implementation methods == */
181
182
    /**
183
     * Get current row
184
     *
185
     * @return array
186
     */
187 14
    public function current()
188
    {
189 14
        return $this->current->toArray();
190
    }
191
192
    /**
193
     * Move pointer to beginning of the next line internally and then load the line
194
     *
195
     * @return self
196
     */
197 7
    public function next()
198
    {
199 7
        $this->loadLine();
200 7
        $this->lineNo++;
201 7
        return $this;
202
    }
203
204
    /**
205
     * Get current line number
206
     *
207
     * @return int
208
     */
209 7
    public function key()
210
    {
211 7
        return $this->lineNo;
212
    }
213
214
    /**
215
     * Have we reached the end of the CSV data?
216
     *
217
     * @return bool
218
     */
219 6
    public function valid()
220
    {
221 6
        if ($this->valid === false) {
222 4
            return false;
223
        }
224 6
        if ($this->input->tell() >= $this->input->getSize()) {
225 5
            $this->valid = false;
226 5
        }
227 6
        return true;
228
    }
229
230
    /**
231
     * Rewind to the beginning
232
     *
233
     * Rewinds the internal pointer to the beginning of the CSV data, load first line, and reset line number to 1. Also
234
     * loads the header (if one exists) and uses its values as indexes within rows.
235
     *
236
     * @return self
237
     */
238 15
    public function rewind()
239
    {
240 15
        $this->valid = null;
241 15
        $this->lineNo = 1;
242 15
        $this->input->rewind();
243 15
        if ($this->getDialect()->hasHeader()) {
244 10
            $this->loadLine();
245 10
            $this->header = $this->current();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->current() of type array is incompatible with the declared type object<Noz\Collection\Collection> of property $header.

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...
246 10
        }
247 15
        $this->loadLine();
248 15
        return $this;
249
    }
250
251
    /**
252
     * Get number of lines in the CSV data (not including header)
253
     *
254
     * @return int
255
     */
256 1
    public function count()
257
    {
258 1
        return count($this->toArray());
259
    }
260
261
    /** == END: SPL implementation methods == */
262
}