Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
52.63% covered (warning)
52.63%
10 / 19
CRAP
61.54% covered (warning)
61.54%
48 / 78
CsvImportService
0.00% covered (danger)
0.00%
0 / 1
52.63% covered (warning)
52.63%
10 / 19
109.74
61.54% covered (warning)
61.54%
48 / 78
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
9 / 9
 current
0.00% covered (danger)
0.00%
0 / 1
5.93
66.67% covered (warning)
66.67%
8 / 12
 getColumnHeaders
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setColumnHeaders
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 setHeaderRowNumber
0.00% covered (danger)
0.00%
0 / 1
2.01
85.71% covered (warning)
85.71%
6 / 7
 rewind
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 count
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 next
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 valid
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 key
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 seek
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getFields
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getRow
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getErrors
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 hasErrors
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 readHeaderRow
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 incrementHeaders
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 mergeDuplicates
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 convertEncodingRows
0.00% covered (danger)
0.00%
0 / 1
6.00
50.00% covered (danger)
50.00%
2 / 4
<?php
/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) 2000-2015 LOCKON CO.,LTD. All Rights Reserved.
 *
 * http://www.lockon.co.jp/
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
namespace Eccube\Service;
use Eccube\Application;
/**
 * Copyright (C) 2012-2014 David de Boer <david@ddeboer.nl>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
class CsvImportService implements \Iterator, \SeekableIterator, \Countable
{
    const DUPLICATE_HEADERS_INCREMENT = 1;
    const DUPLICATE_HEADERS_MERGE = 2;
    /**
     * Number of the row that contains the column names
     *
     * @var integer
     */
    protected $headerRowNumber;
    /**
     * CSV file
     *
     * @var \SplFileObject
     */
    protected $file;
    /**
     * Column headers as read from the CSV file
     *
     * @var array
     */
    protected $columnHeaders = array();
    /**
     * Number of column headers, stored and re-used for performance
     *
     * In case of duplicate headers, this is always the number of unmerged headers.
     *
     * @var integer
     */
    protected $headersCount;
    /**
     * Total number of rows in the CSV file
     *
     * @var integer
     */
    protected $count;
    /**
     * Faulty CSV rows
     *
     * @var array
     */
    protected $errors = array();
    /**
     * How to handle duplicate headers
     *
     * @var integer
     */
    protected $duplicateHeadersFlag;
    /**
     * @param \SplFileObject $file
     * @param string $delimiter
     * @param string $enclosure
     * @param string $escape
     */
    public function __construct(\SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
    {
        ini_set('auto_detect_line_endings', true);
        $this->file = $file;
        $this->file->setFlags(
            \SplFileObject::READ_CSV |
            \SplFileObject::SKIP_EMPTY |
            \SplFileObject::READ_AHEAD |
            \SplFileObject::DROP_NEW_LINE
        );
        $this->file->setCsvControl(
            $delimiter,
            $enclosure,
            $escape
        );
    }
    /**
     * Return the current row as an array
     *
     * If a header row has been set, an associative array will be returned
     *
     * @return array
     */
    public function current()
    {
        // If the CSV has no column headers just return the line
        if (empty($this->columnHeaders)) {
            return $this->file->current();
        }
        // Since the CSV has column headers use them to construct an associative array for the columns in this line
        if ($this->valid()) {
            $current = $this->file->current();
            $current = $this->convertEncodingRows($current);
            $line = $current;
            // See if values for duplicate headers should be merged
            if (self::DUPLICATE_HEADERS_MERGE === $this->duplicateHeadersFlag) {
                $line = $this->mergeDuplicates($line);
            }
            // Count the number of elements in both: they must be equal.
            if (count($this->columnHeaders) === count($line)) {
                return array_combine(array_keys($this->columnHeaders), $line);
            } else {
                return $line;
            }
        };
        return null;
    }
    /**
     * Get column headers
     *
     * @return array
     */
    public function getColumnHeaders()
    {
        return array_keys($this->columnHeaders);
    }
    /**
     * Set column headers
     *
     * @param array $columnHeaders
     */
    public function setColumnHeaders(array $columnHeaders)
    {
        $columnHeaders = $this->convertEncodingRows($columnHeaders);
        $this->columnHeaders = array_count_values($columnHeaders);
        $this->headersCount = count($columnHeaders);
    }
    /**
     * Set header row number
     *
     * @param integer $rowNumber Number of the row that contains column header names
     * @param integer $duplicates How to handle duplicates (optional). One of:
     *                        - CsvReader::DUPLICATE_HEADERS_INCREMENT;
     *                        increments duplicates (dup, dup1, dup2 etc.)
     *                        - CsvReader::DUPLICATE_HEADERS_MERGE; merges
     *                        values for duplicate headers into an array
     *                        (dup => [value1, value2, value3])
     * @return boolean
     */
    public function setHeaderRowNumber($rowNumber, $duplicates = null)
    {
        $this->duplicateHeadersFlag = $duplicates;
        $this->headerRowNumber = $rowNumber;
        $headers = $this->readHeaderRow($rowNumber);
        if ($headers === false) {
            return false;
        }
        $this->setColumnHeaders($headers);
        return true;
    }
    /**
     * Rewind the file pointer
     *
     * If a header row has been set, the pointer is set just below the header
     * row. That way, when you iterate over the rows, that header row is
     * skipped.
     */
    public function rewind()
    {
        $this->file->rewind();
        if (null !== $this->headerRowNumber) {
            $this->file->seek($this->headerRowNumber + 1);
        }
    }
    /**
     * {@inheritdoc}
     */
    public function count()
    {
        if (null === $this->count) {
            $position = $this->key();
            $this->count = iterator_count($this);
            $this->seek($position);
        }
        return $this->count;
    }
    /**
     * {@inheritdoc}
     */
    public function next()
    {
        $this->file->next();
    }
    /**
     * {@inheritdoc}
     */
    public function valid()
    {
        return $this->file->valid();
    }
    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->file->key();
    }
    /**
     * {@inheritdoc}
     */
    public function seek($pointer)
    {
        $this->file->seek($pointer);
    }
    /**
     * {@inheritdoc}
     */
    public function getFields()
    {
        return $this->getColumnHeaders();
    }
    /**
     * Get a row
     *
     * @param integer $number Row number
     *
     * @return array
     */
    public function getRow($number)
    {
        $this->seek($number);
        return $this->current();
    }
    /**
     * Get rows that have an invalid number of columns
     *
     * @return array
     */
    public function getErrors()
    {
        if (0 === $this->key()) {
            // Iterator has not yet been processed, so do that now
            foreach ($this as $row) { /* noop */
            }
        }
        return $this->errors;
    }
    /**
     * Does the reader contain any invalid rows?
     *
     * @return boolean
     */
    public function hasErrors()
    {
        return count($this->getErrors()) > 0;
    }
    /**
     * Read header row from CSV file
     *
     * @param integer $rowNumber Row number
     *
     * @return array
     *
     */
    protected function readHeaderRow($rowNumber)
    {
        $this->file->seek($rowNumber);
        $headers = $this->file->current();
        return $headers;
    }
    /**
     * Add an increment to duplicate headers
     *
     * So the following line:
     * |duplicate|duplicate|duplicate|
     * |first    |second   |third    |
     *
     * Yields value:
     * $duplicate => 'first', $duplicate1 => 'second', $duplicate2 => 'third'
     *
     * @param array $headers
     *
     * @return array
     */
    protected function incrementHeaders(array $headers)
    {
        $incrementedHeaders = array();
        foreach (array_count_values($headers) as $header => $count) {
            if ($count > 1) {
                $incrementedHeaders[] = $header;
                for ($i = 1; $i < $count; $i++) {
                    $incrementedHeaders[] = $header . $i;
                }
            } else {
                $incrementedHeaders[] = $header;
            }
        }
        return $incrementedHeaders;
    }
    /**
     * Merges values for duplicate headers into an array
     *
     * So the following line:
     * |duplicate|duplicate|duplicate|
     * |first    |second   |third    |
     *
     * Yields value:
     * $duplicate => ['first', 'second', 'third']
     *
     * @param array $line
     *
     * @return array
     */
    protected function mergeDuplicates(array $line)
    {
        $values = array();
        $i = 0;
        foreach ($this->columnHeaders as $count) {
            if (1 === $count) {
                $values[] = $line[$i];
            } else {
                $values[] = array_slice($line, $i, $count);
            }
            $i += $count;
        }
        return $values;
    }
    /**
     * 行の文字エンコーディングを変換する.
     *
     * Windows 版 PHP7 環境では、ファイルエンコーディングが CP932 になるため UTF-8 に変換する.
     * それ以外の環境では何もしない。
     */
    protected function convertEncodingRows($row) {
        if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID >= 70000) {
            foreach ($row as &$col) {
                $col = mb_convert_encoding($col , 'UTF-8', 'SJIS-win');
            }
        }
        return $row;
    }
}