src/Eccube/Service/CsvImportService.php line 266

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of EC-CUBE
  4.  *
  5.  * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  6.  *
  7.  * http://www.ec-cube.co.jp/
  8.  *
  9.  * For the full copyright and license information, please view the LICENSE
  10.  * file that was distributed with this source code.
  11.  */
  12. namespace Eccube\Service;
  13. use Eccube\Stream\Filter\ConvertLineFeedFilter;
  14. use Eccube\Stream\Filter\SjisToUtf8EncodingFilter;
  15. /**
  16.  * Copyright (C) 2012-2014 David de Boer <david@ddeboer.nl>
  17.  *
  18.  * Permission is hereby granted, free of charge, to any person obtaining a copy of
  19.  * this software and associated documentation files (the "Software"), to deal in
  20.  * the Software without restriction, including without limitation the rights to
  21.  * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  22.  * the Software, and to permit persons to whom the Software is furnished to do so,
  23.  * subject to the following conditions:
  24.  *
  25.  * The above copyright notice and this permission notice shall be included in all
  26.  * copies or substantial portions of the Software.
  27.  *
  28.  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  29.  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  30.  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  31.  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  32.  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  33.  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  34.  * SOFTWARE.
  35.  */
  36. class CsvImportService implements \Iterator, \SeekableIterator, \Countable
  37. {
  38.     public const DUPLICATE_HEADERS_INCREMENT = 1;
  39.     public const DUPLICATE_HEADERS_MERGE = 2;
  40.     /**
  41.      * Number of the row that contains the column names
  42.      *
  43.      * @var integer
  44.      */
  45.     protected $headerRowNumber;
  46.     /**
  47.      * CSV file
  48.      *
  49.      * @var \SplFileObject
  50.      */
  51.     protected $file;
  52.     /**
  53.      * Column headers as read from the CSV file
  54.      *
  55.      * @var array
  56.      */
  57.     protected $columnHeaders = [];
  58.     /**
  59.      * Number of column headers, stored and re-used for performance
  60.      *
  61.      * In case of duplicate headers, this is always the number of unmerged headers.
  62.      *
  63.      * @var integer
  64.      */
  65.     protected $headersCount;
  66.     /**
  67.      * Total number of rows in the CSV file
  68.      *
  69.      * @var integer
  70.      */
  71.     protected $count;
  72.     /**
  73.      * Faulty CSV rows
  74.      *
  75.      * @var array
  76.      */
  77.     protected $errors = [];
  78.     /**
  79.      * How to handle duplicate headers
  80.      *
  81.      * @var integer
  82.      */
  83.     protected $duplicateHeadersFlag;
  84.     /**
  85.      * @param \SplFileObject $file
  86.      * @param string $delimiter
  87.      * @param string $enclosure
  88.      * @param string $escape
  89.      */
  90.     public function __construct(\SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
  91.     {
  92.         // stream filter を適用して文字エンコーディングと改行コードの変換を行う
  93.         // see https://github.com/EC-CUBE/ec-cube/issues/5252
  94.         $filters = [
  95.             ConvertLineFeedFilter::class,
  96.         ];
  97.         if (!\mb_check_encoding($file->current(), 'UTF-8')) {
  98.             // UTF-8 が検出できなかった場合は SJIS-win の stream filter を適用する
  99.             $filters[] = SjisToUtf8EncodingFilter::class;
  100.         }
  101.         $this->file = self::applyStreamFilter($file, ...$filters);
  102.         $this->file->setFlags(
  103.             \SplFileObject::READ_CSV |
  104.             \SplFileObject::SKIP_EMPTY |
  105.             \SplFileObject::READ_AHEAD
  106.         );
  107.         $this->file->setCsvControl(
  108.             $delimiter,
  109.             $enclosure,
  110.             $escape
  111.         );
  112.     }
  113.     /**
  114.      * Return the current row as an array
  115.      *
  116.      * If a header row has been set, an associative array will be returned
  117.      *
  118.      * @return array
  119.      */
  120.     public function current()
  121.     {
  122.         // If the CSV has no column headers just return the line
  123.         if (empty($this->columnHeaders)) {
  124.             return $this->file->current();
  125.         }
  126.         // Since the CSV has column headers use them to construct an associative array for the columns in this line
  127.         if ($this->valid()) {
  128.             $current = $this->file->current();
  129.             $line = $current;
  130.             // See if values for duplicate headers should be merged
  131.             if (self::DUPLICATE_HEADERS_MERGE === $this->duplicateHeadersFlag) {
  132.                 $line = $this->mergeDuplicates($line);
  133.             }
  134.             // Count the number of elements in both: they must be equal.
  135.             if (count($this->columnHeaders) === count($line)) {
  136.                 return array_combine(array_keys($this->columnHeaders), $line);
  137.             } else {
  138.                 return $line;
  139.             }
  140.         }
  141.         return null;
  142.     }
  143.     /**
  144.      * Get column headers
  145.      *
  146.      * @return array
  147.      */
  148.     public function getColumnHeaders()
  149.     {
  150.         return array_keys($this->columnHeaders);
  151.     }
  152.     /**
  153.      * Set column headers
  154.      *
  155.      * @param array $columnHeaders
  156.      */
  157.     public function setColumnHeaders(array $columnHeaders)
  158.     {
  159.         $this->columnHeaders = array_count_values($columnHeaders);
  160.         $this->headersCount = count($columnHeaders);
  161.     }
  162.     /**
  163.      * Set header row number
  164.      *
  165.      * @param integer $rowNumber Number of the row that contains column header names
  166.      * @param integer $duplicates How to handle duplicates (optional). One of:
  167.      *                        - CsvReader::DUPLICATE_HEADERS_INCREMENT;
  168.      *                        increments duplicates (dup, dup1, dup2 etc.)
  169.      *                        - CsvReader::DUPLICATE_HEADERS_MERGE; merges
  170.      *                        values for duplicate headers into an array
  171.      *                        (dup => [value1, value2, value3])
  172.      *
  173.      * @return boolean
  174.      */
  175.     public function setHeaderRowNumber($rowNumber, $duplicates = null)
  176.     {
  177.         $this->duplicateHeadersFlag = $duplicates;
  178.         $this->headerRowNumber = $rowNumber;
  179.         $headers = $this->readHeaderRow($rowNumber);
  180.         if ($headers === false) {
  181.             return false;
  182.         }
  183.         $this->setColumnHeaders($headers);
  184.         return true;
  185.     }
  186.     /**
  187.      * Rewind the file pointer
  188.      *
  189.      * If a header row has been set, the pointer is set just below the header
  190.      * row. That way, when you iterate over the rows, that header row is
  191.      * skipped.
  192.      */
  193.     #[\ReturnTypeWillChange]
  194.     public function rewind()
  195.     {
  196.         $this->file->rewind();
  197.         if (null !== $this->headerRowNumber) {
  198.             $this->file->seek($this->headerRowNumber + 1);
  199.         }
  200.     }
  201.     /**
  202.      * {@inheritdoc}
  203.      */
  204.     #[\ReturnTypeWillChange]
  205.     public function count()
  206.     {
  207.         if (null === $this->count) {
  208.             $position = $this->key();
  209.             $this->count = iterator_count($this);
  210.             $this->seek($position);
  211.         }
  212.         return $this->count;
  213.     }
  214.     /**
  215.      * {@inheritdoc}
  216.      */
  217.     #[\ReturnTypeWillChange]
  218.     public function next()
  219.     {
  220.         $this->file->next();
  221.     }
  222.     /**
  223.      * {@inheritdoc}
  224.      */
  225.     #[\ReturnTypeWillChange]
  226.     public function valid()
  227.     {
  228.         return $this->file->valid();
  229.     }
  230.     /**
  231.      * {@inheritdoc}
  232.      */
  233.     public function key()
  234.     {
  235.         return $this->file->key();
  236.     }
  237.     /**
  238.      * {@inheritdoc}
  239.      */
  240.     #[\ReturnTypeWillChange]
  241.     public function seek($pointer)
  242.     {
  243.         $this->file->seek($pointer);
  244.     }
  245.     /**
  246.      * {@inheritdoc}
  247.      */
  248.     public function getFields()
  249.     {
  250.         return $this->getColumnHeaders();
  251.     }
  252.     /**
  253.      * Get a row
  254.      *
  255.      * @param integer $number Row number
  256.      *
  257.      * @return array
  258.      */
  259.     public function getRow($number)
  260.     {
  261.         $this->seek($number);
  262.         return $this->current();
  263.     }
  264.     /**
  265.      * Get rows that have an invalid number of columns
  266.      *
  267.      * @return array
  268.      */
  269.     public function getErrors()
  270.     {
  271.         if (0 === $this->key()) {
  272.             // Iterator has not yet been processed, so do that now
  273.             foreach ($this as $row) { /* noop */
  274.             }
  275.         }
  276.         return $this->errors;
  277.     }
  278.     /**
  279.      * Does the reader contain any invalid rows?
  280.      *
  281.      * @return boolean
  282.      */
  283.     public function hasErrors()
  284.     {
  285.         return count($this->getErrors()) > 0;
  286.     }
  287.     /**
  288.      * Stream filter を適用し, 新たな SplFileObject を返す.
  289.      *
  290.      * @param \SplFileObject $file Stream filter を適用する SplFileObject
  291.      * @param \php_user_filter $filters 適用する stream filter のクラス名
  292.      *
  293.      * @return \SplFileObject 適用後の SplFileObject
  294.      */
  295.     public static function applyStreamFilter(\SplFileObject $file, string ...$filters): \SplFileObject
  296.     {
  297.         foreach ($filters as $filter) {
  298.             \stream_filter_register($filter, $filter);
  299.         }
  300.         $tempFile = tmpfile();
  301.         try {
  302.             foreach ($filters as $filter) {
  303.                 \stream_filter_append($tempFile, $filter);
  304.             }
  305.             foreach ($file as $line) {
  306.                 fwrite($tempFile, $line);
  307.             }
  308.             $meta = \stream_get_meta_data($tempFile);
  309.             return new \SplFileObject($meta['uri'], 'r');
  310.         } finally {
  311.             fclose($tempFile);
  312.         }
  313.     }
  314.     /**
  315.      * Read header row from CSV file
  316.      *
  317.      * @param integer $rowNumber Row number
  318.      *
  319.      * @return array
  320.      */
  321.     protected function readHeaderRow($rowNumber)
  322.     {
  323.         $this->file->seek($rowNumber);
  324.         $headers = $this->file->current();
  325.         return $headers;
  326.     }
  327.     /**
  328.      * Add an increment to duplicate headers
  329.      *
  330.      * So the following line:
  331.      * |duplicate|duplicate|duplicate|
  332.      * |first    |second   |third    |
  333.      *
  334.      * Yields value:
  335.      * $duplicate => 'first', $duplicate1 => 'second', $duplicate2 => 'third'
  336.      *
  337.      * @param array $headers
  338.      *
  339.      * @return array
  340.      */
  341.     protected function incrementHeaders(array $headers)
  342.     {
  343.         $incrementedHeaders = [];
  344.         foreach (array_count_values($headers) as $header => $count) {
  345.             if ($count > 1) {
  346.                 $incrementedHeaders[] = $header;
  347.                 for ($i = 1; $i < $count; $i++) {
  348.                     $incrementedHeaders[] = $header.$i;
  349.                 }
  350.             } else {
  351.                 $incrementedHeaders[] = $header;
  352.             }
  353.         }
  354.         return $incrementedHeaders;
  355.     }
  356.     /**
  357.      * Merges values for duplicate headers into an array
  358.      *
  359.      * So the following line:
  360.      * |duplicate|duplicate|duplicate|
  361.      * |first    |second   |third    |
  362.      *
  363.      * Yields value:
  364.      * $duplicate => ['first', 'second', 'third']
  365.      *
  366.      * @param array $line
  367.      *
  368.      * @return array
  369.      */
  370.     protected function mergeDuplicates(array $line)
  371.     {
  372.         $values = [];
  373.         $i = 0;
  374.         foreach ($this->columnHeaders as $count) {
  375.             if (1 === $count) {
  376.                 $values[] = $line[$i];
  377.             } else {
  378.                 $values[] = array_slice($line, $i, $count);
  379.             }
  380.             $i += $count;
  381.         }
  382.         return $values;
  383.     }
  384.     /**
  385.      * 行の文字エンコーディングを変換する.
  386.      *
  387.      * Windows 版 PHP7 環境では、ファイルエンコーディングが CP932 になるため UTF-8 に変換する.
  388.      * それ以外の環境では何もしない。
  389.      *
  390.      * @deprecated 使用していないため削除予定
  391.      */
  392.     protected function convertEncodingRows($row)
  393.     {
  394.         @trigger_error('The '.__METHOD__.' method is deprecated.', E_USER_DEPRECATED);
  395.         if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID >= 70000) {
  396.             foreach ($row as &$col) {
  397.                 $col = mb_convert_encoding($col, 'UTF-8', 'SJIS-win');
  398.             }
  399.         }
  400.         return $row;
  401.     }
  402. }