silverstripe-framework/src/Dev/CSVParser.php

343 lines
7.6 KiB
PHP

<?php
namespace SilverStripe\Dev;
use League\Csv\Reader;
use SilverStripe\Core\Injector\Injectable;
use Iterator;
use SilverStripe\Control\Director;
/**
* Class to handle parsing of CSV files, where the column headers are in the
* first row.
*
* The idea is that you pass it another object to handle the actual processing
* of the data in the CSV file.
*
* Usage:
*
* <code>
* $parser = new CSVParser('myfile.csv');
* $parser->mapColumns(array(
* 'first name' => 'FirstName',
* 'lastname' => 'Surname',
* 'last name' => 'Surname',
* ));
* foreach($parser as $row) {
* // $row is a map of column name => column value
* $obj = new MyDataObject();
* $obj->update($row);
* $obj->write();
* }
* </code>
* @deprecated 4.13.0 Use League\Csv\Reader instead
*/
class CSVParser implements Iterator
{
use Injectable;
/**
* @var string $filename
*/
protected $filename;
/**
* @var resource $fileHandle
*/
protected $fileHandle;
/**
* Map of source columns to output columns.
*
* Once they get into this variable, all of the source columns are in
* lowercase.
*
* @var array
*/
protected $columnMap = [];
/**
* The header row used to map data in the CSV file.
*
* To begin with, this is null. Once it has been set, data will get
* returned from the CSV file.
*
* @var array
*/
protected $headerRow = null;
/**
* A custom header row provided by the caller.
*
* @var array
*/
protected $providedHeaderRow = null;
/**
* The data of the current row.
*
* @var array
*/
protected $currentRow = null;
/**
* The current row number.
*
* 1 is the first data row in the CSV file; the header row, if it exists,
* is ignored.
*
* @var int
*/
protected $rowNum = 0;
/**
* The character for separating columns.
*
* @var string
*/
protected $delimiter = ",";
/**
* The character for quoting columns.
*
* @var string
*/
protected $enclosure = '"';
/**
* Open a CSV file for parsing.
*
* You can use the object returned in a foreach loop to extract the data.
*
* @param string $filename The name of the file. If relative, it will be relative to the site's base dir
* @param string $delimiter The character for separating columns
* @param string $enclosure The character for quoting or enclosing columns
*/
public function __construct($filename, $delimiter = ",", $enclosure = '"')
{
Deprecation::notice('4.13.0', 'Use ' . Reader::class . ' instead', Deprecation::SCOPE_CLASS);
$filename = Director::getAbsFile($filename);
$this->filename = $filename;
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
}
/**
* Re-map columns in the CSV file.
*
* This can be useful for identifying synonyms in the file. For example:
*
* <code>
* $csv->mapColumns(array(
* 'firstname' => 'FirstName',
* 'last name' => 'Surname',
* ));
* </code>
*
* @param array $columnMap
*/
public function mapColumns($columnMap)
{
if ($columnMap) {
$lowerColumnMap = [];
foreach ($columnMap as $k => $v) {
$lowerColumnMap[strtolower($k)] = $v;
}
$this->columnMap = array_merge($this->columnMap, $lowerColumnMap);
}
}
/**
* If your CSV file doesn't have a header row, then you can call this
* function to provide one.
*
* If you call this function, then the first row of the CSV will be
* included in the data returned.
*
* @param array $headerRow
*/
public function provideHeaderRow($headerRow)
{
$this->providedHeaderRow = $headerRow;
}
/**
* Open the CSV file for reading.
*/
protected function openFile()
{
$this->fileHandle = fopen($this->filename ?? '', 'r');
if ($this->providedHeaderRow) {
$this->headerRow = $this->remapHeader($this->providedHeaderRow);
}
}
/**
* Close the CSV file and re-set all of the internal variables.
*/
protected function closeFile()
{
if ($this->fileHandle) {
fclose($this->fileHandle);
}
$this->fileHandle = null;
$this->rowNum = 0;
$this->currentRow = null;
$this->headerRow = null;
}
/**
* Get a header row from the CSV file.
*/
protected function fetchCSVHeader()
{
$srcRow = fgetcsv(
$this->fileHandle,
0,
$this->delimiter ?? '',
$this->enclosure ?? ''
);
$this->headerRow = $this->remapHeader($srcRow);
}
/**
* Map the contents of a header array using $this->mappedColumns.
*
* @param array $header
*
* @return array
*/
protected function remapHeader($header)
{
$mappedHeader = [];
foreach ($header as $item) {
if (isset($this->columnMap[strtolower($item)])) {
$item = $this->columnMap[strtolower($item)];
}
$mappedHeader[] = $item;
}
return $mappedHeader;
}
/**
* Get a row from the CSV file and update $this->currentRow;
*
* @return array
*/
protected function fetchCSVRow()
{
if (!$this->fileHandle) {
$this->openFile();
}
if (!$this->headerRow) {
$this->fetchCSVHeader();
}
$this->rowNum++;
$srcRow = fgetcsv(
$this->fileHandle,
0,
$this->delimiter ?? '',
$this->enclosure ?? ''
);
if ($srcRow) {
$row = [];
foreach ($srcRow as $i => $value) {
// Allow escaping of quotes and commas in the data
$value = str_replace(
['\\' . $this->enclosure,'\\' . $this->delimiter],
[$this->enclosure, $this->delimiter],
$value ?? ''
);
// Trim leading tab
// [SS-2017-007] Ensure all cells with leading [@=+] have a leading tab
$value = ltrim($value ?? '', "\t");
if (array_key_exists($i, $this->headerRow ?? [])) {
if ($this->headerRow[$i]) {
$row[$this->headerRow[$i]] = $value;
}
} else {
user_error("No heading for column $i on row $this->rowNum", E_USER_WARNING);
}
}
$this->currentRow = $row;
} else {
$this->closeFile();
}
return $this->currentRow;
}
/**
* @ignore
*/
public function __destruct()
{
$this->closeFile();
}
//// ITERATOR FUNCTIONS
/**
* @ignore
*/
#[\ReturnTypeWillChange]
public function rewind()
{
$this->closeFile();
$this->fetchCSVRow();
}
/**
* @ignore
*/
#[\ReturnTypeWillChange]
public function current()
{
return $this->currentRow;
}
/**
* @ignore
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->rowNum;
}
/**
* @ignore
*/
#[\ReturnTypeWillChange]
public function next()
{
$this->fetchCSVRow();
return $this->currentRow;
}
/**
* @ignore
*/
#[\ReturnTypeWillChange]
public function valid()
{
return $this->currentRow ? true : false;
}
}