Completed
Push — master ( d8324b...851875 )
by Kentaro
33:50
created

CsvImportService::getColumnHeaders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/*
3
 * This file is part of EC-CUBE
4
 *
5
 * Copyright(c) 2000-2015 LOCKON CO.,LTD. All Rights Reserved.
6
 *
7
 * http://www.lockon.co.jp/
8
 *
9
 * This program is free software; you can redistribute it and/or
10
 * modify it under the terms of the GNU General Public License
11
 * as published by the Free Software Foundation; either version 2
12
 * of the License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program; if not, write to the Free Software
21
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
22
 */
23
24
namespace Eccube\Service;
25
26
use Eccube\Application;
27
28
29
/**
30
 * Copyright (C) 2012-2014 David de Boer <[email protected]>
31
 *
32
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
33
 * this software and associated documentation files (the "Software"), to deal in
34
 * the Software without restriction, including without limitation the rights to
35
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
36
 * the Software, and to permit persons to whom the Software is furnished to do so,
37
 * subject to the following conditions:
38
 *
39
 * The above copyright notice and this permission notice shall be included in all
40
 * copies or substantial portions of the Software.
41
 *
42
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
48
 * SOFTWARE.
49
 */
50
class CsvImportService implements \Iterator, \SeekableIterator, \Countable
51
{
52
53
    const DUPLICATE_HEADERS_INCREMENT = 1;
54
    const DUPLICATE_HEADERS_MERGE = 2;
55
56
    /**
57
     * Number of the row that contains the column names
58
     *
59
     * @var integer
60
     */
61
    protected $headerRowNumber;
62
63
    /**
64
     * CSV file
65
     *
66
     * @var \SplFileObject
67
     */
68
    protected $file;
69
70
    /**
71
     * Column headers as read from the CSV file
72
     *
73
     * @var array
74
     */
75
    protected $columnHeaders = array();
76
77
    /**
78
     * Number of column headers, stored and re-used for performance
79
     *
80
     * In case of duplicate headers, this is always the number of unmerged headers.
81
     *
82
     * @var integer
83
     */
84
    protected $headersCount;
85
86
    /**
87
     * Total number of rows in the CSV file
88
     *
89
     * @var integer
90
     */
91
    protected $count;
92
93
    /**
94
     * Faulty CSV rows
95
     *
96
     * @var array
97
     */
98
    protected $errors = array();
99
100
    /**
101
     * How to handle duplicate headers
102
     *
103
     * @var integer
104
     */
105
    protected $duplicateHeadersFlag;
106
107
108
    /**
109
     * @param \SplFileObject $file
110
     * @param string $delimiter
0 ignored issues
show
introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
111
     * @param string $enclosure
0 ignored issues
show
introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
112
     * @param string $escape
0 ignored issues
show
introduced by
Expected 9 spaces after parameter type; 1 found
Loading history...
113
     */
114 9
    public function __construct(\SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
115
    {
116
        ini_set('auto_detect_line_endings', true);
117
118 9
        $this->file = $file;
119 9
        $this->file->setFlags(
120 9
            \SplFileObject::READ_CSV |
121 9
            \SplFileObject::SKIP_EMPTY |
122 9
            \SplFileObject::READ_AHEAD |
123 9
            \SplFileObject::DROP_NEW_LINE
124
        );
125 9
        $this->file->setCsvControl(
126
            $delimiter,
127
            $enclosure,
128
            $escape
129
        );
130
    }
131
132
    /**
133
     * Return the current row as an array
134
     *
135
     * If a header row has been set, an associative array will be returned
136
     *
137
     * @return array
138
     */
139 1
    public function current()
140
    {
141
        // If the CSV has no column headers just return the line
142 1
        if (empty($this->columnHeaders)) {
143
            return $this->file->current();
144
        }
145
146
        // Since the CSV has column headers use them to construct an associative array for the columns in this line
147
        if ($this->valid()) {
148
            $current = $this->file->current();
149
            $current = $this->convertEncodingRows($current);
150
151 1
            $line = $current;
152
153
            // See if values for duplicate headers should be merged
154
            if (self::DUPLICATE_HEADERS_MERGE === $this->duplicateHeadersFlag) {
155
                $line = $this->mergeDuplicates($line);
156
            }
157
158
            // Count the number of elements in both: they must be equal.
159
            if (count($this->columnHeaders) === count($line)) {
160
                return array_combine(array_keys($this->columnHeaders), $line);
161
            } else {
162
                return $line;
163
            }
164
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
165
        };
166
167
        return null;
168
    }
169
170
    /**
171
     * Get column headers
172
     *
173
     * @return array
174
     */
175
    public function getColumnHeaders()
176
    {
177
        return array_keys($this->columnHeaders);
178
    }
179
180
    /**
181
     * Set column headers
182
     *
183
     * @param array $columnHeaders
184
     */
185
    public function setColumnHeaders(array $columnHeaders)
186
    {
187
        $columnHeaders = $this->convertEncodingRows($columnHeaders);
188
        $this->columnHeaders = array_count_values($columnHeaders);
189
        $this->headersCount = count($columnHeaders);
190
    }
191
192
    /**
193
     * Set header row number
194
     *
195
     * @param integer $rowNumber Number of the row that contains column header names
0 ignored issues
show
introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
196
     * @param integer $duplicates How to handle duplicates (optional). One of:
197
     *                        - CsvReader::DUPLICATE_HEADERS_INCREMENT;
198
     *                        increments duplicates (dup, dup1, dup2 etc.)
199
     *                        - CsvReader::DUPLICATE_HEADERS_MERGE; merges
200 4
     *                        values for duplicate headers into an array
201
     *                        (dup => [value1, value2, value3])
202 4
     * @return boolean
203 4
     */
204
    public function setHeaderRowNumber($rowNumber, $duplicates = null)
205
    {
206 4
        $this->duplicateHeadersFlag = $duplicates;
207
        $this->headerRowNumber = $rowNumber;
208
        $headers = $this->readHeaderRow($rowNumber);
209
210 4
        if ($headers === false) {
211 3
            return false;
212
        }
213
        $this->setColumnHeaders($headers);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by $this->readHeaderRow($rowNumber) on line 208 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...
214
        return true;
0 ignored issues
show
introduced by
Missing blank line before return statement
Loading history...
215
    }
216
217
    /**
218
     * Rewind the file pointer
219
     *
220 8
     * If a header row has been set, the pointer is set just below the header
221
     * row. That way, when you iterate over the rows, that header row is
222
     * skipped.
223 8
     */
224
    public function rewind()
225
    {
226 8
        $this->file->rewind();
227
        if (null !== $this->headerRowNumber) {
228
            $this->file->seek($this->headerRowNumber + 1);
229
        }
230
    }
231 4
232
    /**
233 4
     * {@inheritdoc}
234
     */
235
    public function count()
236
    {
237
        if (null === $this->count) {
238
            $position = $this->key();
239
240
            $this->count = iterator_count($this);
241 4
242 4
            $this->seek($position);
243
        }
244
245
        return $this->count;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function next()
252
    {
253
        $this->file->next();
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function valid()
260
    {
261
        return $this->file->valid();
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public function key()
268
    {
269
        return $this->file->key();
270
    }
271 5
272
    /**
273
     * {@inheritdoc}
274 5
     */
275
    public function seek($pointer)
276
    {
277
        $this->file->seek($pointer);
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function getFields()
284
    {
285
        return $this->getColumnHeaders();
286
    }
287
288
    /**
289
     * Get a row
290
     *
291
     * @param integer $number Row number
292
     *
293
     * @return array
294
     */
295
    public function getRow($number)
296
    {
297
        $this->seek($number);
298
299
        return $this->current();
300
    }
301
302
    /**
303
     * Get rows that have an invalid number of columns
304
     *
305
     * @return array
306
     */
307
    public function getErrors()
308
    {
309
        if (0 === $this->key()) {
310
            // Iterator has not yet been processed, so do that now
311
            foreach ($this as $row) { /* noop */
0 ignored issues
show
Unused Code introduced by
This foreach statement is empty and can be removed.

This check looks for foreach loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
312
            }
313
        }
314
315
        return $this->errors;
316
    }
317
318
    /**
319
     * Does the reader contain any invalid rows?
320
     *
321
     * @return boolean
322
     */
323
    public function hasErrors()
324
    {
325
        return count($this->getErrors()) > 0;
326
    }
327
328
    /**
329
     * Read header row from CSV file
330
     *
331
     * @param integer $rowNumber Row number
332 4
     *
333
     * @return array
334
     *
335
     */
336
    protected function readHeaderRow($rowNumber)
337 4
    {
338
        $this->file->seek($rowNumber);
339
        $headers = $this->file->current();
340
341
        return $headers;
342
    }
343
344
    /**
345
     * Add an increment to duplicate headers
346
     *
347
     * So the following line:
348
     * |duplicate|duplicate|duplicate|
349
     * |first    |second   |third    |
350
     *
351
     * Yields value:
352
     * $duplicate => 'first', $duplicate1 => 'second', $duplicate2 => 'third'
353
     *
354
     * @param array $headers
355
     *
356
     * @return array
357
     */
358
    protected function incrementHeaders(array $headers)
359
    {
360
        $incrementedHeaders = array();
361
        foreach (array_count_values($headers) as $header => $count) {
362
            if ($count > 1) {
363
                $incrementedHeaders[] = $header;
364
                for ($i = 1; $i < $count; $i++) {
365
                    $incrementedHeaders[] = $header . $i;
0 ignored issues
show
Coding Style introduced by
Concat operator must not be surrounded by spaces
Loading history...
366
                }
367
            } else {
368
                $incrementedHeaders[] = $header;
369
            }
370
        }
371
372
        return $incrementedHeaders;
373
    }
374
375
    /**
376
     * Merges values for duplicate headers into an array
377
     *
378
     * So the following line:
379
     * |duplicate|duplicate|duplicate|
380
     * |first    |second   |third    |
381
     *
382
     * Yields value:
383
     * $duplicate => ['first', 'second', 'third']
384
     *
385 1
     * @param array $line
386
     *
387 1
     * @return array
388
     */
389 1
    protected function mergeDuplicates(array $line)
390 1
    {
391
        $values = array();
392
393
        $i = 0;
394
        foreach ($this->columnHeaders as $count) {
395
            if (1 === $count) {
396
                $values[] = $line[$i];
397
            } else {
398
                $values[] = array_slice($line, $i, $count);
399
            }
400 1
401 1
            $i += $count;
402
        }
403
404
        return $values;
405
    }
406
407
    /**
408
     * 行の文字エンコーディングを変換する.
409
     *
410
     * Windows 版 PHP7 環境では、ファイルエンコーディングが CP932 になるため UTF-8 に変換する.
411
     * それ以外の環境では何もしない。
412
     */
413
    protected function convertEncodingRows($row) {
414
        if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID >= 70000) {
415
            foreach ($row as &$col) {
416
                $col = mb_convert_encoding($col , 'UTF-8', 'SJIS-win');
0 ignored issues
show
Coding Style introduced by
Space found before comma in function call
Loading history...
417
            }
418
        }
419
        return $row;
0 ignored issues
show
introduced by
Missing blank line before return statement
Loading history...
420
    }
421
}
422