LICENSE 0000666 00000002107 13436752723 0005571 0 ustar 00 The MIT License (MIT)
Copyright (c) 2013-2017 ignace nyamagana butera
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.
autoload.php 0000666 00000000550 13436752723 0007105 0 ustar 00
*/
class HTMLConverter
{
/**
* table class attribute value
*
* @var string
*/
protected $class_name = 'table-csv-data';
/**
* table id attribute value
*
* @var string
*/
protected $id_value = '';
/**
* @var XMLConverter
*/
protected $xml_converter;
/**
* New Instance
*/
public function __construct()
{
$this->xml_converter = (new XMLConverter())
->rootElement('table')
->recordElement('tr')
->fieldElement('td')
;
}
/**
* Convert an Record collection into a DOMDocument
*
* @param array|Traversable $records the tabular data collection
*
* @return string
*/
public function convert($records): string
{
$doc = $this->xml_converter->convert($records);
$doc->documentElement->setAttribute('class', $this->class_name);
$doc->documentElement->setAttribute('id', $this->id_value);
return $doc->saveHTML($doc->documentElement);
}
/**
* HTML table class name setter
*
* @param string $class_name
* @param string $id_value
*
* @throws DOMException if the id_value contains any type of whitespace
*
* @return self
*/
public function table(string $class_name, string $id_value = ''): self
{
if (preg_match(",\s,", $id_value)) {
throw new DOMException("the id attribute's value must not contain whitespace (spaces, tabs etc.)");
}
$clone = clone $this;
$clone->class_name = $class_name;
$clone->id_value = $id_value;
return $clone;
}
/**
* HTML tr record offset attribute setter
*
* @param string $record_offset_attribute_name
*
* @return self
*/
public function tr(string $record_offset_attribute_name): self
{
$clone = clone $this;
$clone->xml_converter = $this->xml_converter->recordElement('tr', $record_offset_attribute_name);
return $clone;
}
/**
* HTML td field name attribute setter
*
* @param string $fieldname_attribute_name
*
* @return self
*/
public function td(string $fieldname_attribute_name): self
{
$clone = clone $this;
$clone->xml_converter = $this->xml_converter->fieldElement('td', $fieldname_attribute_name);
return $clone;
}
}
src/XMLConverter.php 0000666 00000015161 13436752723 0010420 0 ustar 00
*/
class XMLConverter
{
/**
* XML Root name
*
* @var string
*/
protected $root_name = 'csv';
/**
* XML Node name
*
* @var string
*/
protected $record_name = 'row';
/**
* XML Item name
*
* @var string
*/
protected $field_name = 'cell';
/**
* XML column attribute name
*
* @var string
*/
protected $column_attr = '';
/**
* XML offset attribute name
*
* @var string
*/
protected $offset_attr = '';
/**
* Conversion method list
*
* @var array
*/
protected $encoder = [
'field' => [
true => 'fieldToElementWithAttribute',
false => 'fieldToElement',
],
'record' => [
true => 'recordToElementWithAttribute',
false => 'recordToElement',
],
];
/**
* Convert an Record collection into a DOMDocument
*
* @param array|Traversable $records the CSV records collection
*
* @return DOMDocument
*/
public function convert($records): DOMDocument
{
if (!is_iterable($records)) {
throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
}
$field_encoder = $this->encoder['field']['' !== $this->column_attr];
$record_encoder = $this->encoder['record']['' !== $this->offset_attr];
$doc = new DOMDocument('1.0');
$root = $doc->createElement($this->root_name);
foreach ($records as $offset => $record) {
$node = $this->$record_encoder($doc, $record, $field_encoder, $offset);
$root->appendChild($node);
}
$doc->appendChild($root);
return $doc;
}
/**
* Convert a CSV record into a DOMElement and
* adds its offset as DOMElement attribute
*
* @param DOMDocument $doc
* @param array $record CSV record
* @param string $field_encoder CSV Cell encoder method name
* @param int $offset CSV record offset
*
* @return DOMElement
*/
protected function recordToElementWithAttribute(
DOMDocument $doc,
array $record,
string $field_encoder,
int $offset
): DOMElement {
$node = $this->recordToElement($doc, $record, $field_encoder);
$node->setAttribute($this->offset_attr, (string) $offset);
return $node;
}
/**
* Convert a CSV record into a DOMElement
*
* @param DOMDocument $doc
* @param array $record CSV record
* @param string $field_encoder CSV Cell encoder method name
*
* @return DOMElement
*/
protected function recordToElement(DOMDocument $doc, array $record, string $field_encoder): DOMElement
{
$node = $doc->createElement($this->record_name);
foreach ($record as $node_name => $value) {
$item = $this->$field_encoder($doc, $value, $node_name);
$node->appendChild($item);
}
return $node;
}
/**
* Convert Cell to Item.
*
* Convert the CSV item into a DOMElement and adds the item offset
* as attribute to the returned DOMElement
*
* @param DOMDocument $doc
* @param string $value Record item value
* @param int|string $node_name Record item offset
*
* @return DOMElement
*/
protected function fieldToElementWithAttribute(DOMDocument $doc, string $value, $node_name): DOMElement
{
$item = $this->fieldToElement($doc, $value);
$item->setAttribute($this->column_attr, (string) $node_name);
return $item;
}
/**
* Convert Cell to Item
*
* @param DOMDocument $doc
* @param string $value Record item value
*
* @return DOMElement
*/
protected function fieldToElement(DOMDocument $doc, string $value): DOMElement
{
$item = $doc->createElement($this->field_name);
$item->appendChild($doc->createTextNode($value));
return $item;
}
/**
* XML root element setter
*
* @param string $node_name
*
* @return self
*/
public function rootElement(string $node_name): self
{
$clone = clone $this;
$clone->root_name = $this->filterElementName($node_name);
return $clone;
}
/**
* Filter XML element name
*
* @param string $value Element name
*
* @throws DOMException If the Element name is invalid
*
* @return string
*/
protected function filterElementName(string $value): string
{
return (new DOMElement($value))->tagName;
}
/**
* XML Record element setter
*
* @param string $node_name
* @param string $record_offset_attribute_name
*
* @return self
*/
public function recordElement(string $node_name, string $record_offset_attribute_name = ''): self
{
$clone = clone $this;
$clone->record_name = $this->filterElementName($node_name);
$clone->offset_attr = $this->filterAttributeName($record_offset_attribute_name);
return $clone;
}
/**
* Filter XML attribute name
*
* @param string $value Element name
*
* @throws DOMException If the Element attribute name is invalid
*
* @return string
*/
protected function filterAttributeName(string $value): string
{
if ('' === $value) {
return $value;
}
return (new DOMAttr($value))->name;
}
/**
* XML Field element setter
*
* @param string $node_name
* @param string $fieldname_attribute_name
*
* @return self
*/
public function fieldElement(string $node_name, string $fieldname_attribute_name = ''): self
{
$clone = clone $this;
$clone->field_name = $this->filterElementName($node_name);
$clone->column_attr = $this->filterAttributeName($fieldname_attribute_name);
return $clone;
}
}
src/MapIterator.php 0000666 00000002313 13436752723 0010312 0 ustar 00
* @internal used internally to modify CSV content
*/
class MapIterator extends IteratorIterator
{
/**
* The callback to apply on all InnerIterator current value
*
* @var callable
*/
protected $callable;
/**
* The Constructor
*
* @param Traversable $iterator
* @param callable $callable
*/
public function __construct(Traversable $iterator, callable $callable)
{
parent::__construct($iterator);
$this->callable = $callable;
}
/**
* {@inheritdoc}
*/
public function current()
{
return ($this->callable)(parent::current(), $this->key());
}
}
src/Exception.php 0000666 00000001041 13436752723 0010016 0 ustar 00
*/
class Exception extends \Exception
{
}
src/EscapeFormula.php 0000666 00000007376 13436752723 0010627 0 ustar 00
*/
class EscapeFormula
{
/**
* Spreadsheet formula starting character
*/
const FORMULA_STARTING_CHARS = ['=', '-', '+', '@'];
/**
* Effective Spreadsheet formula starting characters
*
* @var array
*/
protected $special_chars = [];
/**
* Escape character to escape each CSV formula field
*
* @var string
*/
protected $escape;
/**
* New instance
*
* @param string $escape escape character to escape each CSV formula field
* @param string[] $special_chars additional spreadsheet formula starting characters
*
*/
public function __construct(string $escape = "\t", array $special_chars = [])
{
$this->escape = $escape;
if (!empty($special_chars)) {
$special_chars = $this->filterSpecialCharacters(...$special_chars);
}
$chars = array_merge(self::FORMULA_STARTING_CHARS, $special_chars);
$chars = array_unique($chars);
$this->special_chars = array_fill_keys($chars, 1);
}
/**
* Filter submitted special characters.
*
* @param string ...$characters
*
* @throws InvalidArgumentException if the string is not a single character
*
* @return string[]
*/
protected function filterSpecialCharacters(string ...$characters): array
{
foreach ($characters as $str) {
if (1 != strlen($str)) {
throw new InvalidArgumentException(sprintf('The submitted string %s must be a single character', $str));
}
}
return $characters;
}
/**
* Returns the list of character the instance will escape.
*
* @return string[]
*/
public function getSpecialCharacters(): array
{
return array_keys($this->special_chars);
}
/**
* Returns the escape character.
*
* @return string
*/
public function getEscape(): string
{
return $this->escape;
}
/**
* League CSV formatter hook.
*
* @see escapeRecord
*
* @param array $record
*
* @return array
*/
public function __invoke(array $record): array
{
return $this->escapeRecord($record);
}
/**
* Escape a CSV record.
*
* @param array $record
*
* @return array
*/
public function escapeRecord(array $record): array
{
return array_map([$this, 'escapeField'], $record);
}
/**
* Escape a CSV cell.
*
* @param mixed $cell
*
* @return mixed
*/
protected function escapeField($cell)
{
if (!$this->isStringable($cell)) {
return $cell;
}
$str_cell = (string) $cell;
if (isset($str_cell[0], $this->special_chars[$str_cell[0]])) {
return $this->escape.$str_cell;
}
return $cell;
}
/**
* Tell whether the submitted value is stringable.
*
* @param mixed $value
*
* @return bool
*/
protected function isStringable($value): bool
{
return is_string($value) || (is_object($value) && method_exists($value, '__toString'));
}
}
src/ResultSet.php 0000666 00000015252 13436752723 0010023 0 ustar 00
*/
class ResultSet implements Countable, IteratorAggregate, JsonSerializable
{
/**
* The CSV records collection
*
* @var Iterator
*/
protected $records;
/**
* The CSV records collection header
*
* @var array
*/
protected $header = [];
/**
* New instance
*
* @param Iterator $records a CSV records collection iterator
* @param array $header the associated collection column names
*/
public function __construct(Iterator $records, array $header)
{
$this->records = $records;
$this->header = $header;
}
/**
* {@inheritdoc}
*/
public function __destruct()
{
unset($this->records);
}
/**
* Returns the header associated with the result set
*
* @return string[]
*/
public function getHeader(): array
{
return $this->header;
}
/**
* {@inheritdoc}
*/
public function getRecords(): Generator
{
foreach ($this->records as $offset => $value) {
yield $offset => $value;
}
}
/**
* {@inheritdoc}
*/
public function getIterator(): Generator
{
return $this->getRecords();
}
/**
* {@inheritdoc}
*/
public function count(): int
{
return iterator_count($this->records);
}
/**
* {@inheritdoc}
*/
public function jsonSerialize(): array
{
return iterator_to_array($this->records, false);
}
/**
* Returns the nth record from the result set
*
* By default if no index is provided the first record of the resultet is returned
*
* @param int $nth_record the CSV record offset
*
* @throws Exception if argument is lesser than 0
*
* @return array
*/
public function fetchOne(int $nth_record = 0): array
{
if ($nth_record < 0) {
throw new Exception(sprintf('%s() expects the submitted offset to be a positive integer or 0, %s given', __METHOD__, $nth_record));
}
$iterator = new LimitIterator($this->records, $nth_record, 1);
$iterator->rewind();
return (array) $iterator->current();
}
/**
* Returns a single column from the next record of the result set
*
* By default if no value is supplied the first column is fetch
*
* @param string|int $index CSV column index
*
* @return Generator
*/
public function fetchColumn($index = 0): Generator
{
$offset = $this->getColumnIndex($index, __METHOD__.'() expects the column index to be a valid string or integer, `%s` given');
$filter = function (array $record) use ($offset): bool {
return isset($record[$offset]);
};
$select = function (array $record) use ($offset): string {
return $record[$offset];
};
$iterator = new MapIterator(new CallbackFilterIterator($this->records, $filter), $select);
foreach ($iterator as $offset => $value) {
yield $offset => $value;
}
}
/**
* Filter a column name against the header if any
*
* @param string|int $field the field name or the field index
* @param string $error_message the associated error message
*
* @return string|int
*/
protected function getColumnIndex($field, string $error_message)
{
$method = is_string($field) ? 'getColumnIndexByValue' : 'getColumnIndexByKey';
return $this->$method($field, $error_message);
}
/**
* Returns the selected column name
*
* @param string $value
* @param string $error_message
*
* @throws Exception if the column is not found
*
* @return string
*/
protected function getColumnIndexByValue(string $value, string $error_message): string
{
if (false !== array_search($value, $this->header, true)) {
return $value;
}
throw new Exception(sprintf($error_message, $value));
}
/**
* Returns the selected column name according to its offset
*
* @param int $index
* @param string $error_message
*
* @throws Exception if the field is invalid or not found
*
* @return int|string
*/
protected function getColumnIndexByKey(int $index, string $error_message)
{
if ($index < 0) {
throw new Exception($error_message);
}
if (empty($this->header)) {
return $index;
}
$value = array_search($index, array_flip($this->header), true);
if (false !== $value) {
return $value;
}
throw new Exception(sprintf($error_message, $index));
}
/**
* Returns the next key-value pairs from a result set (first
* column is the key, second column is the value).
*
* By default if no column index is provided:
* - the first column is used to provide the keys
* - the second column is used to provide the value
*
* @param string|int $offset_index The column index to serve as offset
* @param string|int $value_index The column index to serve as value
*
* @return Generator
*/
public function fetchPairs($offset_index = 0, $value_index = 1): Generator
{
$offset = $this->getColumnIndex($offset_index, __METHOD__.'() expects the offset index value to be a valid string or integer, `%s` given');
$value = $this->getColumnIndex($value_index, __METHOD__.'() expects the value index value to be a valid string or integer, `%s` given');
$filter = function (array $record) use ($offset): bool {
return isset($record[$offset]);
};
$select = function (array $record) use ($offset, $value): array {
return [$record[$offset], $record[$value] ?? null];
};
$iterator = new MapIterator(new CallbackFilterIterator($this->records, $filter), $select);
foreach ($iterator as $pair) {
yield $pair[0] => $pair[1];
}
}
}
src/Stream.php 0000666 00000027334 13436752723 0007330 0 ustar 00
* @internal used internally to iterate over a stream resource
*/
class Stream implements SeekableIterator
{
/**
* Attached filters
*
* @var resource[]
*/
protected $filters = [];
/**
* stream resource
*
* @var resource
*/
protected $stream;
/**
* Tell whether the stream should be closed on object destruction
*
* @var bool
*/
protected $should_close_stream = false;
/**
* Current iterator value
*
* @var mixed
*/
protected $value;
/**
* Current iterator key
*
* @var int
*/
protected $offset;
/**
* Flags for the Document
*
* @var int
*/
protected $flags = 0;
/**
* 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 = '\\';
/**
* Tell whether the current stream is seekable;
*
* @var bool
*/
protected $is_seekable = false;
/**
* New instance
*
* @param resource $resource stream type resource
*/
public function __construct($resource)
{
if (!is_resource($resource)) {
throw new TypeError(sprintf('Argument passed must be a stream resource, %s given', gettype($resource)));
}
if ('stream' !== ($type = get_resource_type($resource))) {
throw new TypeError(sprintf('Argument passed must be a stream resource, %s resource given', $type));
}
$this->is_seekable = stream_get_meta_data($resource)['seekable'];
$this->stream = $resource;
}
/**
* {@inheritdoc}
*/
public function __destruct()
{
$walker = function ($filter): bool {
return stream_filter_remove($filter);
};
array_walk_recursive($this->filters, $walker);
if ($this->should_close_stream && is_resource($this->stream)) {
fclose($this->stream);
}
unset($this->stream);
}
/**
* {@inheritdoc}
*/
public function __clone()
{
throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
}
/**
* {@inheritdoc}
*/
public function __debugInfo()
{
return stream_get_meta_data($this->stream) + [
'delimiter' => $this->delimiter,
'enclosure' => $this->enclosure,
'escape' => $this->escape,
'stream_filters' => array_keys($this->filters),
];
}
/**
* Return a new instance from a file path
*
* @param string $path file path
* @param string $open_mode the file open mode flag
* @param resource|null $context the resource context
*
* @throws Exception if the stream resource can not be created
*
* @return static
*/
public static function createFromPath(string $path, string $open_mode = 'r', $context = null): self
{
$args = [$path, $open_mode];
if (null !== $context) {
$args[] = false;
$args[] = $context;
}
if (!$resource = @fopen(...$args)) {
throw new Exception(sprintf('`%s`: failed to open stream: No such file or directory', $path));
}
$instance = new static($resource);
$instance->should_close_stream = true;
return $instance;
}
/**
* Return a new instance from a string
*
* @param string $content the CSV document as a string
*
* @return static
*/
public static function createFromString(string $content): self
{
$resource = fopen('php://temp', 'r+');
fwrite($resource, $content);
$instance = new static($resource);
$instance->should_close_stream = true;
return $instance;
}
/**
* append a filter
*
* @see http://php.net/manual/en/function.stream-filter-append.php
*
* @param string $filtername
* @param int $read_write
* @param mixed $params
*
* @throws Exception if the filter can not be appended
*/
public function appendFilter(string $filtername, int $read_write, $params = null)
{
$res = @stream_filter_append($this->stream, $filtername, $read_write, $params);
if (is_resource($res)) {
$this->filters[$filtername][] = $res;
return;
}
throw new Exception(sprintf('unable to locate filter `%s`', $filtername));
}
/**
* Set CSV control
*
* @see http://php.net/manual/en/splfileobject.setcsvcontrol.php
*
* @param string $delimiter
* @param string $enclosure
* @param string $escape
*/
public function setCsvControl(string $delimiter = ',', string $enclosure = '"', string $escape = '\\')
{
list($this->delimiter, $this->enclosure, $this->escape) = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
}
/**
* Filter Csv control characters
*
* @param string $delimiter CSV delimiter character
* @param string $enclosure CSV enclosure character
* @param string $escape CSV escape character
* @param string $caller caller
*
* @throws Exception If the Csv control character is not one character only.
*
* @return array
*/
protected function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array
{
$controls = ['delimiter' => $delimiter, 'enclosure' => $enclosure, 'escape' => $escape];
foreach ($controls as $type => $control) {
if (1 !== strlen($control)) {
throw new Exception(sprintf('%s() expects %s to be a single character', $caller, $type));
}
}
return array_values($controls);
}
/**
* Set CSV control
*
* @see http://php.net/manual/en/splfileobject.getcsvcontrol.php
*
* @return string[]
*/
public function getCsvControl()
{
return [$this->delimiter, $this->enclosure, $this->escape];
}
/**
* Set CSV stream flags
*
* @see http://php.net/manual/en/splfileobject.setflags.php
*
* @param int $flags
*/
public function setFlags(int $flags)
{
$this->flags = $flags;
}
/**
* Write a field array as a CSV line
*
* @see http://php.net/manual/en/splfileobject.fputcsv.php
*
* @param array $fields
* @param string $delimiter
* @param string $enclosure
* @param string $escape
*
* @return int|bool
*/
public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\')
{
$controls = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
return fputcsv($this->stream, $fields, ...$controls);
}
/**
* Get line number
*
* @see http://php.net/manual/en/splfileobject.key.php
*
* @return int
*/
public function key()
{
return $this->offset;
}
/**
* Read next line
*
* @see http://php.net/manual/en/splfileobject.next.php
*
*/
public function next()
{
$this->value = false;
$this->offset++;
}
/**
* Rewind the file to the first line
*
* @see http://php.net/manual/en/splfileobject.rewind.php
*
* @throws Exception if the stream resource is not seekable
*/
public function rewind()
{
if (!$this->is_seekable) {
throw new Exception('stream does not support seeking');
}
rewind($this->stream);
$this->offset = 0;
$this->value = false;
if ($this->flags & SplFileObject::READ_AHEAD) {
$this->current();
}
}
/**
* Not at EOF
*
* @see http://php.net/manual/en/splfileobject.valid.php
*
* @return bool
*/
public function valid()
{
if ($this->flags & SplFileObject::READ_AHEAD) {
return $this->current() !== false;
}
return !feof($this->stream);
}
/**
* Retrieves the current line of the file.
*
* @see http://php.net/manual/en/splfileobject.current.php
*
* @return mixed
*/
public function current()
{
if (false !== $this->value) {
return $this->value;
}
$this->value = $this->getCurrentRecord();
return $this->value;
}
/**
* Retrieves the current line as a CSV Record
*
* @return array|bool
*/
protected function getCurrentRecord()
{
do {
$ret = fgetcsv($this->stream, 0, $this->delimiter, $this->enclosure, $this->escape);
} while ($this->flags & SplFileObject::SKIP_EMPTY && $ret !== false && $ret[0] === null);
return $ret;
}
/**
* Seek to specified line
*
* @see http://php.net/manual/en/splfileobject.seek.php
*
* @param int $position
* @throws Exception if the position is negative
*/
public function seek($position)
{
if ($position < 0) {
throw new Exception(sprintf('%s() can\'t seek stream to negative line %d', __METHOD__, $position));
}
$this->rewind();
while ($this->key() !== $position && $this->valid()) {
$this->current();
$this->next();
}
$this->offset--;
$this->current();
}
/**
* Output all remaining data on a file pointer
*
* @see http://php.net/manual/en/splfileobject.fpatssthru.php
*
* @return int
*/
public function fpassthru()
{
return fpassthru($this->stream);
}
/**
* Read from file
*
* @see http://php.net/manual/en/splfileobject.fread.php
*
* @param int $length The number of bytes to read
*
* @return string|false
*/
public function fread($length)
{
return fread($this->stream, $length);
}
/**
* Seek to a position
*
* @see http://php.net/manual/en/splfileobject.fseek.php
*
* @param int $offset
* @param int $whence
*
* @throws Exception if the stream resource is not seekable
*
* @return int
*/
public function fseek(int $offset, int $whence = SEEK_SET)
{
if (!$this->is_seekable) {
throw new Exception('stream does not support seeking');
}
return fseek($this->stream, $offset, $whence);
}
/**
* Write to stream
*
* @see http://php.net/manual/en/splfileobject.fwrite.php
*
* @param string $str
* @param int $length
*
* @return int|bool
*/
public function fwrite(string $str, int $length = 0)
{
return fwrite($this->stream, $str, $length);
}
/**
* Flushes the output to a file
*
* @see http://php.net/manual/en/splfileobject.fwrite.php
*
* @return bool
*/
public function fflush()
{
return fflush($this->stream);
}
}
src/Reader.php 0000666 00000022465 13436752723 0007277 0 ustar 00 header_offset;
}
/**
* Returns the CSV record used as header
*
* The returned header is represented as an array of string values
*
* @return string[]
*/
public function getHeader(): array
{
if (null === $this->header_offset) {
return $this->header;
}
if (!empty($this->header)) {
return $this->header;
}
$this->header = $this->setHeader($this->header_offset);
return $this->header;
}
/**
* Determine the CSV record header
*
* @param int $offset
*
* @throws Exception If the header offset is set and no record is found or is the empty array
*
* @return string[]
*/
protected function setHeader(int $offset): array
{
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$this->document->seek($offset);
if (empty($header = $this->document->current())) {
throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset));
}
if (0 === $offset) {
return $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
}
return $header;
}
/**
* Strip the BOM sequence from a record
*
* @param string[] $record
* @param int $bom_length
* @param string $enclosure
*
* @return string[]
*/
protected function removeBOM(array $record, int $bom_length, string $enclosure): array
{
if (0 == $bom_length) {
return $record;
}
$record[0] = mb_substr($record[0], $bom_length);
if ($enclosure.$enclosure != substr($record[0].$record[0], strlen($record[0]) - 1, 2)) {
return $record;
}
$record[0] = substr($record[0], 1, -1);
return $record;
}
/**
* {@inheritdoc}
*/
public function __call($method, array $arguments)
{
static $whitelisted = ['fetchColumn' => 1, 'fetchOne' => 1, 'fetchPairs' => 1];
if (isset($whitelisted[$method])) {
return (new ResultSet($this->getRecords(), $this->getHeader()))->$method(...$arguments);
}
throw new BadMethodCallException(sprintf('%s::%s() method does not exist', __CLASS__, $method));
}
/**
* {@inheritdoc}
*/
public function count(): int
{
if (-1 === $this->nb_records) {
$this->nb_records = iterator_count($this->getRecords());
}
return $this->nb_records;
}
/**
* {@inheritdoc}
*/
public function getIterator(): Iterator
{
return $this->getRecords();
}
/**
* {@inheritdoc}
*/
public function jsonSerialize(): array
{
return iterator_to_array($this->getRecords(), false);
}
/**
* Returns the CSV records as an iterator object.
*
* Each CSV record is represented as a simple array containig strings or null values.
*
* If the CSV document has a header record then each record is combined
* to the header record and the header record is removed from the iterator.
*
* If the CSV document is inconsistent. Missing record fields are
* filled with null values while extra record fields are strip from
* the returned object.
*
* @param string[] $header an optional header to use instead of the CSV document header
*
* @return Iterator
*/
public function getRecords(array $header = []): Iterator
{
$header = $this->computeHeader($header);
$normalized = function ($record): bool {
return is_array($record) && $record != [null];
};
$bom = $this->getInputBOM();
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$records = $this->stripBOM(new CallbackFilterIterator($this->document, $normalized), $bom);
if (null !== $this->header_offset) {
$records = new CallbackFilterIterator($records, function (array $record, int $offset): bool {
return $offset !== $this->header_offset;
});
}
return $this->combineHeader($records, $header);
}
/**
* Returns the header to be used for iteration
*
* @param string[] $header
*
* @throws Exception If the header contains non unique column name
*
* @return string[]
*/
protected function computeHeader(array $header)
{
if (empty($header)) {
$header = $this->getHeader();
}
if ($header === array_unique(array_filter($header, 'is_string'))) {
return $header;
}
throw new Exception('The header record must be empty or a flat array with unique string values');
}
/**
* Combine the CSV header to each record if present
*
* @param Iterator $iterator
* @param string[] $header
*
* @return Iterator
*/
protected function combineHeader(Iterator $iterator, array $header): Iterator
{
if (empty($header)) {
return $iterator;
}
$field_count = count($header);
$mapper = function (array $record) use ($header, $field_count): array {
if (count($record) != $field_count) {
$record = array_slice(array_pad($record, $field_count, null), 0, $field_count);
}
return array_combine($header, $record);
};
return new MapIterator($iterator, $mapper);
}
/**
* Strip the BOM sequence from the returned records if necessary
*
* @param Iterator $iterator
* @param string $bom
*
* @return Iterator
*/
protected function stripBOM(Iterator $iterator, string $bom): Iterator
{
if ('' === $bom) {
return $iterator;
}
$bom_length = mb_strlen($bom);
$mapper = function (array $record, int $index) use ($bom_length): array {
if (0 != $index) {
return $record;
}
return $this->removeBOM($record, $bom_length, $this->enclosure);
};
return new MapIterator($iterator, $mapper);
}
/**
* Selects the record to be used as the CSV header
*
* Because the header is represented as an array, to be valid
* a header MUST contain only unique string value.
*
* @param int|null $offset the header record offset
*
* @throws Exception if the offset is a negative integer
*
* @return static
*/
public function setHeaderOffset($offset): self
{
if ($offset === $this->header_offset) {
return $this;
}
if (!is_nullable_int($offset)) {
throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($offset)));
}
if (null !== $offset && 0 > $offset) {
throw new Exception(__METHOD__.'() expects 1 Argument to be greater or equal to 0');
}
$this->header_offset = $offset;
$this->resetProperties();
return $this;
}
/**
* {@inheritdoc}
*/
protected function resetProperties()
{
$this->nb_records = -1;
$this->header = [];
}
}
src/functions_include.php 0000666 00000000233 13436752723 0011575 0 ustar 00
*/
abstract class AbstractCsv implements ByteSequence
{
/**
* The stream filter mode (read or write)
*
* @var int
*/
protected $stream_filter_mode;
/**
* collection of stream filters
*
* @var bool[]
*/
protected $stream_filters = [];
/**
* The CSV document BOM sequence
*
* @var string|null
*/
protected $input_bom = null;
/**
* The Output file BOM character
*
* @var string
*/
protected $output_bom = '';
/**
* 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 CSV document
*
* @var SplFileObject|Stream
*/
protected $document;
/**
* New instance
*
* @param SplFileObject|Stream $document The CSV Object instance
*/
protected function __construct($document)
{
$this->document = $document;
list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl();
}
/**
* {@inheritdoc}
*/
public function __destruct()
{
unset($this->document);
}
/**
* {@inheritdoc}
*/
public function __clone()
{
throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
}
/**
* Return a new instance from a SplFileObject
*
* @param SplFileObject $file
*
* @return static
*/
public static function createFromFileObject(SplFileObject $file): self
{
return new static($file);
}
/**
* Return a new instance from a PHP resource stream
*
* @param resource $stream
*
* @return static
*/
public static function createFromStream($stream): self
{
return new static(new Stream($stream));
}
/**
* Return a new instance from a string
*
* @param string $content the CSV document as a string
*
* @return static
*/
public static function createFromString(string $content): self
{
return new static(Stream::createFromString($content));
}
/**
* Return a new instance from a file path
*
* @param string $path file path
* @param string $open_mode the file open mode flag
* @param resource|null $context the resource context
*
* @return static
*/
public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): self
{
return new static(Stream::createFromPath($path, $open_mode, $context));
}
/**
* Returns the current field delimiter
*
* @return string
*/
public function getDelimiter(): string
{
return $this->delimiter;
}
/**
* Returns the current field enclosure
*
* @return string
*/
public function getEnclosure(): string
{
return $this->enclosure;
}
/**
* Returns the current field escape character
*
* @return string
*/
public function getEscape(): string
{
return $this->escape;
}
/**
* Returns the BOM sequence in use on Output methods
*
* @return string
*/
public function getOutputBOM(): string
{
return $this->output_bom;
}
/**
* Returns the BOM sequence of the given CSV
*
* @return string
*/
public function getInputBOM(): string
{
if (null !== $this->input_bom) {
return $this->input_bom;
}
$this->document->setFlags(SplFileObject::READ_CSV);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$this->document->rewind();
$this->input_bom = bom_match(implode(',', (array) $this->document->current()));
return $this->input_bom;
}
/**
* Returns the stream filter mode
*
* @return int
*/
public function getStreamFilterMode(): int
{
return $this->stream_filter_mode;
}
/**
* Tells whether the stream filter capabilities can be used
*
* @return bool
*/
public function supportsStreamFilter(): bool
{
return $this->document instanceof Stream;
}
/**
* Tell whether the specify stream filter is attach to the current stream
*
* @param string $filtername
*
* @return bool
*/
public function hasStreamFilter(string $filtername): bool
{
return $this->stream_filters[$filtername] ?? false;
}
/**
* Retuns the CSV document as a Generator of string chunk
*
* @param int $length number of bytes read
*
* @throws Exception if the number of bytes is lesser than 1
*
* @return Generator
*/
public function chunk(int $length): Generator
{
if ($length < 1) {
throw new Exception(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
}
$input_bom = $this->getInputBOM();
$this->document->rewind();
$this->document->fseek(strlen($input_bom));
foreach (str_split($this->output_bom.$this->document->fread($length), $length) as $chunk) {
yield $chunk;
}
while ($this->document->valid()) {
yield $this->document->fread($length);
}
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release
*
* @deprecated deprecated since version 9.1.0
* @see AbstractCsv::getContent
*
* Retrieves the CSV content
*
* @return string
*/
public function __toString(): string
{
return $this->getContent();
}
/**
* Retrieves the CSV content
*
* @return string
*/
public function getContent(): string
{
$raw = '';
foreach ($this->chunk(8192) as $chunk) {
$raw .= $chunk;
}
return $raw;
}
/**
* Outputs all data on the CSV file
*
* @param string $filename CSV downloaded name if present adds extra headers
*
* @return int Returns the number of characters read from the handle
* and passed through to the output.
*/
public function output(string $filename = null): int
{
if (null !== $filename) {
$this->sendHeaders($filename);
}
$input_bom = $this->getInputBOM();
$this->document->rewind();
$this->document->fseek(strlen($input_bom));
echo $this->output_bom;
return strlen($this->output_bom) + $this->document->fpassthru();
}
/**
* Send the CSV headers
*
* Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
*
* @param string|null $filename CSV disposition name
*
* @throws Exception if the submitted header is invalid according to RFC 6266
*
* @see https://tools.ietf.org/html/rfc6266#section-4.3
*/
protected function sendHeaders(string $filename)
{
if (strlen($filename) != strcspn($filename, '\\/')) {
throw new Exception('The filename cannot contain the "/" and "\\" characters.');
}
$flag = FILTER_FLAG_STRIP_LOW;
if (strlen($filename) !== mb_strlen($filename)) {
$flag |= FILTER_FLAG_STRIP_HIGH;
}
$filenameFallback = str_replace('%', '', filter_var($filename, FILTER_SANITIZE_STRING, $flag));
$disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
if ($filename !== $filenameFallback) {
$disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
}
header('Content-Type: text/csv');
header('Content-Transfer-Encoding: binary');
header('Content-Description: File Transfer');
header('Content-Disposition: '.$disposition);
}
/**
* Sets the field delimiter
*
* @param string $delimiter
*
* @throws Exception If the Csv control character is not one character only.
*
* @return static
*/
public function setDelimiter(string $delimiter): self
{
if ($delimiter === $this->delimiter) {
return $this;
}
if (1 === strlen($delimiter)) {
$this->delimiter = $delimiter;
$this->resetProperties();
return $this;
}
throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
}
/**
* Reset dynamic object properties to improve performance
*/
protected function resetProperties()
{
}
/**
* Sets the field enclosure
*
* @param string $enclosure
*
* @throws Exception If the Csv control character is not one character only.
*
* @return static
*/
public function setEnclosure(string $enclosure): self
{
if ($enclosure === $this->enclosure) {
return $this;
}
if (1 === strlen($enclosure)) {
$this->enclosure = $enclosure;
$this->resetProperties();
return $this;
}
throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
}
/**
* Sets the field escape character
*
* @param string $escape
*
* @throws Exception If the Csv control character is not one character only.
*
* @return static
*/
public function setEscape(string $escape): self
{
if ($escape === $this->escape) {
return $this;
}
if (1 === strlen($escape)) {
$this->escape = $escape;
$this->resetProperties();
return $this;
}
throw new Exception(sprintf('%s() expects escape to be a single character %s given', __METHOD__, $escape));
}
/**
* Sets the BOM sequence to prepend the CSV on output
*
* @param string $str The BOM sequence
*
* @return static
*/
public function setOutputBOM(string $str): self
{
$this->output_bom = $str;
return $this;
}
/**
* append a stream filter
*
* @param string $filtername a string or an object that implements the '__toString' method
* @param mixed $params additional parameters for the filter
*
* @throws Exception If the stream filter API can not be used
*
* @return static
*/
public function addStreamFilter(string $filtername, $params = null): self
{
if (!$this->document instanceof Stream) {
throw new Exception('The stream filter API can not be used');
}
$this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
$this->stream_filters[$filtername] = true;
$this->resetProperties();
$this->input_bom = null;
return $this;
}
}
src/CannotInsertRecord.php 0000666 00000003634 13436752723 0011640 0 ustar 00
*/
class CannotInsertRecord extends Exception
{
/**
* The record submitted for insertion
*
* @var array
*/
protected $record;
/**
* Validator which did not validated the data
*
* @var string
*/
protected $name = '';
/**
* Create an Exception from a record insertion into a stream
*
* @param string[] $record
*
* @return self
*/
public static function triggerOnInsertion(array $record): self
{
$exception = new static('Unable to write record to the CSV document');
$exception->record = $record;
return $exception;
}
/**
* Create an Exception from a Record Validation
*
* @param string $name validator name
* @param string[] $record invalid data
*
* @return self
*/
public static function triggerOnValidation(string $name, array $record): self
{
$exception = new static('Record validation failed');
$exception->name = $name;
$exception->record = $record;
return $exception;
}
/**
* return the validator name
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* return the invalid data submitted
*
* @return array
*/
public function getRecord(): array
{
return $this->record;
}
}
src/ByteSequence.php 0000666 00000001674 13436752723 0010470 0 ustar 00
*/
interface ByteSequence
{
/**
* 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-32 BE BOM sequence
*/
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
/**
* UTF-32 LE BOM sequence
*/
const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
}
src/Statement.php 0000666 00000010562 13436752723 0010034 0 ustar 00
*/
class Statement
{
/**
* Callables to filter the iterator
*
* @var callable[]
*/
protected $where = [];
/**
* Callables to sort the iterator
*
* @var callable[]
*/
protected $order_by = [];
/**
* iterator Offset
*
* @var int
*/
protected $offset = 0;
/**
* iterator maximum length
*
* @var int
*/
protected $limit = -1;
/**
* Set the Iterator filter method
*
* @param callable $callable
*
* @return self
*/
public function where(callable $callable): self
{
$clone = clone $this;
$clone->where[] = $callable;
return $clone;
}
/**
* Set an Iterator sorting callable function
*
* @param callable $callable
*
* @return self
*/
public function orderBy(callable $callable): self
{
$clone = clone $this;
$clone->order_by[] = $callable;
return $clone;
}
/**
* Set LimitIterator Offset
*
* @param $offset
*
* @throws Exception if the offset is lesser than 0
*
* @return self
*/
public function offset(int $offset): self
{
if (0 > $offset) {
throw new Exception(sprintf('%s() expects the offset to be a positive integer or 0, %s given', __METHOD__, $offset));
}
if ($offset === $this->offset) {
return $this;
}
$clone = clone $this;
$clone->offset = $offset;
return $clone;
}
/**
* Set LimitIterator Count
*
* @param int $limit
*
* @throws Exception if the limit is lesser than -1
*
* @return self
*/
public function limit(int $limit): self
{
if (-1 > $limit) {
throw new Exception(sprintf('%s() expects the limit to be greater or equel to -1, %s given', __METHOD__, $limit));
}
if ($limit === $this->limit) {
return $this;
}
$clone = clone $this;
$clone->limit = $limit;
return $clone;
}
/**
* Execute the prepared Statement on the {@link Reader} object
*
* @param Reader $csv
* @param string[] $header an optional header to use instead of the CSV document header
*
* @return ResultSet
*/
public function process(Reader $csv, array $header = []): ResultSet
{
if (empty($header)) {
$header = $csv->getHeader();
}
$iterator = array_reduce($this->where, [$this, 'filter'], $csv->getRecords($header));
$iterator = $this->buildOrderBy($iterator);
return new ResultSet(new LimitIterator($iterator, $this->offset, $this->limit), $header);
}
/**
* Filters elements of an Iterator using a callback function
*
* @param Iterator $iterator
* @param callable $callable
*
* @return CallbackFilterIterator
*/
protected function filter(Iterator $iterator, callable $callable): CallbackFilterIterator
{
return new CallbackFilterIterator($iterator, $callable);
}
/**
* Sort the Iterator
*
* @param Iterator $iterator
*
* @return Iterator
*/
protected function buildOrderBy(Iterator $iterator): Iterator
{
if (empty($this->order_by)) {
return $iterator;
}
$compare = function (array $record_a, array $record_b): int {
foreach ($this->order_by as $callable) {
if (0 !== ($cmp = $callable($record_a, $record_b))) {
return $cmp;
}
}
return $cmp ?? 0;
};
$iterator = new ArrayIterator(iterator_to_array($iterator));
$iterator->uasort($compare);
return $iterator;
}
}
src/Writer.php 0000666 00000013626 13436752723 0007350 0 ustar 00
*/
class Writer extends AbstractCsv
{
/**
* callable collection to format the record before insertion
*
* @var callable[]
*/
protected $formatters = [];
/**
* callable collection to validate the record before insertion
*
* @var callable[]
*/
protected $validators = [];
/**
* newline character
*
* @var string
*/
protected $newline = "\n";
/**
* Insert records count for flushing
*
* @var int
*/
protected $flush_counter = 0;
/**
* Buffer flush threshold
*
* @var int|null
*/
protected $flush_threshold;
/**
* {@inheritdoc}
*/
protected $stream_filter_mode = STREAM_FILTER_WRITE;
/**
* Returns the current newline sequence characters
*
* @return string
*/
public function getNewline(): string
{
return $this->newline;
}
/**
* Get the flush threshold
*
* @return int|null
*/
public function getFlushThreshold()
{
return $this->flush_threshold;
}
/**
* Adds multiple records to the CSV document
*
* a simple wrapper method around insertOne
*
* @param Traversable|array $records a multidimensional array or a Traversable object
*
* @return int
*/
public function insertAll($records): int
{
if (!is_iterable($records)) {
throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
}
$bytes = 0;
foreach ($records as $record) {
$bytes += $this->insertOne($record);
}
$this->flush_counter = 0;
$this->document->fflush();
return $bytes;
}
/**
* Adds a single record to a CSV document
*
* @param string[] $record an array
*
* @throws CannotInsertRecord If the record can not be inserted
*
* @return int
*/
public function insertOne(array $record): int
{
$record = array_reduce($this->formatters, [$this, 'formatRecord'], $record);
$this->validateRecord($record);
$bytes = $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
if (!$bytes) {
throw CannotInsertRecord::triggerOnInsertion($record);
}
return $bytes + $this->consolidate();
}
/**
* Format a record
*
* @param string[] $record
* @param callable $formatter
*
* @return string[]
*/
protected function formatRecord(array $record, callable $formatter): array
{
return $formatter($record);
}
/**
* Validate a record
*
* @param string[] $record
*
* @throws CannotInsertRecord If the validation failed
*/
protected function validateRecord(array $record)
{
foreach ($this->validators as $name => $validator) {
if (true !== $validator($record)) {
throw CannotInsertRecord::triggerOnValidation($name, $record);
}
}
}
/**
* Apply post insertion actions
*
* @return int
*/
protected function consolidate(): int
{
$bytes = 0;
if ("\n" !== $this->newline) {
$this->document->fseek(-1, SEEK_CUR);
$bytes = $this->document->fwrite($this->newline, strlen($this->newline)) - 1;
}
if (null === $this->flush_threshold) {
return $bytes;
}
++$this->flush_counter;
if (0 === $this->flush_counter % $this->flush_threshold) {
$this->flush_counter = 0;
$this->document->fflush();
}
return $bytes;
}
/**
* Adds a record formatter
*
* @param callable $formatter
*
* @return static
*/
public function addFormatter(callable $formatter): self
{
$this->formatters[] = $formatter;
return $this;
}
/**
* Adds a record validator
*
* @param callable $validator
* @param string $validator_name the validator name
*
* @return static
*/
public function addValidator(callable $validator, string $validator_name): self
{
$this->validators[$validator_name] = $validator;
return $this;
}
/**
* Sets the newline sequence
*
* @param string $newline
*
* @return static
*/
public function setNewline(string $newline): self
{
$this->newline = $newline;
return $this;
}
/**
* Set the flush threshold
*
* @param int|null $threshold
*
* @throws Exception if the threshold is a integer lesser than 1
*
* @return static
*/
public function setFlushThreshold($threshold): self
{
if ($threshold === $this->flush_threshold) {
return $this;
}
if (!is_nullable_int($threshold)) {
throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($threshold)));
}
if (null !== $threshold && 1 >= $threshold) {
throw new Exception(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1');
}
$this->flush_threshold = $threshold;
$this->flush_counter = 0;
$this->document->fflush();
return $this;
}
}
src/functions.php 0000666 00000005445 13436752723 0010104 0 ustar 00 getConstants();
foreach ($list as $sequence) {
if (0 === strpos($str, $sequence)) {
return $sequence;
}
}
return '';
}
/**
* Detect Delimiters usage in a {@link Reader} object
*
* Returns a associative array where each key represents
* a submitted delimiter and each value the number CSV fields found
* when processing at most $limit CSV records with the given delimiter
*
* @param Reader $csv the CSV object
* @param string[] $delimiters list of delimiters to consider
* @param int $limit Detection is made using up to $limit records
*
* @return int[]
*/
function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array
{
$found = array_unique(array_filter($delimiters, function (string $value): bool {
return 1 == strlen($value);
}));
$stmt = (new Statement())->limit($limit)->where(function (array $record): bool {
return count($record) > 1;
});
$reducer = function (array $result, string $delimiter) use ($csv, $stmt): array {
$result[$delimiter] = count(iterator_to_array($stmt->process($csv->setDelimiter($delimiter)), false), COUNT_RECURSIVE);
return $result;
};
$delimiter = $csv->getDelimiter();
$header_offset = $csv->getHeaderOffset();
$csv->setHeaderOffset(null);
$stats = array_reduce($found, $reducer, array_fill_keys($delimiters, 0));
$csv->setHeaderOffset($header_offset)->setDelimiter($delimiter);
return $stats;
}
if (!function_exists('\is_iterable')) {
/**
* Tell whether the content of the variable is iterable
*
* @see http://php.net/manual/en/function.is-iterable.php
*
* @param mixed $iterable
*
* @return bool
*/
function is_iterable($iterable): bool
{
return is_array($iterable) || $iterable instanceof Traversable;
}
}
/**
* Tell whether the content of the variable is an int or null
*
* @see https://wiki.php.net/rfc/nullable_types
*
* @param mixed $value
*
* @return bool
*/
function is_nullable_int($value): bool
{
return null === $value || is_int($value);
}
src/CharsetConverter.php 0000666 00000015152 13436752723 0011351 0 ustar 00
*/
class CharsetConverter extends php_user_filter
{
const FILTERNAME = 'convert.league.csv';
/**
* the filter name used to instantiate the class with
*
* @var string
*/
public $filtername;
/**
* Contents of the params parameter passed to stream_filter_append
* or stream_filter_prepend functions
*
* @var mixed
*/
public $params;
/**
* The records input encoding charset
*
* @var string
*/
protected $input_encoding = 'UTF-8';
/**
* The records output encoding charset
*
* @var string
*/
protected $output_encoding = 'UTF-8';
/**
* Static method to add the stream filter to a {@link AbstractCsv} object
*
* @param AbstractCsv $csv
* @param string $input_encoding
* @param string $output_encoding
*
* @return AbstractCsv
*/
public static function addTo(AbstractCsv $csv, string $input_encoding, string $output_encoding): AbstractCsv
{
self::register();
return $csv->addStreamFilter(self::getFiltername($input_encoding, $output_encoding));
}
/**
* Static method to register the class as a stream filter
*/
public static function register()
{
$filtername = self::FILTERNAME.'.*';
if (!in_array($filtername, stream_get_filters())) {
stream_filter_register($filtername, __CLASS__);
}
}
/**
* Static method to return the stream filter filtername
*
* @param string $input_encoding
* @param string $output_encoding
*
* @return string
*/
public static function getFiltername(string $input_encoding, string $output_encoding): string
{
return sprintf(
'%s.%s/%s',
self::FILTERNAME,
self::filterEncoding($input_encoding),
self::filterEncoding($output_encoding)
);
}
/**
* Filter encoding charset
*
* @param string $encoding
*
* @throws OutOfRangeException if the charset is malformed or unsupported
*
* @return string
*/
protected static function filterEncoding(string $encoding): string
{
static $encoding_list;
if (null === $encoding_list) {
$list = mb_list_encodings();
$encoding_list = array_combine(array_map('strtolower', $list), $list);
}
$key = strtolower($encoding);
if (isset($encoding_list[$key])) {
return $encoding_list[$key];
}
throw new OutOfRangeException(sprintf('The submitted charset %s is not supported by the mbstring extension', $encoding));
}
/**
* {@inheritdoc}
*/
public function onCreate()
{
$prefix = self::FILTERNAME.'.';
if (0 !== strpos($this->filtername, $prefix)) {
return false;
}
$encodings = substr($this->filtername, strlen($prefix));
if (!preg_match(',^(?[-\w]+)\/(?