Completed
Push — 4.0 ( 268f2c...88f012 )
by Hideki
05:48 queued 10s
created

src/Eccube/Service/CsvImportService.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.ec-cube.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service;
15
16
/**
17
 * Copyright (C) 2012-2014 David de Boer <[email protected]>
18
 *
19
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 * this software and associated documentation files (the "Software"), to deal in
21
 * the Software without restriction, including without limitation the rights to
22
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 * the Software, and to permit persons to whom the Software is furnished to do so,
24
 * subject to the following conditions:
25
 *
26
 * The above copyright notice and this permission notice shall be included in all
27
 * copies or substantial portions of the Software.
28
 *
29
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
 * SOFTWARE.
36
 */
37
class CsvImportService implements \Iterator, \SeekableIterator, \Countable
38
{
39
    const DUPLICATE_HEADERS_INCREMENT = 1;
40
    const DUPLICATE_HEADERS_MERGE = 2;
41
42
    /**
43
     * Number of the row that contains the column names
44
     *
45
     * @var integer
46
     */
47
    protected $headerRowNumber;
48
49
    /**
50
     * CSV file
51
     *
52
     * @var \SplFileObject
53
     */
54
    protected $file;
55
56
    /**
57
     * Column headers as read from the CSV file
58
     *
59
     * @var array
60
     */
61
    protected $columnHeaders = [];
62
63
    /**
64
     * Number of column headers, stored and re-used for performance
65
     *
66
     * In case of duplicate headers, this is always the number of unmerged headers.
67
     *
68
     * @var integer
69
     */
70
    protected $headersCount;
71
72
    /**
73
     * Total number of rows in the CSV file
74
     *
75
     * @var integer
76
     */
77
    protected $count;
78
79
    /**
80
     * Faulty CSV rows
81
     *
82
     * @var array
83
     */
84
    protected $errors = [];
85
86
    /**
87
     * How to handle duplicate headers
88
     *
89
     * @var integer
90
     */
91
    protected $duplicateHeadersFlag;
92
93
    /**
94
     * @param \SplFileObject $file
95
     * @param string $delimiter
96
     * @param string $enclosure
97
     * @param string $escape
98
     */
99 33
    public function __construct(\SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
100
    {
101 33
        ini_set('auto_detect_line_endings', true);
102
103 33
        $this->file = $file;
104 33
        $this->file->setFlags(
105
            \SplFileObject::READ_CSV |
106
            \SplFileObject::SKIP_EMPTY |
107
            \SplFileObject::READ_AHEAD |
108 33
            \SplFileObject::DROP_NEW_LINE
109
        );
110 33
        $this->file->setCsvControl(
111 33
            $delimiter,
112 33
            $enclosure,
113 33
            $escape
114
        );
115
    }
116
117
    /**
118
     * Return the current row as an array
119
     *
120
     * If a header row has been set, an associative array will be returned
121
     *
122
     * @return array
123
     */
124 27
    public function current()
125
    {
126
        // If the CSV has no column headers just return the line
127 27
        if (empty($this->columnHeaders)) {
128
            return $this->file->current();
129
        }
130
131
        // Since the CSV has column headers use them to construct an associative array for the columns in this line
132 27
        if ($this->valid()) {
133 27
            $current = $this->file->current();
134 27
            $current = $this->convertEncodingRows($current);
135
136 27
            $line = $current;
137
138
            // See if values for duplicate headers should be merged
139 27
            if (self::DUPLICATE_HEADERS_MERGE === $this->duplicateHeadersFlag) {
140 1
                $line = $this->mergeDuplicates($line);
141
            }
142
143
            // Count the number of elements in both: they must be equal.
144 27
            if (count($this->columnHeaders) === count($line)) {
145 27
                return array_combine(array_keys($this->columnHeaders), $line);
146
            } else {
147
                return $line;
148
            }
149
        }
150
151
        return null;
152
    }
153
154
    /**
155
     * Get column headers
156
     *
157
     * @return array
158
     */
159 27
    public function getColumnHeaders()
160
    {
161 27
        return array_keys($this->columnHeaders);
162
    }
163
164
    /**
165
     * Set column headers
166
     *
167
     * @param array $columnHeaders
168
     */
169 30
    public function setColumnHeaders(array $columnHeaders)
170
    {
171 30
        $columnHeaders = $this->convertEncodingRows($columnHeaders);
172 30
        $this->columnHeaders = array_count_values($columnHeaders);
173 30
        $this->headersCount = count($columnHeaders);
174
    }
175
176
    /**
177
     * Set header row number
178
     *
179
     * @param integer $rowNumber Number of the row that contains column header names
180
     * @param integer $duplicates How to handle duplicates (optional). One of:
181
     *                        - CsvReader::DUPLICATE_HEADERS_INCREMENT;
182
     *                        increments duplicates (dup, dup1, dup2 etc.)
183
     *                        - CsvReader::DUPLICATE_HEADERS_MERGE; merges
184
     *                        values for duplicate headers into an array
185
     *                        (dup => [value1, value2, value3])
186
     *
187
     * @return boolean
188
     */
189 28
    public function setHeaderRowNumber($rowNumber, $duplicates = null)
190
    {
191 28
        $this->duplicateHeadersFlag = $duplicates;
192 28
        $this->headerRowNumber = $rowNumber;
193 28
        $headers = $this->readHeaderRow($rowNumber);
194
195 28
        if ($headers === false) {
196
            return false;
197
        }
198 28
        $this->setColumnHeaders($headers);
0 ignored issues
show
It seems like $headers defined by $this->readHeaderRow($rowNumber) on line 193 can also be of type string; however, Eccube\Service\CsvImport...ice::setColumnHeaders() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
199
200 28
        return true;
201
    }
202
203
    /**
204
     * Rewind the file pointer
205
     *
206
     * If a header row has been set, the pointer is set just below the header
207
     * row. That way, when you iterate over the rows, that header row is
208
     * skipped.
209
     */
210 31
    public function rewind()
211
    {
212 31
        $this->file->rewind();
213 31
        if (null !== $this->headerRowNumber) {
214 27
            $this->file->seek($this->headerRowNumber + 1);
215
        }
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221 27
    public function count()
222
    {
223 27
        if (null === $this->count) {
224 27
            $position = $this->key();
225
226 27
            $this->count = iterator_count($this);
227
228 27
            $this->seek($position);
229
        }
230
231 27
        return $this->count;
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 30
    public function next()
238
    {
239 30
        $this->file->next();
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 31
    public function valid()
246
    {
247 31
        return $this->file->valid();
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 27
    public function key()
254
    {
255 27
        return $this->file->key();
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 28
    public function seek($pointer)
262
    {
263 28
        $this->file->seek($pointer);
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269 1
    public function getFields()
270
    {
271 1
        return $this->getColumnHeaders();
272
    }
273
274
    /**
275
     * Get a row
276
     *
277
     * @param integer $number Row number
278
     *
279
     * @return array
280
     */
281 1
    public function getRow($number)
282
    {
283 1
        $this->seek($number);
284
285 1
        return $this->current();
286
    }
287
288
    /**
289
     * Get rows that have an invalid number of columns
290
     *
291
     * @return array
292
     */
293
    public function getErrors()
294
    {
295
        if (0 === $this->key()) {
296
            // Iterator has not yet been processed, so do that now
297
            foreach ($this as $row) { /* noop */
298
            }
299
        }
300
301
        return $this->errors;
302
    }
303
304
    /**
305
     * Does the reader contain any invalid rows?
306
     *
307
     * @return boolean
308
     */
309
    public function hasErrors()
310
    {
311
        return count($this->getErrors()) > 0;
312
    }
313
314
    /**
315
     * Read header row from CSV file
316
     *
317
     * @param integer $rowNumber Row number
318
     *
319
     * @return array
320
     */
321 28
    protected function readHeaderRow($rowNumber)
322
    {
323 28
        $this->file->seek($rowNumber);
324 28
        $headers = $this->file->current();
325
326 28
        return $headers;
327
    }
328
329
    /**
330
     * Add an increment to duplicate headers
331
     *
332
     * So the following line:
333
     * |duplicate|duplicate|duplicate|
334
     * |first    |second   |third    |
335
     *
336
     * Yields value:
337
     * $duplicate => 'first', $duplicate1 => 'second', $duplicate2 => 'third'
338
     *
339
     * @param array $headers
340
     *
341
     * @return array
342
     */
343
    protected function incrementHeaders(array $headers)
344
    {
345
        $incrementedHeaders = [];
346
        foreach (array_count_values($headers) as $header => $count) {
347
            if ($count > 1) {
348
                $incrementedHeaders[] = $header;
349
                for ($i = 1; $i < $count; $i++) {
350
                    $incrementedHeaders[] = $header.$i;
351
                }
352
            } else {
353
                $incrementedHeaders[] = $header;
354
            }
355
        }
356
357
        return $incrementedHeaders;
358
    }
359
360
    /**
361
     * Merges values for duplicate headers into an array
362
     *
363
     * So the following line:
364
     * |duplicate|duplicate|duplicate|
365
     * |first    |second   |third    |
366
     *
367
     * Yields value:
368
     * $duplicate => ['first', 'second', 'third']
369
     *
370
     * @param array $line
371
     *
372
     * @return array
373
     */
374 1
    protected function mergeDuplicates(array $line)
375
    {
376 1
        $values = [];
377
378 1
        $i = 0;
379 1
        foreach ($this->columnHeaders as $count) {
380 1
            if (1 === $count) {
381 1
                $values[] = $line[$i];
382
            } else {
383 1
                $values[] = array_slice($line, $i, $count);
384
            }
385
386 1
            $i += $count;
387
        }
388
389 1
        return $values;
390
    }
391
392
    /**
393
     * 行の文字エンコーディングを変換する.
394
     *
395
     * Windows 版 PHP7 環境では、ファイルエンコーディングが CP932 になるため UTF-8 に変換する.
396
     * それ以外の環境では何もしない。
397
     */
398 30
    protected function convertEncodingRows($row)
399
    {
400 30
        if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID >= 70000) {
401
            foreach ($row as &$col) {
402
                $col = mb_convert_encoding($col, 'UTF-8', 'SJIS-win');
403
            }
404
        }
405
406 30
        return $row;
407
    }
408
}
409