Completed
Pull Request — master (#183)
by Luke
04:23 queued 02:18
created

Reader::loadLine()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 0
dl 0
loc 13
ccs 10
cts 10
cp 1
crap 3
rs 9.8333
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
    /**
42
     * Reader constructor.
43
     *
44
     * Although this is the constructor, I don't envision it being used much in userland. I think much more common
45
     * methods of creating readers will be available within CSVelte base class such as CSVelte::fromPath(),
46
     * CSVelte::fromString(), CSVelte::fromSplFileObject, CSVelte::toSplFileObject, CSVelte::toPath(), etc.
47
     *
48
     * @param Streamable $input The source being read from
49
     * @param Dialect $dialect The dialect being read
50
     */
51 13
    public function __construct(Streamable $input, Dialect $dialect = null)
52
    {
53 13
        if (is_null($dialect)) {
54 7
            $dialect = new Dialect;
55 7
        }
56 13
        $this->setInputStream($input)
57 13
            ->setDialect($dialect);
58 13
    }
59
60
    /**
61
     * Get csv data as a two-dimensional array
62
     *
63
     * @return array
64
     */
65 1
    public function toArray()
66
    {
67 1
        return iterator_to_array($this);
68
    }
69
70
    /**
71
     * Set the CSV dialect
72
     *
73
     * @param Dialect $dialect The *dialect* of CSV to read
74
     *
75
     * @return self
76
     */
77 13
    public function setDialect(Dialect $dialect)
78
    {
79 13
        $this->dialect = $dialect;
80
        // call rewind because new dialect needs to be used to re-read
81 13
        return $this->rewind();
82
    }
83
84
    /**
85
     * Get dialect
86
     *
87
     * @return Dialect
88
     */
89 13
    public function getDialect()
90
    {
91 13
        return $this->dialect;
92
    }
93
94
    /**
95
     * Fetch a single row
96
     *
97
     * Fetch the next row from the CSV data. If no more data available, returns false.
98
     *
99
     * @return array|false
100
     */
101 2
    public function fetchRow()
102
    {
103 2
        if (!$this->valid()) {
104 1
            return false;
105
        }
106 1
        $line = $this->current();
107 1
        $this->next();
108 1
        return $line;
109
    }
110
111
    /**
112
     * Set input stream
113
     *
114
     * @param Streamable $stream The input stream to read from
115
     *
116
     * @return self
117
     */
118 13
    protected function setInputStream(Streamable $stream)
119
    {
120 13
        $this->input = $stream;
121 13
        return $this;
122
    }
123
124
    /**
125
     * Loads next line into memory
126
     *
127
     * Reads from input one character at a time until a newline is reached that isn't within quotes. Once a completed
128
     * line has been loaded, it is assigned to the `$this->current` property. Subsequent calls will continue to load
129
     * successive lines until the end of the input source is reached.
130
     *
131
     * @return self
132
     */
133 13
    protected function loadLine()
134
    {
135 13
        $d = $this->getDialect();
136 13
        $line = '';
137 13
        while ($str = $this->input->readLine($d->getLineTerminator())) {
138 13
            $line .= $str;
139 13
            if (count(s($line)->split($d->getQuoteChar())) % 2) {
140 13
                break;
141
            }
142 8
        }
143 13
        $this->current = $this->parseLine($line);
144 13
        return $this;
145
    }
146
147
    /**
148
     * Parse a line of CSV into individual fields
149
     *
150
     * Accepts a line (string) of CSV data that it then splits at the delimiter character. The method is smart, in that
151
     * it knows not to split at delimiters within quotes. Ultimately, fields are placed into a collection and returned.
152
     *
153
     * @param string $line A single line of CSV data to parse into individual fields
154
     *
155
     * @return Collection
156
     */
157 13
    protected function parseLine($line)
158
    {
159 13
        $d = $this->getDialect();
160 13
        $fields = collect(s($line)
161 13
            ->trimRight($d->getLineTerminator())
162 13
            ->split($d->getDelimiter() . "(?=([^\"]*\"[^\"]*\")*[^\"]*$)"));
163 13
        if (!is_null($this->header)) {
164 9
            $fields = $fields->rekey($this->header);
165 9
        }
166 13
        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...
167 13
            if ($d->isDoubleQuote()) {
168 13
                $field = $field->replace('""', '"');
169 13
            }
170 13
            return (string) $field->trim($d->getQuoteChar());
171 13
        });
172
    }
173
174
    /** == BEGIN: SPL implementation methods == */
175
176
    /**
177
     * Get current row
178
     *
179
     * @return array
180
     */
181 12
    public function current()
182
    {
183 12
        return $this->current->toArray();
184
    }
185
186
    /**
187
     * Move pointer to beginning of the next line internally and then load the line
188
     *
189
     * @return self
190
     */
191 4
    public function next()
192
    {
193 4
        $this->loadLine();
194 4
        $this->lineNo++;
195 4
        return $this;
196
    }
197
198
    /**
199
     * Get current line number
200
     *
201
     * @return int
202
     */
203 4
    public function key()
204
    {
205 4
        return $this->lineNo;
206
    }
207
208
    /**
209
     * Have we reached the end of the CSV data?
210
     *
211
     * @return bool
212
     */
213 4
    public function valid()
214
    {
215 4
        return !$this->input->eof();
216
    }
217
218
    /**
219
     * Rewind to the beginning
220
     *
221
     * Rewinds the internal pointer to the beginning of the CSV data, load first line, and reset line number to 1. Also
222
     * loads the header (if one exists) and uses its values as indexes within rows.
223
     *
224
     * @return self
225
     */
226 13
    public function rewind()
227
    {
228 13
        $this->lineNo = 1;
229 13
        $this->input->rewind();
230 13
        if ($this->getDialect()->hasHeader()) {
231 9
            $this->loadLine();
232 9
            $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...
233 9
        }
234 13
        $this->loadLine();
235 13
        return $this;
236
    }
237
238
    /**
239
     * Get number of lines in the CSV data (not including header)
240
     *
241
     * @return int
242
     */
243 1
    public function count()
244
    {
245 1
        return count($this->toArray());
246
    }
247
248
    /** == END: SPL implementation methods == */
249
}