Failed Conditions
Pull Request — master (#1543)
by Tsuyoshi
489:47 queued 481:38
created

CsvImportService::seek()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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 4
    public function __construct(\SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
115
    {
116
        ini_set('auto_detect_line_endings', true);
117
118 4
        $this->file = $file;
119 4
        $this->file->setFlags(
120 4
            \SplFileObject::READ_CSV |
121 4
            \SplFileObject::SKIP_EMPTY |
122 4
            \SplFileObject::READ_AHEAD |
123 4
            \SplFileObject::DROP_NEW_LINE
124
        );
125 4
        $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
    public function current()
140
    {
141
        // If the CSV has no column headers just return the line
142
        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
            $line = $this->file->current();
149
150
            // See if values for duplicate headers should be merged
151
            if (self::DUPLICATE_HEADERS_MERGE === $this->duplicateHeadersFlag) {
152
                $line = $this->mergeDuplicates($line);
0 ignored issues
show
Bug introduced by
It seems like $line can also be of type string; however, Eccube\Service\CsvImportService::mergeDuplicates() 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...
153
            }
154
155
            // Count the number of elements in both: they must be equal.
156
            if (count($this->columnHeaders) === count($line)) {
157
                return array_combine(array_keys($this->columnHeaders), $line);
158
            } else {
159
                return $line;
160
            }
161
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
162
        };
163
164
        return null;
165
    }
166
167
    /**
168
     * Get column headers
169
     *
170
     * @return array
171
     */
172
    public function getColumnHeaders()
173
    {
174
        return array_keys($this->columnHeaders);
175
    }
176
177
    /**
178
     * Set column headers
179
     *
180
     * @param array $columnHeaders
181
     */
182
    public function setColumnHeaders(array $columnHeaders)
183
    {
184
        $this->columnHeaders = array_count_values($columnHeaders);
185
        $this->headersCount = count($columnHeaders);
186
    }
187
188
    /**
189
     * Set header row number
190
     *
191
     * @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...
192
     * @param integer $duplicates How to handle duplicates (optional). One of:
193
     *                        - CsvReader::DUPLICATE_HEADERS_INCREMENT;
194
     *                        increments duplicates (dup, dup1, dup2 etc.)
195
     *                        - CsvReader::DUPLICATE_HEADERS_MERGE; merges
196
     *                        values for duplicate headers into an array
197
     *                        (dup => [value1, value2, value3])
198
     * @return boolean
199
     */
200 1
    public function setHeaderRowNumber($rowNumber, $duplicates = null)
201
    {
202 1
        $this->duplicateHeadersFlag = $duplicates;
203 1
        $this->headerRowNumber = $rowNumber;
204
        $headers = $this->readHeaderRow($rowNumber);
205
206 1
        if ($headers === false) {
207
            return false;            
0 ignored issues
show
introduced by
Please trim any trailing whitespace
Loading history...
208
        }
209
        $this->setColumnHeaders($headers);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by $this->readHeaderRow($rowNumber) on line 204 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...
210 1
        return true;
0 ignored issues
show
introduced by
Missing blank line before return statement
Loading history...
211 1
    }
212
213
    /**
214
     * Rewind the file pointer
215
     *
216
     * If a header row has been set, the pointer is set just below the header
217
     * row. That way, when you iterate over the rows, that header row is
218
     * skipped.
219
     */
220 3
    public function rewind()
221
    {
222
        $this->file->rewind();
223 3
        if (null !== $this->headerRowNumber) {
224
            $this->file->seek($this->headerRowNumber + 1);
225
        }
226 3
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 2
    public function count()
232
    {
233 2
        if (null === $this->count) {
234
            $position = $this->key();
235
236
            $this->count = iterator_count($this);
237
238
            $this->seek($position);
239
        }
240
241 2
        return $this->count;
242 2
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247
    public function next()
248
    {
249
        $this->file->next();
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     */
255
    public function valid()
256
    {
257
        return $this->file->valid();
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function key()
264
    {
265
        return $this->file->key();
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271 2
    public function seek($pointer)
272
    {
273
        $this->file->seek($pointer);
274 2
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function getFields()
280
    {
281
        return $this->getColumnHeaders();
282
    }
283
284
    /**
285
     * Get a row
286
     *
287
     * @param integer $number Row number
288
     *
289
     * @return array
290
     */
291
    public function getRow($number)
292
    {
293
        $this->seek($number);
294
295
        return $this->current();
296
    }
297
298
    /**
299
     * Get rows that have an invalid number of columns
300
     *
301
     * @return array
302
     */
303
    public function getErrors()
304
    {
305
        if (0 === $this->key()) {
306
            // Iterator has not yet been processed, so do that now
307
            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...
308
            }
309
        }
310
311
        return $this->errors;
312
    }
313
314
    /**
315
     * Does the reader contain any invalid rows?
316
     *
317
     * @return boolean
318
     */
319
    public function hasErrors()
320
    {
321
        return count($this->getErrors()) > 0;
322
    }
323
324
    /**
325
     * Read header row from CSV file
326
     *
327
     * @param integer $rowNumber Row number
328
     *
329
     * @return array
330
     *
331
     */
332 1
    protected function readHeaderRow($rowNumber)
333
    {
334
        $this->file->seek($rowNumber);
335
        $headers = $this->file->current();
336
337 1
        return $headers;
338
    }
339
340
    /**
341
     * Add an increment to duplicate headers
342
     *
343
     * So the following line:
344
     * |duplicate|duplicate|duplicate|
345
     * |first    |second   |third    |
346
     *
347
     * Yields value:
348
     * $duplicate => 'first', $duplicate1 => 'second', $duplicate2 => 'third'
349
     *
350
     * @param array $headers
351
     *
352
     * @return array
353
     */
354
    protected function incrementHeaders(array $headers)
355
    {
356
        $incrementedHeaders = array();
357
        foreach (array_count_values($headers) as $header => $count) {
358
            if ($count > 1) {
359
                $incrementedHeaders[] = $header;
360
                for ($i = 1; $i < $count; $i++) {
361
                    $incrementedHeaders[] = $header . $i;
0 ignored issues
show
Coding Style introduced by
Concat operator must not be surrounded by spaces
Loading history...
362
                }
363
            } else {
364
                $incrementedHeaders[] = $header;
365
            }
366
        }
367
368
        return $incrementedHeaders;
369
    }
370
371
    /**
372
     * Merges values for duplicate headers into an array
373
     *
374
     * So the following line:
375
     * |duplicate|duplicate|duplicate|
376
     * |first    |second   |third    |
377
     *
378
     * Yields value:
379
     * $duplicate => ['first', 'second', 'third']
380
     *
381
     * @param array $line
382
     *
383
     * @return array
384
     */
385
    protected function mergeDuplicates(array $line)
386
    {
387
        $values = array();
388
389
        $i = 0;
390
        foreach ($this->columnHeaders as $count) {
391
            if (1 === $count) {
392
                $values[] = $line[$i];
393
            } else {
394
                $values[] = array_slice($line, $i, $count);
395
            }
396
397
            $i += $count;
398
        }
399
400
        return $values;
401
    }
402
403
}
404