Completed
Push — master ( 77ec27...fb3ff4 )
by Iurii
01:19
created

Csv::key()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
/**
4
 * @package Import
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2015, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\import\helpers;
11
12
use Iterator;
13
use UnexpectedValueException;
14
15
/**
16
 * Provides methods to read CSV data
17
 */
18
class Csv implements Iterator
19
{
20
21
    /**
22
     * File handler
23
     * @var resource
24
     */
25
    protected $handle;
26
27
    /**
28
     * Current CSV line
29
     * @var string
30
     */
31
    protected $line;
32
33
    /**
34
     * @var int
35
     */
36
    protected $key;
37
38
    /**
39
     * Current offset in bytes
40
     * @var integer
41
     */
42
    protected $position;
43
44
    /**
45
     * Total CSV file size in bytes
46
     * @var integer
47
     */
48
    protected $total;
49
50
    /**
51
     * Path to CSV file
52
     * @var string
53
     */
54
    protected $file;
55
56
    /**
57
     * Max number of rows to parse
58
     * @var integer
59
     */
60
    protected $limit;
61
62
    /**
63
     * Final offset in bytes
64
     * @var integer
65
     */
66
    protected $last = 0;
67
68
    /**
69
     * Starting offset in bytes
70
     * @var integer
71
     */
72
    protected $offset = 0;
73
74
    /**
75
     * Array of header names
76
     * @var array
77
     */
78
    protected $header = array();
79
80
    /**
81
     * Skip or not first line
82
     * @var boolean
83
     */
84
    protected $skip_header = false;
85
86
    /**
87
     * CSV delimiter
88
     * @var string
89
     */
90
    protected $delimiter = ",";
91
92
    /**
93
     * Constructor
94
     */
95
    public function __construct()
96
    {
97
        ini_set('auto_detect_line_endings', true);
98
    }
99
100
    /**
101
     * Closes file handler
102
     */
103
    public function __destruct()
104
    {
105
        if (is_resource($this->handle)) {
106
            fclose($this->handle);
107
        }
108
    }
109
110
    /**
111
     * Sets file to parse
112
     * @param string $file
113
     * @param integer $filesize
114
     * @return $this
115
     */
116
    public function open($file, $filesize = null)
117
    {
118
        $this->handle = fopen($file, 'r');
119
120
        if (!is_resource($this->handle)) {
121
            throw new UnexpectedValueException('Failed to open CSV file');
122
        }
123
124
        $this->file = $file;
125
        $this->total = isset($filesize) ? (int) $filesize : filesize($file);
126
        return $this;
127
    }
128
129
    /**
130
     * Sets max lines to parse
131
     * @param integer $limit
132
     * @return $this
133
     */
134
    public function setLimit($limit)
135
    {
136
        $this->limit = (int) $limit;
137
        return $this;
138
    }
139
140
    /**
141
     * Sets separator between columns
142
     * @param string $character
143
     * @return $this
144
     */
145
    public function setDelimiter($character)
146
    {
147
        $this->delimiter = $character;
148
        return $this;
149
    }
150
151
    /**
152
     * Get first line of CSV file
153
     * @return array
154
     */
155
    public function getHeader()
156
    {
157
        $this->limit = 1;
158
        $header = $this->read();
159
160
        return empty($header) ? array() : reset($header);
161
    }
162
163
    /**
164
     * Sets header (first line)
165
     * @param array $header
166
     * @return $this
167
     */
168
    public function setHeader(array $header)
169
    {
170
        $this->header = $header;
171
        return $this;
172
    }
173
174
    /**
175
     * CSV reader
176
     * @return array
177
     */
178
    public function read()
179
    {
180
        $rows = array();
181
182
        $start = $this->offset ? $this->offset : $this->last;
183
184
        $this->key = 0;
185
        $this->last = 0;
186
187
        $parsed = 0;
188
        for ($this->rewind($start); $this->valid(); $this->next()) {
189
190
            $line = trim($this->current(), "\r\n");
191
192
            if (empty($line)) {
193
                continue;
194
            }
195
196
            if ($this->skip_header) {
197
                $this->skip_header = false;
198
                continue;
199
            }
200
201
            $quoted = false;
202
            $current_index = 0;
203
            $current_field = '';
204
            $fields = array();
205
206
            while ($current_index <= strlen($line)) {
207
                if ($quoted) {
208
                    $next_quote_index = strpos($line, '"', $current_index);
209
210
                    if ($next_quote_index === false) {
211
                        $current_field .= substr($line, $current_index);
212
                        $this->next();
213
214
                        if (!$this->valid()) {
215
                            $fields[] = $current_field;
216
                            break;
217
                        }
218
219
                        $current_field .= "\n";
220
                        $line = trim($this->current(), "\r\n");
221
                        $current_index = 0;
222
                        continue;
223
                    }
224
225
                    $current_field .= substr($line, $current_index, $next_quote_index - $current_index);
226
227
                    if (isset($line[$next_quote_index + 1]) && $line[$next_quote_index + 1] === '"') {
228
                        $current_field .= '"';
229
                        $current_index = $next_quote_index + 2;
230
                    } else {
231
                        $quoted = false;
232
                        $current_index = $next_quote_index + 1;
233
                    }
234
                } else {
235
                    $next_quote_index = strpos($line, '"', $current_index);
236
                    $next_delimiter_index = strpos($line, $this->delimiter, $current_index);
237
238
                    if ($next_quote_index === false) {
239
                        $next_index = $next_delimiter_index;
240
                    } elseif ($next_delimiter_index === false) {
241
                        $next_index = $next_quote_index;
242
                    } else {
243
                        $next_index = min($next_quote_index, $next_delimiter_index);
244
                    }
245
246
                    if ($next_index === false) {
247
                        $current_field .= substr($line, $current_index);
248
                        $fields[] = $current_field;
249
                        break;
250
                    } elseif ($line[$next_index] === $this->delimiter) {
251
                        $length = ($next_index + strlen($this->delimiter) - 1) - $current_index;
252
                        $current_field .= substr($line, $current_index, $length);
253
                        $fields[] = $current_field;
254
                        $current_field = '';
255
                        $current_index += $length + 1;
256
                    } else {
257
                        $quoted = true;
258
                        $current_field .= substr($line, $current_index, $next_index - $current_index);
259
                        $current_index = $next_index + 1;
260
                    }
261
                }
262
            }
263
264
            if (!empty($this->header)) {
265
                $row = array();
266
                foreach ($this->header as $key => $name) {
267
                    $field = array_shift($fields);
268
                    $row[$key] = isset($field) ? $field : '';
269
                }
270
            } else {
271
                $row = $fields;
272
            }
273
274
            $rows[$this->key] = $row;
275
276
            $parsed++;
277
278
            if (!empty($this->limit) && $parsed >= $this->limit) {
279
                $this->last = $this->getPosition();
280
                break;
281
            }
282
283
            $this->key++;
284
        }
285
286
        return $rows;
287
    }
288
289
    /**
290
     * Moves pointer to a certain position
291
     * @param integer $position
292
     */
293
    public function rewind($position = 0)
294
    {
295
        if (isset($this->handle)) {
296
            fseek($this->handle, $position);
297
            $this->next();
298
        }
299
    }
300
301
    /**
302
     * Sets current string and offset
303
     * @return null|integer
304
     */
305
    public function next()
306
    {
307
        if (isset($this->handle)) {
308
            $this->line = feof($this->handle) ? null : fgets($this->handle);
309
            $this->position = ftell($this->handle);
310
            return $this->line;
311
        }
312
313
        return null;
314
    }
315
316
    /**
317
     * Determines if CSV line is valid
318
     * @return boolean
319
     */
320
    public function valid()
321
    {
322
        return isset($this->line);
323
    }
324
325
    /**
326
     * Gets current CSV row
327
     * @return string
328
     */
329
    public function current()
330
    {
331
        return $this->line;
332
    }
333
334
    /**
335
     * Row key
336
     * @return int
337
     */
338
    public function key()
339
    {
340
        return $this->key;
341
    }
342
343
    /**
344
     * Gets current offset in bytes
345
     * @return integer
346
     */
347
    public function getPosition()
348
    {
349
        return $this->position;
350
    }
351
352
    /**
353
     * Get latest file pointer offset in bytes
354
     * @return integer
355
     */
356
    public function getOffset()
357
    {
358
        return $this->last;
359
    }
360
361
    /**
362
     * Sets initial file offset in bytes
363
     * @param integer $offset
364
     * @return $this
365
     */
366
    public function setOffset($offset)
367
    {
368
        $this->offset = $offset;
369
        return $this;
370
    }
371
372
    /**
373
     * Force to skip first row (header)
374
     * @return $this
375
     */
376
    public function skipHeader()
377
    {
378
        $this->skip_header = true;
379
        return $this;
380
    }
381
382
    /**
383
     * Parses CSV into multidimensional array
384
     * @return array
385
     */
386
    public function parse()
387
    {
388
        if (!empty($this->total)) {
389
            return $this->read();
390
        }
391
392
        return array();
393
    }
394
395
}
396