Passed
Push — master ( bf91e1...52cffa )
by Fabrice
02:32
created

CsvExtractor::getNextNonEmptyRecord()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 3
nop 0
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of YaEtl.
5
 *     (c) Fabrice de Stefanis / https://github.com/fab2s/YaEtl
6
 * This source file is licensed under the MIT license which you will
7
 * find in the LICENSE file or at https://opensource.org/licenses/MIT
8
 */
9
10
namespace fab2s\YaEtl\Loaders\File;
11
12
use fab2s\NodalFlow\NodalFlowException;
13
use fab2s\NodalFlow\YaEtlException;
14
use fab2s\YaEtl\Extractors\File\FileExtractorAbstract;
15
use fab2s\YaEtl\Traits\CsvHandlerTrait;
16
17
/**
18
 * Class CsvExtractor
19
 */
20
class CsvExtractor extends FileExtractorAbstract
21
{
22
    use CsvHandlerTrait;
23
24
    /**
25
     * CsvExtractor constructor
26
     *
27
     * @param resource|string $input
28
     * @param string          $delimiter
29
     * @param string          $enclosure
30
     * @param string          $escape
31
     *
32
     * @throws NodalFlowException
33
     * @throws YaEtlException
34
     */
35
    public function __construct($input, $delimiter = ',', $enclosure = '"', $escape = '"')
36
    {
37
        parent::__construct($input);
38
        $this->delimiter = $delimiter;
39
        $this->enclosure = $enclosure;
40
        $this->escape    = $escape;
41
    }
42
43
    /**
44
     * @param mixed $param
45
     *
46
     * @return \Generator
47
     */
48
    public function getTraversable($param = null)
49
    {
50
        while ($this->extract($param)) {
51
            if (!$this->readBom() || !$this->readSep() || false === ($firstRecord = $this->readHeader())) {
52
                return;
53
            }
54
55
            /* @var array $firstRecord */
56
            yield $this->bakeRecord($firstRecord);
57
            while (false !== ($record = fgetcsv($this->handle, 0, $this->delimiter, $this->enclosure, $this->escape))) {
58
                /* @var array $record */
59
                yield $this->bakeRecord($record);
60
            }
61
        }
62
63
        $this->releaseHandle();
64
    }
65
66
    /**
67
     * @param array $record
68
     *
69
     * @return array
70
     */
71
    protected function bakeRecord($record)
72
    {
73
        return isset($this->header) ? array_combine($this->header, $record) : $record;
74
    }
75
76
    /**
77
     * @return bool
78
     */
79
    protected function readHeader()
80
    {
81
        if (false === ($firstRecord = $this->getNextNonEmptyRecord())) {
82
            return false;
83
        }
84
85
        if ($this->useHeader && !isset($this->header)) {
86
            $this->header = array_map('trim', $firstRecord);
87
88
            return $this->getNextNonEmptyRecord();
89
        }
90
91
        return $firstRecord;
92
    }
93
94
    /**
95
     * @return bool
96
     */
97
    protected function readSep()
98
    {
99
        if (false === ($firstChar = $this->getNextNonEmptyChars())) {
100
            return false;
101
        }
102
103
        $firstCharPos = ftell($this->handle);
104
        /* @var string $firstChar */
105
        if ($firstChar === 's') {
106
            if (false === ($chars = fread($this->handle, 4))) {
107
                return false;
108
            }
109
110
            /* @var string $chars */
111
            $line = $firstChar . $chars;
112
113
            if (strpos($line, 'sep=') === 0) {
114
                $this->useSep    = true;
115
                $this->delimiter = $line[4];
116
117
                return !fseek($this->handle, $firstCharPos + 5);
118
            }
119
        }
120
121
        return !fseek($this->handle, $firstCharPos - 1);
122
    }
123
124
    /**
125
     * @return array|false
126
     */
127
    protected function getNextNonEmptyRecord()
128
    {
129
        $lastPos = ftell($this->handle);
130
        do {
131
            $record = fgetcsv($this->handle, 0, $this->delimiter, $this->enclosure, $this->escape);
132
            // since it is unclear if all PHP versions do return false, empty array or array(null)
133
            // for blank lines, we won't rely on false for EOF
134
            if (empty($record) || $record === [null]) {
135
                // at least make sure we go forward
136
                $pos = ftell($this->handle);
137
                if (feof($this->handle) || $pos <= $lastPos) {
138
                    return false;
139
                }
140
141
                $lastPos = $pos;
142
                continue;
143
            }
144
145
            return $record;
146
        } while (!feof($this->handle));
147
    }
148
}
149