Passed
Push — releases/v0.3 ( 02b4c1...da0bd3 )
by Luke
02:13
created

Reader   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 266
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 98.86%

Importance

Changes 0
Metric Value
dl 0
loc 266
ccs 87
cts 88
cp 0.9886
rs 10
c 0
b 0
f 0
wmc 30
lcom 1
cbo 4

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A toArray() 0 4 1
A setDialect() 0 6 1
A getDialect() 0 4 1
B getRow() 0 20 7
A getColumn() 0 6 1
A setInputStream() 0 5 1
A loadLine() 0 13 3
A parseLine() 0 19 4
A current() 0 4 1
A next() 0 6 1
A key() 0 4 1
A valid() 0 10 3
A rewind() 0 12 2
A count() 0 4 1
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 18
    public function __construct(Streamable $input, Dialect $dialect = null)
55
    {
56 18
        if (is_null($dialect)) {
57 11
            $dialect = new Dialect;
58 11
        }
59 18
        $this->setInputStream($input)
60 18
            ->setDialect($dialect);
61 18
    }
62
63
    /**
64
     * Get csv data as a two-dimensional array
65
     *
66
     * @return array
67
     */
68 6
    public function toArray()
69
    {
70 6
        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 18
    public function setDialect(Dialect $dialect)
81
    {
82 18
        $this->dialect = $dialect;
83
        // call rewind because new dialect needs to be used to re-read
84 18
        return $this->rewind();
85
    }
86
87
    /**
88
     * Get dialect
89
     *
90
     * @return Dialect
91
     */
92 18
    public function getDialect()
93
    {
94 18
        return $this->dialect;
95
    }
96
97
    /**
98
     * Get a single row
99
     *
100
     * Get the next row from the CSV data. If no more data available, returns false.
101
     *
102
     * @param int $offset An optional row offset (negative offsets not yet supported)
103
     *
104
     * @return array|false
105
     */
106 4
    public function getRow($offset = null)
107
    {
108 4
        if (!is_null($offset)) {
109 2
            if ($offset < 0) {
110 1
                return collect($this->toArray())->getValueAt($offset);
111
            }
112 1
            foreach ($this as $key => $row) {
113 1
                if ($key === $offset) {
114 1
                    return $row;
115
                }
116 1
            }
117
            return false;
118
        }
119 2
        if (!$this->valid() || $this->input->eof()) {
120 1
            return false;
121
        }
122 1
        $line = $this->current();
123 1
        $this->next();
124 1
        return $line;
125
    }
126
127
    /**
128
     * Get the column at given $index
129
     *
130
     * @param int|string $index The column index
131
     *
132
     * @return array
133
     */
134 1
    public function getColumn($index)
135
    {
136 1
        return collect($this->toArray())
137 1
            ->getColumn($index)
138 1
            ->toArray();
139
    }
140
141
    /**
142
     * Set input stream
143
     *
144
     * @param Streamable $stream The input stream to read from
145
     *
146
     * @return self
147
     */
148 18
    protected function setInputStream(Streamable $stream)
149
    {
150 18
        $this->input = $stream;
151 18
        return $this;
152
    }
153
154
    /**
155
     * Loads next line into memory
156
     *
157
     * Reads from input one character at a time until a newline is reached that isn't within quotes. Once a completed
158
     * line has been loaded, it is assigned to the `$this->current` property. Subsequent calls will continue to load
159
     * successive lines until the end of the input source is reached.
160
     *
161
     * @return self
162
     */
163 18
    protected function loadLine()
164
    {
165 18
        $d = $this->getDialect();
166 18
        $line = '';
167 18
        while ($str = $this->input->readLine($d->getLineTerminator())) {
168 18
            $line .= $str;
169 18
            if (count(s($line)->split($d->getQuoteChar())) % 2) {
170 18
                break;
171
            }
172 11
        }
173 18
        $this->current = $this->parseLine($line);
174 18
        return $this;
175
    }
176
177
    /**
178
     * Parse a line of CSV into individual fields
179
     *
180
     * Accepts a line (string) of CSV data that it then splits at the delimiter character. The method is smart, in that
181
     * it knows not to split at delimiters within quotes. Ultimately, fields are placed into a collection and returned.
182
     *
183
     * @param string $line A single line of CSV data to parse into individual fields
184
     *
185
     * @return Collection
186
     */
187 18
    protected function parseLine($line)
188
    {
189 18
        $d = $this->getDialect();
190 18
        $fields = collect(s($line)
191 18
            ->trimRight($d->getLineTerminator())
192 18
            ->split(" *{$d->getDelimiter()} *(?=([^\"]*\"[^\"]*\")*[^\"]*$)"));
193 18
        if (!is_null($this->header)) {
194
            // @todo there may be cases where this gives a false positive...
195 13
            if (count($fields) == count($this->header)) {
196 13
                $fields = $fields->rekey($this->header);
197 13
            }
198 13
        }
199 18
        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...
200 18
            if ($d->isDoubleQuote()) {
201 18
                $field = $field->replace('""', '"');
202 18
            }
203 18
            return (string) $field->trim($d->getQuoteChar());
204 18
        });
205
    }
206
207
    /** == BEGIN: SPL implementation methods == */
208
209
    /**
210
     * Get current row
211
     *
212
     * @return array
213
     */
214 17
    public function current()
215
    {
216 17
        return $this->current->toArray();
217
    }
218
219
    /**
220
     * Move pointer to beginning of the next line internally and then load the line
221
     *
222
     * @return self
223
     */
224 10
    public function next()
225
    {
226 10
        $this->loadLine();
227 10
        $this->lineNo++;
228 10
        return $this;
229
    }
230
231
    /**
232
     * Get current line number
233
     *
234
     * @return int
235
     */
236 10
    public function key()
237
    {
238 10
        return $this->lineNo;
239
    }
240
241
    /**
242
     * Have we reached the end of the CSV data?
243
     *
244
     * @return bool
245
     */
246 9
    public function valid()
247
    {
248 9
        if ($this->valid === false) {
249 6
            return false;
250
        }
251 9
        if ($this->input->tell() >= $this->input->getSize()) {
252 7
            $this->valid = false;
253 7
        }
254 9
        return true;
255
    }
256
257
    /**
258
     * Rewind to the beginning
259
     *
260
     * Rewinds the internal pointer to the beginning of the CSV data, load first line, and reset line number to 1. Also
261
     * loads the header (if one exists) and uses its values as indexes within rows.
262
     *
263
     * @return self
264
     */
265 18
    public function rewind()
266
    {
267 18
        $this->valid = null;
268 18
        $this->lineNo = 0;
269 18
        $this->input->rewind();
270 18
        if ($this->getDialect()->hasHeader()) {
271 13
            $this->loadLine();
272 13
            $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...
273 13
        }
274 18
        $this->loadLine();
275 18
        return $this;
276
    }
277
278
    /**
279
     * Get number of lines in the CSV data (not including header)
280
     *
281
     * @return int
282
     */
283 1
    public function count()
284
    {
285 1
        return count($this->toArray());
286
    }
287
288
    /** == END: SPL implementation methods == */
289
}