Completed
Push — master ( f5f3dd...77ec27 )
by Iurii
07:12
created

Csv::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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