Reader   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 217
Duplicated Lines 0 %

Test Coverage

Coverage 77.38%

Importance

Changes 18
Bugs 0 Features 0
Metric Value
wmc 38
eloc 68
c 18
b 0
f 0
dl 0
loc 217
ccs 65
cts 84
cp 0.7738
rs 9.36

23 Methods

Rating   Name   Duplication   Size   Complexity  
A valid() 0 5 1
A getHeaders() 0 7 3
A toArray() 0 8 2
A getHeader() 0 3 1
A getOffset() 0 3 1
A current() 0 5 1
A setHeaderNormalizer() 0 5 1
A getCurrentRow() 0 17 6
A getNormalizer() 0 13 4
A rewind() 0 20 4
A setOffset() 0 5 1
A setDefaultNormalizer() 0 5 1
A next() 0 3 1
A getHeaderNormalizer() 0 3 1
A setStripBom() 0 5 1
A getHeaderOffset() 0 3 1
A getDefaultNormalizer() 0 3 1
A addNormalizer() 0 5 1
A read() 0 3 1
A setHeaderOffset() 0 5 1
A getOpenMode() 0 3 1
A key() 0 3 1
A addNormalizers() 0 7 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Palmtree\Csv;
6
7
use Palmtree\Csv\Normalizer\NormalizerInterface;
8
use Palmtree\Csv\Normalizer\NullNormalizer;
9
use Palmtree\Csv\Row\Row;
10
use Palmtree\Csv\Util\StringUtil;
11
12
/**
13
 * Reads a CSV file by loading each line into memory one at a time.
14
 */
15
class Reader extends AbstractCsvDocument implements \Iterator
16
{
17
    /** @var class-string<NormalizerInterface> */
18
    private string $defaultNormalizer = NullNormalizer::class;
19
    private ?NormalizerInterface $headerNormalizer = null;
20
    /** @var array<NormalizerInterface> */
21
    private array $normalizers = [];
22
    private ?Row $headers = null;
23
    private ?Row $row = null;
24
    private ?string $stripBom = StringUtil::BOM_UTF8;
25
    private int $offset = 0;
26
    private int $headerOffset = 0;
27
28
    public static function read(string $filePath, bool $hasHeaders = true): self
29
    {
30
        return new self($filePath, $hasHeaders);
31
    }
32
33 3
    public function getHeaders(): ?Row
34
    {
35 3
        if ($this->hasHeaders && $this->headers === null) {
36 3
            $this->rewind();
37
        }
38
39 3
        return $this->headers;
40
    }
41
42
    /**
43
     * @param string|int $key
44
     *
45
     * @return string|int
46
     */
47 12
    public function getHeader($key)
48
    {
49 12
        return $this->headers[$key] ?? $key;
50
    }
51
52
    public function setHeaderNormalizer(NormalizerInterface $headerNormalizer): self
53
    {
54
        $this->headerNormalizer = $headerNormalizer;
55
56
        return $this;
57
    }
58
59 10
    public function getHeaderNormalizer(): NormalizerInterface
60
    {
61 10
        return $this->headerNormalizer ??= new NullNormalizer();
62
    }
63
64
    public function addNormalizer(string $key, NormalizerInterface $normalizer): self
65
    {
66
        $this->normalizers[$key] = $normalizer;
67
68
        return $this;
69
    }
70
71
    /**
72
     * @param iterable<NormalizerInterface> $normalizers
73
     */
74
    public function addNormalizers(iterable $normalizers): self
75
    {
76
        foreach ($normalizers as $key => $normalizer) {
77
            $this->addNormalizer($key, $normalizer);
78
        }
79
80
        return $this;
81
    }
82
83
    /**
84
     * @param string|int $key
85
     */
86 12
    public function getNormalizer($key): NormalizerInterface
87
    {
88 12
        if ($this->hasHeaders && \is_int($key)) {
89 10
            $this->normalizers[$key] = $this->getHeaderNormalizer();
90
        }
91
92 12
        if (!isset($this->normalizers[$key])) {
93 6
            $class = $this->getDefaultNormalizer();
94
95 6
            $this->normalizers[$key] = new $class();
96
        }
97
98 12
        return $this->normalizers[$key];
99
    }
100
101
    /**
102
     * @param class-string<NormalizerInterface> $defaultNormalizer
103
     */
104
    public function setDefaultNormalizer(string $defaultNormalizer): self
105
    {
106
        $this->defaultNormalizer = $defaultNormalizer;
107
108
        return $this;
109
    }
110
111
    /**
112
     * @return class-string<NormalizerInterface>
113
     */
114 6
    public function getDefaultNormalizer(): string
115
    {
116 6
        return $this->defaultNormalizer;
117
    }
118
119 1
    public function setStripBom(?string $stripBom): self
120
    {
121 1
        $this->stripBom = $stripBom;
122
123 1
        return $this;
124
    }
125
126 1
    public function setOffset(int $offset): self
127
    {
128 1
        $this->offset = $offset;
129
130 1
        return $this;
131
    }
132
133
    public function getOffset(): int
134
    {
135
        return $this->offset;
136
    }
137
138 1
    public function setHeaderOffset(int $headerOffset): self
139
    {
140 1
        $this->headerOffset = $headerOffset;
141
142 1
        return $this;
143
    }
144
145
    public function getHeaderOffset(): int
146
    {
147
        return $this->headerOffset;
148
    }
149
150 6
    public function current(): Row
151
    {
152 6
        \assert($this->row instanceof Row);
153
154 6
        return $this->row;
155
    }
156
157 6
    public function next(): void
158
    {
159 6
        $this->getDocument()->next();
160 6
    }
161
162 8
    public function key(): int
163
    {
164 8
        return $this->getDocument()->key();
165
    }
166
167 6
    public function valid(): bool
168
    {
169 6
        $this->row = $this->getCurrentRow();
170
171 6
        return $this->row instanceof Row;
172
    }
173
174 8
    public function rewind(): void
175
    {
176 8
        $this->getDocument()->rewind();
177
178 8
        $dataOffset = $this->offset + $this->headerOffset;
179 8
        if ($this->hasHeaders) {
180 6
            if ($this->headerOffset) {
181 1
                $this->getDocument()->seek($this->headerOffset);
182
            }
183
184
            // Set headers to null first so the header row is a zero-based array and can be used
185
            // to set the array keys of all other rows.
186 6
            $this->headers = null;
187 6
            $this->headers = $this->getCurrentRow();
188
189 6
            ++$dataOffset;
190
        }
191
192 8
        if ($dataOffset > 0) {
193 6
            $this->getDocument()->seek($dataOffset);
194
        }
195 8
    }
196
197 4
    public function toArray(): array
198
    {
199 4
        $result = [];
200 4
        foreach ($this as $row) {
201 4
            $result[] = $row->toArray();
202
        }
203
204 4
        return $result;
205
    }
206
207 7
    protected function getOpenMode(): string
208
    {
209 7
        return 'r';
210
    }
211
212
    /**
213
     * Reads the next line in the CSV file and returns a Row object from it.
214
     */
215 8
    private function getCurrentRow(): ?Row
216
    {
217 8
        $cells = $this->getDocument()->current();
218
219 8
        if (!\is_array($cells) || $cells == [null]) {
220 6
            return null;
221
        }
222
223 8
        if ($this->key() === 0 && $this->stripBom) {
224 6
            $stripped = StringUtil::stripBom($cells[0], $this->stripBom);
225
226 6
            if ($stripped !== $cells[0]) {
227 1
                $cells[0] = trim($stripped, $this->enclosure);
228
            }
229
        }
230
231 8
        return new Row($cells, $this);
232
    }
233
}
234