add League csv plugin

This commit is contained in:
Li Jianxuan 2015-01-27 10:19:56 +00:00
parent 460ec78cb7
commit d323ccd1ff
9 changed files with 2025 additions and 0 deletions

View File

@ -0,0 +1,213 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv;
use IteratorAggregate;
use JsonSerializable;
use League\Csv\Config\Controls;
use League\Csv\Config\Factory;
use League\Csv\Config\Output;
use League\Csv\Config\StreamFilter;
use SplFileInfo;
use SplFileObject;
/**
* An abstract class to enable basic CSV manipulation
*
* @package League.csv
* @since 4.0.0
*
*/
abstract class AbstractCsv implements JsonSerializable, IteratorAggregate
{
/**
* UTF-8 BOM sequence
*/
const BOM_UTF8 = "\xEF\xBB\xBF";
/**
* UTF-16 BE BOM sequence
*/
const BOM_UTF16_BE = "\xFE\xFF";
/**
* UTF-16 LE BOM sequence
*/
const BOM_UTF16_LE = "\xFF\xFE";
/**
* UTF-16 BE BOM sequence
*/
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
/**
* UTF-16 LE BOM sequence
*/
const BOM_UTF32_LE = "\x00\x00\xFF\xFE";
/**
* The constructor path
*
* can be a SplFileInfo object or the string path to a file
*
* @var \SplFileObject|string
*/
protected $path;
/**
* The file open mode flag
*
* @var string
*/
protected $open_mode;
/**
* Csv Controls Trait
*/
use Controls;
/**
* Csv Factory Trait
*/
use Factory;
/**
* Csv Ouputting Trait
*/
use Output;
/**
* Stream Filter API Trait
*/
use StreamFilter;
/**
* Create a new instance
*
* The path must be an SplFileInfo object
* an object that implements the `__toString` method
* a path to a file
*
* @param object|string $path The file path
* @param string $open_mode the file open mode flag
*/
public function __construct($path, $open_mode = 'r+')
{
ini_set('auto_detect_line_endings', '1');
$this->path = $this->normalizePath($path);
$this->open_mode = strtolower($open_mode);
$this->initStreamFilter($this->path);
}
/**
* Return a normalize path which could be a SplFileObject
* or a string path
*
* @param object|string $path the filepath
*
* @return \SplFileObject|string
*/
protected function normalizePath($path)
{
if ($path instanceof SplFileObject) {
return $path;
} elseif ($path instanceof SplFileInfo) {
return $path->getPath().'/'.$path->getBasename();
}
return trim($path);
}
/**
* The destructor
*/
public function __destruct()
{
//in case $this->path is a SplFileObject we need to remove its reference
$this->path = null;
}
/**
* Return the CSV Iterator
*
* @return \SplFileObject
*/
public function getIterator()
{
$obj = $this->path;
if (! $obj instanceof SplFileObject) {
$obj = new SplFileObject($this->getStreamFilterPath(), $this->open_mode);
}
$obj->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$obj->setFlags($this->flags);
return $obj;
}
/**
* Create a {@link AbstractCsv} instance from another {@link AbstractCsv} object
*
* @param string $class_name the class to be instantiated
* @param string $open_mode the file open mode flag
*
* @return static
*/
protected function newInstance($class_name, $open_mode)
{
$csv = new $class_name($this->path, $open_mode);
$csv->delimiter = $this->delimiter;
$csv->enclosure = $this->enclosure;
$csv->escape = $this->escape;
$csv->encodingFrom = $this->encodingFrom;
$csv->bom = $this->bom;
return $csv;
}
/**
* Create a {@link Writer} instance from a {@link AbstractCsv} object
*
* @param string $open_mode the file open mode flag
*
* @return \League\Csv\Writer
*/
public function newWriter($open_mode = 'r+')
{
return $this->newInstance('\League\Csv\Writer', $open_mode);
}
/**
* Create a {@link Reader} instance from a {@link AbstractCsv} object
*
* @param string $open_mode the file open mode flag
*
* @return \League\Csv\Reader
*/
public function newReader($open_mode = 'r+')
{
return $this->newInstance('\League\Csv\Reader', $open_mode);
}
/**
* Validate a variable to be stringable
*
* @param object|string $str
*
* @return bool
*/
public static function isValidString($str)
{
return is_scalar($str) || (is_object($str) && method_exists($str, '__toString'));
}
}

View File

@ -0,0 +1,259 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Config;
use CallbackFilterIterator;
use InvalidArgumentException;
use LimitIterator;
use SplFileObject;
/**
* A trait to configure and check CSV file and content
*
* @package League.csv
* @since 6.0.0
*
*/
trait Controls
{
/**
* the field delimiter (one character only)
*
* @var string
*/
protected $delimiter = ',';
/**
* the field enclosure character (one character only)
*
* @var string
*/
protected $enclosure = '"';
/**
* the field escape character (one character only)
*
* @var string
*/
protected $escape = '\\';
/**
* the \SplFileObject flags holder
*
* @var int
*/
protected $flags = SplFileObject::READ_CSV;
/**
* return a SplFileOjbect
*
* @return \SplFileOjbect
*/
abstract public function getIterator();
/**
* set the field delimeter
*
* @param string $delimiter
*
* @throws \InvalidArgumentException If $delimeter is not a single character
*
* @return $this
*/
public function setDelimiter($delimiter = ',')
{
if (1 != mb_strlen($delimiter)) {
throw new InvalidArgumentException('The delimiter must be a single character');
}
$this->delimiter = $delimiter;
return $this;
}
/**
* return the current field delimiter
*
* @return string
*/
public function getDelimiter()
{
return $this->delimiter;
}
/**
* detect the actual number of row according to a delimiter
*
* @param string $delimiter a CSV delimiter
* @param int $nb_rows the number of row to consider
*
* @return int
*/
protected function fetchRowsCountByDelimiter($delimiter, $nb_rows = 1)
{
$iterator = $this->getIterator();
$iterator->setCsvControl($delimiter, $this->enclosure, $this->escape);
//"reduce" the csv length to a maximum of $nb_rows
$iterator = new LimitIterator($iterator, 0, $nb_rows);
//return the parse rows
$iterator = new CallbackFilterIterator($iterator, function ($row) {
return is_array($row) && count($row) > 1;
});
return count(iterator_to_array($iterator, false), COUNT_RECURSIVE);
}
/**
* Detect the CSV file delimiter
*
* @param int $nb_rows
* @param string[] $delimiters additional delimiters
*
* @throws \InvalidArgumentException If $nb_rows value is invalid
*
* @return string[]
*/
public function detectDelimiterList($nb_rows = 1, array $delimiters = [])
{
$nb_rows = filter_var($nb_rows, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (! $nb_rows) {
throw new InvalidArgumentException('`$nb_rows` must be a valid positive integer');
}
$delimiters = array_filter($delimiters, function ($str) {
return 1 == mb_strlen($str);
});
$delimiters = array_merge([$this->delimiter, ',', ';', "\t"], $delimiters);
$delimiters = array_unique($delimiters);
$res = array_fill_keys($delimiters, 0);
array_walk($res, function (&$value, $delim) use ($nb_rows) {
$value = $this->fetchRowsCountByDelimiter($delim, $nb_rows);
});
arsort($res, SORT_NUMERIC);
return array_keys(array_filter($res));
}
/**
* set the field enclosure
*
* @param string $enclosure
*
* @throws \InvalidArgumentException If $enclosure is not a single character
*
* @return $this
*/
public function setEnclosure($enclosure = '"')
{
if (1 != mb_strlen($enclosure)) {
throw new InvalidArgumentException('The enclosure must be a single character');
}
$this->enclosure = $enclosure;
return $this;
}
/**
* return the current field enclosure
*
* @return string
*/
public function getEnclosure()
{
return $this->enclosure;
}
/**
* set the field escape character
*
* @param string $escape
*
* @throws \InvalidArgumentException If $escape is not a single character
*
* @return $this
*/
public function setEscape($escape = "\\")
{
if (1 != mb_strlen($escape)) {
throw new InvalidArgumentException('The escape character must be a single character');
}
$this->escape = $escape;
return $this;
}
/**
* return the current field escape character
*
* @return string
*/
public function getEscape()
{
return $this->escape;
}
/**
* Set the Flags associated to the CSV SplFileObject
*
* @param int $flags
*
* @throws \InvalidArgumentException If the argument is not a valid integer
*
* @return $this
*/
public function setFlags($flags)
{
if (false === filter_var($flags, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
throw new InvalidArgumentException('you should use a `SplFileObject` Constant');
}
$this->flags = $flags|SplFileObject::READ_CSV|SplFileObject::DROP_NEW_LINE;
return $this;
}
/**
* Returns the file Flags
*
* @return int
*/
public function getFlags()
{
return $this->flags;
}
/**
* Returns the BOM sequence of the given CSV
*
* @return string
*/
public function getInputBOM()
{
$bom = [
self::BOM_UTF8,
self::BOM_UTF16_BE,
self::BOM_UTF16_LE,
self::BOM_UTF32_BE,
self::BOM_UTF32_LE
];
$csv = $this->getIterator();
$csv->rewind();
$line = $csv->fgets();
$res = array_filter($bom, function ($sequence) use ($line) {
return strpos($line, $sequence) === 0;
});
return array_shift($res);
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.X.X
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Config;
use InvalidArgumentException;
use SplFileInfo;
use SplFileObject;
use SplTempFileObject;
/**
* A trait to facilate class instantiation
*
* @package League.csv
* @since 6.X.X
*
*/
trait Factory
{
/**
* Create a {@link AbstractCsv} from a string
*
* The path can be:
* - an SplFileInfo,
* - a SplFileObject,
* - an object that implements the `__toString` method,
* - a string
*
* BUT NOT a SplTempFileObject
*
* <code>
*<?php
* $csv = new Reader::createFromPath('/path/to/file.csv', 'a+');
* $csv = new Reader::createFromPath(new SplFileInfo('/path/to/file.csv'));
* $csv = new Reader::createFromPath(new SplFileObject('/path/to/file.csv'), 'rb');
*
* ?>
* </code>
*
* @param object|string $path file path
* @param string $open_mode the file open mode flag
*
* @throws \InvalidArgumentException If $path is a \SplTempFileObject object
*
* @return static
*/
public static function createFromPath($path, $open_mode = 'r+')
{
if ($path instanceof SplTempFileObject) {
throw new InvalidArgumentException('an `SplTempFileObject` object does not contain a valid path');
} elseif ($path instanceof SplFileInfo) {
$path = $path->getPath().'/'.$path->getBasename();
}
return new static(trim($path), $open_mode);
}
/**
* Create a {@link AbstractCsv} from a SplFileObject
*
* The path can be:
* - a SplFileObject,
* - a SplTempFileObject
*
* <code>
*<?php
* $csv = new Writer::createFromFileObject(new SplFileInfo('/path/to/file.csv'));
* $csv = new Writer::createFromFileObject(new SplTempFileObject);
*
* ?>
* </code>
*
* @param SplFileObject $obj
*
* @return static
*/
public static function createFromFileObject(SplFileObject $obj)
{
return new static($obj);
}
/**
* Create a {@link AbstractCsv} from a string
*
* The string must be an object that implements the `__toString` method,
* or a string
*
* @param string|object $str the string
*
* @throws \InvalidArgumentException If the data provided is invalid
*
* @return static
*/
public static function createFromString($str)
{
if (! self::isValidString($str)) {
throw new InvalidArgumentException(
'the submitted data must be a string or an object implementing the `__toString` method'
);
}
$obj = new SplTempFileObject();
$obj->fwrite(rtrim($str)."\n");
return static::createFromFileObject($obj);
}
}

View File

@ -0,0 +1,217 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Config;
use DomDocument;
use InvalidArgumentException;
use League\Csv\Iterator\MapIterator;
use Traversable;
/**
* A trait to output CSV
*
* @package League.csv
* @since 6.3.0
*
*/
trait Output
{
/**
* Charset Encoding for the CSV
*
* @var string
*/
protected $encodingFrom = 'UTF-8';
/**
* BOM sequence for Outputting the CSV
* @var string
*/
protected $bom;
/**
* Return the CSV Iterator
*
* @return \SplFileObject
*/
abstract public function getIterator();
/**
* JsonSerializable Interface
*
* @return array
*/
public function jsonSerialize()
{
return iterator_to_array($this->convertToUtf8($this->getIterator()), false);
}
/**
* Set the CSV encoding charset
*
* @param string $str
*
* @return static
*/
public function setEncodingFrom($str)
{
$str = str_replace('_', '-', $str);
$str = filter_var($str, FILTER_SANITIZE_STRING, ['flags' => FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH]);
if (empty($str)) {
throw new InvalidArgumentException('you should use a valid charset');
}
$this->encodingFrom = strtoupper($str);
return $this;
}
/**
* Get the source CSV encoding charset
*
* @return string
*/
public function getEncodingFrom()
{
return $this->encodingFrom;
}
/**
* Set the BOM sequence to prepend the CSV on output
*
* @param string $str The BOM sequence
*
* @return static
*/
public function setOutputBOM($str = null)
{
if (empty($str)) {
$this->bom = null;
return $this;
}
$str = (string) $str;
$str = trim($str);
$this->bom = $str;
return $this;
}
/**
* Returns the BOM sequence in use on Output methods
*
* @return string
*/
public function getOutputBOM()
{
return $this->bom;
}
/**
* Output all data on the CSV file
*
* @param string $filename CSV downloaded name if present adds extra headers
*/
public function output($filename = null)
{
$iterator = $this->getIterator();
$iterator->rewind();
//@codeCoverageIgnoreStart
if (! is_null($filename) && self::isValidString($filename)) {
$filename = trim($filename);
$filename = filter_var($filename, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
header("Content-Type: application/octet-stream");
header("Content-Transfer-Encoding: binary");
header('Content-Disposition: attachment; filename="'.$filename);
}
//@codeCoverageIgnoreEnd
echo $this->bom;
$iterator->fpassthru();
}
/**
* Retrieves the CSV content
*
* @return string
*/
public function __toString()
{
ob_start();
$this->output();
return ob_get_clean();
}
/**
* Return a HTML table representation of the CSV Table
*
* @param string $class_name optional classname
*
* @return string
*/
public function toHTML($class_name = 'table-csv-data')
{
$doc = $this->toXML('table', 'tr', 'td');
$doc->documentElement->setAttribute('class', $class_name);
return $doc->saveHTML($doc->documentElement);
}
/**
* transform a CSV into a XML
*
* @param string $root_name XML root node name
* @param string $row_name XML row node name
* @param string $cell_name XML cell node name
*
* @return \DomDocument
*/
public function toXML($root_name = 'csv', $row_name = 'row', $cell_name = 'cell')
{
$doc = new DomDocument('1.0', 'UTF-8');
$root = $doc->createElement($root_name);
$iterator = $this->convertToUtf8($this->getIterator());
foreach ($iterator as $row) {
$item = $doc->createElement($row_name);
array_walk($row, function ($value) use (&$item, $doc, $cell_name) {
$content = $doc->createTextNode($value);
$cell = $doc->createElement($cell_name);
$cell->appendChild($content);
$item->appendChild($cell);
});
$root->appendChild($item);
}
$doc->appendChild($root);
return $doc;
}
/**
* Convert Csv file into UTF-8
*
* @return \Traversable
*/
protected function convertToUtf8(Traversable $iterator)
{
if (strpos($this->encodingFrom, 'UTF-8') !== false) {
return $iterator;
}
return new MapIterator($iterator, function ($row) {
foreach ($row as &$value) {
$value = mb_convert_encoding($value, 'UTF-8', $this->encodingFrom);
}
unset($value);
return $row;
});
}
}

View File

@ -0,0 +1,271 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Config;
use LogicException;
use OutOfBoundsException;
use SplFileInfo;
/**
* A Trait to ease PHP Stream Filters manipulation
* with a SplFileObject
*
* @package League.csv
* @since 6.0.0
*
*/
trait StreamFilter
{
/**
* collection of stream filters
*
* @var array
*/
protected $stream_filters = [];
/**
* Stream filtering mode to apply on all filters
*
* @var int
*/
protected $stream_filter_mode = STREAM_FILTER_ALL;
/**
*the real path
*
* @var string the real path to the file
*
*/
protected $stream_uri;
/**
* Internal path setter
*
* The path must be an SplFileInfo object
* an object that implements the `__toString` method
* a path to a file
*
* @param \SplFileObject|string $path The file path
*/
protected function initStreamFilter($path)
{
if (! is_string($path)) {
$this->stream_uri = null;
$this->stream_filters = [];
return;
}
$this->extractStreamSettings($path);
}
/**
* Extract Available stream settings from $path
*
* @param string $path the file path
*/
protected function extractStreamSettings($path)
{
if (! preg_match(
',^php://filter/(?P<mode>:?read=|write=)?(?P<filters>.*?)/resource=(?P<resource>.*)$,i',
$path,
$matches
)) {
$this->stream_uri = $path;
$this->stream_filters = [];
return;
}
$matches['mode'] = strtolower($matches['mode']);
$mode = STREAM_FILTER_ALL;
if ('write=' == $matches['mode']) {
$mode = STREAM_FILTER_WRITE;
} elseif ('read=' == $matches['mode']) {
$mode = STREAM_FILTER_READ;
}
$this->stream_filter_mode = $mode;
$this->stream_uri = $matches['resource'];
$this->stream_filters = explode('|', $matches['filters']);
}
/**
* Check if the trait methods can be used
*
* @throws \LogicException If the API can not be use
*/
protected function assertStreamable()
{
if (! is_string($this->stream_uri)) {
throw new LogicException('The stream filter API can not be used');
}
}
/**
* Tells whether the stream filter capabilities can be used
*
* @return bool
*/
public function isActiveStreamFilter()
{
return is_string($this->stream_uri);
}
/**
* stream filter mode Setter
*
* Set the new Stream Filter mode and remove all
* previously attached stream filters
*
* @param int $mode
*
* @throws \OutOfBoundsException If the mode is invalid
*
* @return $this
*/
public function setStreamFilterMode($mode)
{
$this->assertStreamable();
if (! in_array($mode, [STREAM_FILTER_ALL, STREAM_FILTER_READ, STREAM_FILTER_WRITE])) {
throw new OutOfBoundsException('the $mode should be a valid `STREAM_FILTER_*` constant');
}
$this->stream_filter_mode = $mode;
$this->stream_filters = [];
return $this;
}
/**
* stream filter mode getter
*
* @return int
*/
public function getStreamFilterMode()
{
$this->assertStreamable();
return $this->stream_filter_mode;
}
/**
* append a stream filter
*
* @param string $filter_name a string or an object that implements the '__toString' method
*
* @return $this
*/
public function appendStreamFilter($filter_name)
{
$this->assertStreamable();
$this->stream_filters[] = $this->sanitizeStreamFilter($filter_name);
return $this;
}
/**
* prepend a stream filter
*
* @param string $filter_name a string or an object that implements the '__toString' method
*
* @return $this
*/
public function prependStreamFilter($filter_name)
{
$this->assertStreamable();
array_unshift($this->stream_filters, $this->sanitizeStreamFilter($filter_name));
return $this;
}
/**
* Sanitize the stream filter name
*
* @param string $filter_name the stream filter name
*
* @return string
*/
protected function sanitizeStreamFilter($filter_name)
{
$this->assertStreamable();
$filter_name = (string) $filter_name;
return trim($filter_name);
}
/**
* Detect if the stream filter is already present
*
* @param string $filter_name
*
* @return bool
*/
public function hasStreamFilter($filter_name)
{
$this->assertStreamable();
return false !== array_search($filter_name, $this->stream_filters, true);
}
/**
* Remove a filter from the collection
*
* @param string $filter_name
*
* @return $this
*/
public function removeStreamFilter($filter_name)
{
$this->assertStreamable();
$res = array_search($filter_name, $this->stream_filters, true);
if (false !== $res) {
unset($this->stream_filters[$res]);
}
return $this;
}
/**
* Remove all registered stream filter
*
* @return $this
*/
public function clearStreamFilter()
{
$this->assertStreamable();
$this->stream_filters = [];
return $this;
}
/**
* Return the filter path
*
* @return string
*/
protected function getStreamFilterPath()
{
$this->assertStreamable();
if (! $this->stream_filters) {
return $this->stream_uri;
}
$prefix = '';
if (STREAM_FILTER_READ == $this->stream_filter_mode) {
$prefix = 'read=';
} elseif (STREAM_FILTER_WRITE == $this->stream_filter_mode) {
$prefix = 'write=';
}
return 'php://filter/'.$prefix.implode('|', $this->stream_filters).'/resource='.$this->stream_uri;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Iterator;
use IteratorIterator;
use Traversable;
/**
* A simple MapIterator
*
* @package League.csv
* @since 3.3.0
* @internal used internally to modify CSV content
*
*/
class MapIterator extends IteratorIterator
{
/**
* The function to be apply on all InnerIterator element
*
* @var callable
*/
private $callable;
/**
* The Constructor
*
* @param Traversable $iterator
* @param callable $callable
*/
public function __construct(Traversable $iterator, callable $callable)
{
parent::__construct($iterator);
$this->callable = $callable;
}
/**
* Get the value of the current element
*/
public function current()
{
$iterator = $this->getInnerIterator();
$callable = $this->callable;
return $callable($iterator->current(), $iterator->key(), $iterator);
}
}

View File

@ -0,0 +1,269 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv\Iterator;
use ArrayIterator;
use CallbackFilterIterator;
use InvalidArgumentException;
use Iterator;
use LimitIterator;
/**
* A Trait to Query rows against a SplFileObject
*
* @package League.csv
* @since 4.2.1
*
*/
trait Query
{
/**
* Callable function to filter the iterator
*
* @var array
*/
protected $iterator_filters = [];
/**
* Callable function to sort the ArrayObject
*
* @var callable
*/
protected $iterator_sort_by = [];
/**
* iterator Offset
*
* @var int
*/
protected $iterator_offset = 0;
/**
* iterator maximum length
*
* @var int
*/
protected $iterator_limit = -1;
/**
* Set LimitIterator Offset
*
* @param $offset
*
* @return $this
*/
public function setOffset($offset = 0)
{
if (false === filter_var($offset, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
throw new InvalidArgumentException('the offset must be a positive integer or 0');
}
$this->iterator_offset = $offset;
return $this;
}
/**
* Set LimitInterator Count
*
* @param int $limit
*
* @return $this
*/
public function setLimit($limit = -1)
{
if (false === filter_var($limit, FILTER_VALIDATE_INT, ['options' => ['min_range' => -1]])) {
throw new InvalidArgumentException('the limit must an integer greater or equals to -1');
}
$this->iterator_limit = $limit;
return $this;
}
/**
* Sort the Iterator
*
* @param \Iterator $iterator
*
* @return \LimitIterator
*/
protected function applyIteratorInterval(Iterator $iterator)
{
if (0 == $this->iterator_offset && -1 == $this->iterator_limit) {
return $iterator;
}
$offset = $this->iterator_offset;
$limit = $this->iterator_limit;
$this->iterator_limit = -1;
$this->iterator_offset = 0;
return new LimitIterator($iterator, $offset, $limit);
}
/**
* Set an Iterator sorting callable function
*
* @param callable $callable
*
* @return $this
*/
public function addSortBy(callable $callable)
{
$this->iterator_sort_by[] = $callable;
return $this;
}
/**
* Remove a callable from the collection
*
* @param callable $callable
*
* @return $this
*/
public function removeSortBy(callable $callable)
{
$res = array_search($callable, $this->iterator_sort_by, true);
if (false !== $res) {
unset($this->iterator_sort_by[$res]);
}
return $this;
}
/**
* Detect if the callable is already registered
*
* @param callable $callable
*
* @return bool
*/
public function hasSortBy(callable $callable)
{
return false !== array_search($callable, $this->iterator_sort_by, true);
}
/**
* Remove all registered callable
*
* @return $this
*/
public function clearSortBy()
{
$this->iterator_sort_by = [];
return $this;
}
/**
* Sort the Iterator
*
* @param \Iterator $iterator
*
* @return \ArrayIterator
*/
protected function applyIteratorSortBy(Iterator $iterator)
{
if (! $this->iterator_sort_by) {
return $iterator;
}
$nb_callbacks = count($this->iterator_sort_by);
$this->iterator_sort_by = array_values($this->iterator_sort_by);
$res = iterator_to_array($iterator, false);
uasort($res, function ($rowA, $rowB) use ($nb_callbacks) {
$res = 0;
$index = 0;
while ($index < $nb_callbacks && 0 === $res) {
$res = $this->iterator_sort_by[$index]($rowA, $rowB);
++$index;
}
return $res;
});
$this->clearSortBy();
return new ArrayIterator($res);
}
/**
* Set the Iterator filter method
*
* @param callable $callable
*
* @return $this
*/
public function addFilter(callable $callable)
{
$this->iterator_filters[] = $callable;
return $this;
}
/**
* Remove a filter from the callable collection
*
* @param callable $callable
*
* @return $this
*/
public function removeFilter(callable $callable)
{
$res = array_search($callable, $this->iterator_filters, true);
if (false !== $res) {
unset($this->iterator_filters[$res]);
}
return $this;
}
/**
* Detect if the callable filter is already registered
*
* @param callable $callable
*
* @return bool
*/
public function hasFilter(callable $callable)
{
return false !== array_search($callable, $this->iterator_filters, true);
}
/**
* Remove all registered callable filter
*
* @return $this
*/
public function clearFilter()
{
$this->iterator_filters = [];
return $this;
}
/**
* Filter the Iterator
*
* @param \Iterator $iterator
*
* @return \Iterator
*/
protected function applyIteratorFilter(Iterator $iterator)
{
foreach ($this->iterator_filters as $callable) {
$iterator = new CallbackFilterIterator($iterator, $callable);
}
$this->clearFilter();
return $iterator;
}
}

View File

@ -0,0 +1,269 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv;
use CallbackFilterIterator;
use InvalidArgumentException;
use Iterator;
use League\Csv\Iterator\MapIterator;
use League\Csv\Iterator\Query;
use LimitIterator;
/**
* A class to manage extracting and filtering a CSV
*
* @package League.csv
* @since 3.0.0
*
*/
class Reader extends AbstractCsv
{
/**
* Iterator Query Trait
*/
use Query;
/**
* {@ihneritdoc}
*/
protected $stream_filter_mode = STREAM_FILTER_READ;
/**
* Return a Filtered Iterator
*
* @param callable $callable a callable function to be applied to each Iterator item
*
* @return \Traversable
*/
public function query(callable $callable = null)
{
$iterator = new CallbackFilterIterator($this->getIterator(), function ($row) {
return is_array($row);
});
$iterator = $this->applyIteratorFilter($iterator);
$iterator = $this->applyIteratorSortBy($iterator);
$iterator = $this->applyIteratorInterval($iterator);
if (! is_null($callable)) {
$iterator = new MapIterator($iterator, $callable);
}
return $iterator;
}
/**
* Apply a callback function on the CSV
*
* The callback function must return TRUE in order to continue
* iterating over the iterator.
*
* @param callable $callable The callback function
*
* @return int the iteration count
*/
public function each(callable $callable)
{
$index = 0;
$iterator = $this->query();
$iterator->rewind();
while ($iterator->valid() && true === $callable($iterator->current(), $iterator->key(), $iterator)) {
++$index;
$iterator->next();
}
return $index;
}
/**
* Return a single column from the CSV data
*
* The callable function will be applied to each value to be return
*
* @param int $column_index field Index
* @param callable $callable a callable function
*
* @throws \InvalidArgumentException If the column index is not a positive integer or 0
*
* @return array
*/
public function fetchColumn($column_index = 0, callable $callable = null)
{
if (false === filter_var($column_index, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
throw new InvalidArgumentException(
'the column index must be a positive integer or 0'
);
}
$iterator = $this->query($callable);
$iterator = new MapIterator($iterator, function ($row) use ($column_index) {
if (! array_key_exists($column_index, $row)) {
return null;
}
return $row[$column_index];
});
return iterator_to_array($iterator, false);
}
/**
* Return a single row from the CSV
*
* @param int $offset
*
* @throws \InvalidArgumentException If the $offset is not a valid Integer
*
* @return array
*/
public function fetchOne($offset = 0)
{
$this->setOffset($offset);
$this->setLimit(1);
$iterator = $this->query();
$iterator->rewind();
return (array) $iterator->current();
}
/**
* Return a sequential array of all CSV lines
*
* The callable function will be applied to each Iterator item
*
* @param callable $callable a callable function
*
* @return array
*/
public function fetchAll(callable $callable = null)
{
return iterator_to_array($this->query($callable), false);
}
/**
* Return a sequential array of all CSV lines;
*
* The rows are presented as associated arrays
* The callable function will be applied to each Iterator item
*
* @param array|int $offset_or_keys the name for each key member OR the row Index to be
* used as the associated named keys
* @param callable $callable a callable function
*
* @throws \InvalidArgumentException If the submitted keys are invalid
*
* @return array
*/
public function fetchAssoc($offset_or_keys = 0, callable $callable = null)
{
$keys = $this->getAssocKeys($offset_or_keys);
$iterator = $this->query($callable);
$iterator = new MapIterator($iterator, function ($row) use ($keys) {
return static::combineArray($keys, $row);
});
return iterator_to_array($iterator, false);
}
/**
* Select the array to be used as key for the fetchAssoc method
* @param array|int $offset_or_keys the assoc key OR the row Index to be used
* as the key index
*
* @throws \InvalidArgumentException If the row index and/or the resulting array is invalid
*
* @return array
*/
protected function getAssocKeys($offset_or_keys)
{
$res = $offset_or_keys;
if (! is_array($res)) {
$res = $this->getRow($offset_or_keys);
$this->addFilter(function ($row, $rowIndex) use ($offset_or_keys) {
return is_array($row) && $rowIndex != $offset_or_keys;
});
}
if (! $this->isValidAssocKeys($res)) {
throw new InvalidArgumentException(
'Use a flat non empty array with unique string values'
);
}
return $res;
}
/**
* Return a single row from the CSV without filtering
*
* @param int $offset
*
* @throws \InvalidArgumentException If the $offset is not valid or the row does not exist
*
* @return array
*/
protected function getRow($offset)
{
if (false === filter_var($offset, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
throw new InvalidArgumentException('the row index must be a positive integer or 0');
}
$iterator = new LimitIterator($this->getIterator(), $offset, 1);
$iterator->rewind();
$res = $iterator->current();
if (is_null($res)) {
throw new InvalidArgumentException('the specified row does not exist');
}
return $res;
}
/**
* Validate the array to be used by the fetchAssoc method
*
* @param array $keys
*
* @return boolean
*/
protected function isValidAssocKeys(array $keys)
{
$validKeys = array_unique(array_filter($keys, function ($value) {
return self::isValidString($value);
}));
return count($keys) && $keys === $validKeys;
}
/**
* Intelligent Array Combine
*
* add or remove values from the $value array to
* match array $keys length before using PHP array_combine function
*
* @param array $keys
* @param array $value
*
* @return array
*/
protected static function combineArray(array $keys, array $value)
{
$nbKeys = count($keys);
$diff = $nbKeys - count($value);
if ($diff > 0) {
$value = array_merge($value, array_fill(0, $diff, null));
} elseif ($diff < 0) {
$value = array_slice($value, 0, $nbKeys);
}
return array_combine($keys, $value);
}
}

View File

@ -0,0 +1,355 @@
<?php
/**
* This file is part of the League.csv library
*
* @license http://opensource.org/licenses/MIT
* @link https://github.com/thephpleague/csv/
* @version 6.3.0
* @package League.csv
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv;
use InvalidArgumentException;
use OutOfBoundsException;
use RuntimeException;
use SplFileObject;
use Traversable;
/**
* A class to manage data insertion into a CSV
*
* @package League.csv
* @since 4.0.0
*
*/
class Writer extends AbstractCsv
{
/**
* set null handling mode to throw exception
*/
const NULL_AS_EXCEPTION = 1;
/**
* set null handling mode to remove cell
*/
const NULL_AS_SKIP_CELL = 2;
/**
* set null handling mode to convert null into empty string
*/
const NULL_AS_EMPTY = 3;
/**
* the object current null handling mode
*
* @var int
*/
protected $null_handling_mode = self::NULL_AS_EXCEPTION;
/**
* The number of column per row
*
* @var int
*/
protected $columns_count = -1;
/**
* should the class detect the column count based the inserted row
*
* @var bool
*/
protected $detect_columns_count = false;
/**
* {@ihneritdoc}
*/
protected $stream_filter_mode = STREAM_FILTER_WRITE;
/**
* The CSV object holder
*
* @var \SplFileObject
*/
protected $csv;
/**
* newline character
*
* @var string
*/
protected $newline = "\n";
/**
* Tell the class how to handle null value
*
* @param int $value a Writer null behavior constant
*
* @throws \OutOfBoundsException If the Integer is not valid
*
* @return static
*/
public function setNullHandlingMode($value)
{
if (!in_array($value, [self::NULL_AS_SKIP_CELL, self::NULL_AS_EXCEPTION, self::NULL_AS_EMPTY])) {
throw new OutOfBoundsException('invalid value for null handling');
}
$this->null_handling_mode = $value;
return $this;
}
/**
* null handling getter
*
* @return int
*/
public function getNullHandlingMode()
{
return $this->null_handling_mode;
}
/**
* set the newline sequence characters
*
* @param string $newline
*
* @return static
*/
public function setNewline($newline)
{
$this->newline = (string) $newline;
return $this;
}
/**
* return the current newline sequence characters
*
* @return string
*/
public function getNewline()
{
return $this->newline;
}
/**
* Set Inserted row column count
*
* @param int $value
*
* @throws \InvalidArgumentException If $value is lesser than -1
*
* @return static
*/
public function setColumnsCount($value)
{
if (false === filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -1]])) {
throw new InvalidArgumentException('the column count must an integer greater or equals to -1');
}
$this->detect_columns_count = false;
$this->columns_count = $value;
return $this;
}
/**
* Column count getter
*
* @return int
*/
public function getColumnsCount()
{
return $this->columns_count;
}
/**
* The method will set the $columns_count property according to the next inserted row
* and therefore will also validate the next line whatever length it has no matter
* the current $columns_count property value.
*
* @return static
*/
public function autodetectColumnsCount()
{
$this->detect_columns_count = true;
return $this;
}
/**
* Add multiple lines to the CSV your are generating
*
* a simple helper/Wrapper method around insertOne
*
* @param \Traversable|array $rows a multidimentional array or a Traversable object
*
* @throws \InvalidArgumentException If the given rows format is invalid
*
* @return static
*/
public function insertAll($rows)
{
if (! is_array($rows) && ! $rows instanceof Traversable) {
throw new InvalidArgumentException(
'the provided data must be an array OR a \Traversable object'
);
}
foreach ($rows as $row) {
$this->insertOne($row);
}
return $this;
}
/**
* Add a new CSV row to the generated CSV
*
* @param string[]|string $data a string, an array or an object implementing to '__toString' method
*
* @throws \InvalidArgumentException If the given row is invalid
*
* @return static
*/
public function insertOne($data)
{
$data = $this->validateRow($data);
$data = $this->sanitizeColumnsContent($data);
if (! $this->isColumnsCountConsistent($data)) {
throw new RuntimeException(
'You are trying to add '.count($data).' columns to a CSV
that requires '.$this->columns_count.' columns per row.'
);
}
$csv = $this->getCsv();
$csv->fputcsv($data, $this->delimiter, $this->enclosure);
if ("\n" !== $this->newline) {
$csv->fseek(-1, SEEK_CUR);
$csv->fwrite($this->newline);
}
return $this;
}
/**
* Is the submitted row valid
*
* @param string[]|string $row
*
* @throws \InvalidArgumentException If the given $row is not valid
*
* @return array
*/
protected function validateRow($row)
{
if (! is_array($row)) {
$row = str_getcsv((string) $row, $this->delimiter, $this->enclosure, $this->escape);
}
array_walk($row, function ($value) {
if (! $this->isConvertibleContent($value)) {
throw new InvalidArgumentException(
'the values are not convertible into strings'
);
}
});
return $row;
}
/**
* Check if a given value can be added into a CSV cell
*
* The value MUST respect the null handling mode
* The valie MUST be convertible into a string
*
* @param string|null $value the value to be added
*
* @return bool
*/
protected function isConvertibleContent($value)
{
return (is_null($value) && self::NULL_AS_EXCEPTION != $this->null_handling_mode)
|| self::isValidString($value);
}
/**
* Format the row according to the null handling behavior
*
* @param array $row
*
* @return array
*/
protected function sanitizeColumnsContent(array $row)
{
if (self::NULL_AS_EXCEPTION == $this->null_handling_mode) {
return $row;
} elseif (self::NULL_AS_EMPTY == $this->null_handling_mode) {
return str_replace(null, '', $row);
}
return array_filter($row, function ($value) {
return !is_null($value);
});
}
/**
* Check column count consistency
*
* @param array $row the row to be added to the CSV
*
* @return bool
*/
protected function isColumnsCountConsistent(array $row)
{
if ($this->detect_columns_count) {
$this->columns_count = count($row);
$this->detect_columns_count = false;
return true;
} elseif (-1 == $this->columns_count) {
return true;
}
return count($row) == $this->columns_count;
}
/**
* set the csv container as a SplFileObject instance
* insure we use the same object for insertion to
* avoid loosing the cursor position
*
* @return \SplFileObject
*/
protected function getCsv()
{
if (is_null($this->csv)) {
$this->csv = $this->getIterator();
}
return $this->csv;
}
/**
* Tells whether the stream filter capabilities can be used
*
* @return bool
*/
public function isActiveStreamFilter()
{
return parent::isActiveStreamFilter() && is_null($this->csv);
}
/**
* The destructor
*/
public function __destruct()
{
$this->csv = null;
parent::__destruct();
}
}