new appraoch

This commit is contained in:
Tim Bendt
2025-11-26 13:22:58 -05:00
parent de3d100844
commit c520b7df89
6760 changed files with 1009780 additions and 0 deletions

20
pancake/system/vendor/league/csv/LICENSE vendored Executable file
View File

@@ -0,0 +1,20 @@
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.

18
pancake/system/vendor/league/csv/autoload.php vendored Executable file
View File

@@ -0,0 +1,18 @@
<?php
require __DIR__.'/src/functions_include.php';
spl_autoload_register(function ($class) {
$prefix = 'League\Csv\\';
if (0 !== strpos($class, $prefix)) {
return;
}
$file = __DIR__.'/src/'.str_replace('\\', '/', substr($class, strlen($prefix))).'.php';
if (!is_readable($file)) {
return;
}
require $file;
});

View File

@@ -0,0 +1,82 @@
{
"name": "league/csv",
"type": "library",
"description" : "CSV data manipulation made easy in PHP",
"keywords": ["csv", "import", "export", "read", "write", "filter", "convert", "transform"],
"license": "MIT",
"homepage" : "https://csv.thephpleague.com",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://github.com/nyamsprod/",
"role" : "Developer"
}
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php" : "^7.4 || ^8.0",
"ext-json" : "*",
"ext-mbstring" : "*"
},
"require-dev": {
"ext-curl" : "*",
"ext-dom": "*",
"friendsofphp/php-cs-fixer": "^v3.4.0",
"phpunit/phpunit" : "^9.5.11",
"phpstan/phpstan": "^1.3.0",
"phpstan/phpstan-strict-rules": "^1.1.0",
"phpstan/phpstan-phpunit": "^1.0.0"
},
"autoload": {
"psr-4": {
"League\\Csv\\": "src"
},
"files": ["src/functions_include.php"]
},
"autoload-dev": {
"psr-4": {
"LeagueTest\\Csv\\": "tests"
}
},
"scripts": {
"phpcs": "php-cs-fixer fix -vvv --diff --dry-run --allow-risky=yes --ansi",
"phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi",
"phpstan": "phpstan analyse -l max -c phpstan.neon src --ansi --memory-limit=192M",
"phpunit": "phpunit --coverage-text",
"test": [
"@phpunit",
"@phpstan",
"@phpcs"
]
},
"scripts-descriptions": {
"phpcs": "Runs coding style test suite",
"phpstan": "Runs complete codebase static analysis",
"phpunit": "Runs unit and functional testing",
"test": "Runs full test suite"
},
"suggest": {
"ext-iconv" : "Needed to ease transcoding CSV using iconv stream filters",
"ext-dom" : "Required to use the XMLConverter and or the HTMLConverter classes"
},
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"config": {
"sort-packages": true
}
}

View File

@@ -0,0 +1,492 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Generator;
use SplFileObject;
use function filter_var;
use function get_class;
use function mb_strlen;
use function rawurlencode;
use function sprintf;
use function str_replace;
use function str_split;
use function strcspn;
use function strlen;
use const FILTER_FLAG_STRIP_HIGH;
use const FILTER_FLAG_STRIP_LOW;
use const FILTER_UNSAFE_RAW;
/**
* An abstract class to enable CSV document loading.
*/
abstract class AbstractCsv implements ByteSequence
{
protected const STREAM_FILTER_MODE = STREAM_FILTER_READ;
/** @var SplFileObject|Stream The CSV document. */
protected $document;
/** @var array<string, bool> collection of stream filters. */
protected array $stream_filters = [];
protected ?string $input_bom = null;
protected string $output_bom = '';
protected string $delimiter = ',';
protected string $enclosure = '"';
protected string $escape = '\\';
protected bool $is_input_bom_included = false;
/**
* @final This method should not be overwritten in child classes
*
* @param SplFileObject|Stream $document The CSV Object instance
*/
protected function __construct($document)
{
$this->document = $document;
[$this->delimiter, $this->enclosure, $this->escape] = $this->document->getCsvControl();
$this->resetProperties();
}
/**
* Reset dynamic object properties to improve performance.
*/
abstract protected function resetProperties(): void;
public function __destruct()
{
unset($this->document);
}
public function __clone()
{
throw UnavailableStream::dueToForbiddenCloning(static::class);
}
/**
* Return a new instance from a SplFileObject.
*
* @return static
*/
public static function createFromFileObject(SplFileObject $file)
{
return new static($file);
}
/**
* Return a new instance from a PHP resource stream.
*
* @param resource $stream
*
* @return static
*/
public static function createFromStream($stream)
{
return new static(new Stream($stream));
}
/**
* Return a new instance from a string.
*
* @return static
*/
public static function createFromString(string $content = '')
{
return new static(Stream::createFromString($content));
}
/**
* Return a new instance from a file path.
*
* @param resource|null $context the resource context
*
* @return static
*/
public static function createFromPath(string $path, string $open_mode = 'r+', $context = null)
{
return new static(Stream::createFromPath($path, $open_mode, $context));
}
/**
* Returns the current field delimiter.
*/
public function getDelimiter(): string
{
return $this->delimiter;
}
/**
* Returns the current field enclosure.
*/
public function getEnclosure(): string
{
return $this->enclosure;
}
/**
* Returns the pathname of the underlying document.
*/
public function getPathname(): string
{
return $this->document->getPathname();
}
/**
* Returns the current field escape character.
*/
public function getEscape(): string
{
return $this->escape;
}
/**
* Returns the BOM sequence in use on Output methods.
*/
public function getOutputBOM(): string
{
return $this->output_bom;
}
/**
* Returns the BOM sequence of the given CSV.
*/
public function getInputBOM(): string
{
if (null !== $this->input_bom) {
return $this->input_bom;
}
$this->document->setFlags(SplFileObject::READ_CSV);
$this->document->rewind();
$this->input_bom = Info::fetchBOMSequence((string) $this->document->fread(4)) ?? '';
return $this->input_bom;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see AbstractCsv::supportsStreamFilterOnRead
* @see AbstractCsv::supportsStreamFilterOnWrite
*
* Returns the stream filter mode.
*/
public function getStreamFilterMode(): int
{
return static::STREAM_FILTER_MODE;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see AbstractCsv::supportsStreamFilterOnRead
* @see AbstractCsv::supportsStreamFilterOnWrite
*
* Tells whether the stream filter capabilities can be used.
*/
public function supportsStreamFilter(): bool
{
return $this->document instanceof Stream;
}
/**
* Tells whether the stream filter read capabilities can be used.
*/
public function supportsStreamFilterOnRead(): bool
{
return $this->document instanceof Stream
&& (static::STREAM_FILTER_MODE & STREAM_FILTER_READ) === STREAM_FILTER_READ;
}
/**
* Tells whether the stream filter write capabilities can be used.
*/
public function supportsStreamFilterOnWrite(): bool
{
return $this->document instanceof Stream
&& (static::STREAM_FILTER_MODE & STREAM_FILTER_WRITE) === STREAM_FILTER_WRITE;
}
/**
* Tell whether the specify stream filter is attach to the current stream.
*/
public function hasStreamFilter(string $filtername): bool
{
return $this->stream_filters[$filtername] ?? false;
}
/**
* Tells whether the BOM can be stripped if presents.
*/
public function isInputBOMIncluded(): bool
{
return $this->is_input_bom_included;
}
/**
* Returns 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
*/
public function chunk(int $length): Generator
{
if ($length < 1) {
throw InvalidArgument::dueToInvalidChunkSize($length, __METHOD__);
}
$input_bom = $this->getInputBOM();
$this->document->rewind();
$this->document->setFlags(0);
$this->document->fseek(strlen($input_bom));
/** @var array<int, string> $chunks */
$chunks = str_split($this->output_bom.$this->document->fread($length), $length);
foreach ($chunks 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 since version 9.1.0
* @see AbstractCsv::toString
*
* Retrieves the CSV content
*/
public function __toString(): string
{
return $this->toString();
}
/**
* Retrieves the CSV content.
*
* DEPRECATION WARNING! This method will be removed in the next major point release
*
* @deprecated since version 9.7.0
* @see AbstractCsv::toString
*/
public function getContent(): string
{
return $this->toString();
}
/**
* Retrieves the CSV content.
*
* @throws Exception If the string representation can not be returned
*/
public function toString(): string
{
$raw = '';
foreach ($this->chunk(8192) as $chunk) {
$raw .= $chunk;
}
return $raw;
}
/**
* Outputs all data on the CSV file.
*
* @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);
}
$this->document->rewind();
if (!$this->is_input_bom_included) {
$this->document->fseek(strlen($this->getInputBOM()));
}
echo $this->output_bom;
return strlen($this->output_bom) + (int) $this->document->fpassthru();
}
/**
* Send the CSV headers.
*
* Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
*
* @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): void
{
if (strlen($filename) != strcspn($filename, '\\/')) {
throw InvalidArgument::dueToInvalidHeaderFilename($filename);
}
$flag = FILTER_FLAG_STRIP_LOW;
if (strlen($filename) !== mb_strlen($filename)) {
$flag |= FILTER_FLAG_STRIP_HIGH;
}
/** @var string $filtered_name */
$filtered_name = filter_var($filename, FILTER_UNSAFE_RAW, $flag);
$filename_fallback = str_replace('%', '', $filtered_name);
$disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filename_fallback));
if ($filename !== $filename_fallback) {
$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.
*
* @throws InvalidArgument 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)) {
throw InvalidArgument::dueToInvalidDelimiterCharacter($delimiter, __METHOD__);
}
$this->delimiter = $delimiter;
$this->resetProperties();
return $this;
}
/**
* Sets the field enclosure.
*
* @throws InvalidArgument 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)) {
throw InvalidArgument::dueToInvalidEnclosureCharacter($enclosure, __METHOD__);
}
$this->enclosure = $enclosure;
$this->resetProperties();
return $this;
}
/**
* Sets the field escape character.
*
* @throws InvalidArgument 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 ('' !== $escape && 1 !== strlen($escape)) {
throw InvalidArgument::dueToInvalidEscapeCharacter($escape, __METHOD__);
}
$this->escape = $escape;
$this->resetProperties();
return $this;
}
/**
* Enables BOM Stripping.
*
* @return static
*/
public function skipInputBOM(): self
{
$this->is_input_bom_included = false;
return $this;
}
/**
* Disables skipping Input BOM.
*
* @return static
*/
public function includeInputBOM(): self
{
$this->is_input_bom_included = true;
return $this;
}
/**
* Sets the BOM sequence to prepend the CSV on output.
*
* @return static
*/
public function setOutputBOM(string $str): self
{
$this->output_bom = $str;
return $this;
}
/**
* append a stream filter.
*
* @param null|array $params
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature 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 UnavailableFeature::dueToUnsupportedStreamFilterApi(get_class($this->document));
}
$this->document->appendFilter($filtername, static::STREAM_FILTER_MODE, $params);
$this->stream_filters[$filtername] = true;
$this->resetProperties();
$this->input_bom = null;
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Csv;
/**
* Defines constants for common BOM sequences.
*/
interface ByteSequence
{
const BOM_UTF8 = "\xEF\xBB\xBF";
const BOM_UTF16_BE = "\xFE\xFF";
const BOM_UTF16_LE = "\xFF\xFE";
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
/**
* Thrown when a data is not added to the Csv Document.
*/
class CannotInsertRecord extends Exception
{
/** The record submitted for insertion. */
protected array $record;
/** Validator which did not validated the data. */
protected string $name = '';
/**
* Create an Exception from a record insertion into a stream.
*/
public static function triggerOnInsertion(array $record): self
{
$exception = new self('Unable to write record to the CSV document');
$exception->record = $record;
return $exception;
}
/**
* Create an Exception from a Record Validation.
*/
public static function triggerOnValidation(string $name, array $record): self
{
$exception = new self('Record validation failed');
$exception->name = $name;
$exception->record = $record;
return $exception;
}
/**
* return the validator name.
*
*/
public function getName(): string
{
return $this->name;
}
/**
* return the invalid data submitted.
*
*/
public function getRecord(): array
{
return $this->record;
}
}

View File

@@ -0,0 +1,220 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use OutOfRangeException;
use php_user_filter;
use Traversable;
use function array_combine;
use function array_map;
use function in_array;
use function is_numeric;
use function mb_convert_encoding;
use function mb_list_encodings;
use function preg_match;
use function sprintf;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_filter_register;
use function stream_get_filters;
use function strpos;
use function strtolower;
use function substr;
/**
* Converts resource stream or tabular data content charset.
*/
class CharsetConverter extends php_user_filter
{
const FILTERNAME = 'convert.league.csv';
protected string $input_encoding = 'UTF-8';
protected string $output_encoding = 'UTF-8';
/**
* Static method to add the stream filter to a {@link AbstractCsv} object.
*/
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(): void
{
$filter_name = self::FILTERNAME.'.*';
if (!in_array($filter_name, stream_get_filters(), true)) {
stream_filter_register($filter_name, self::class);
}
}
/**
* Static method to return the stream filter filtername.
*/
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.
*
* @throws OutOfRangeException if the charset is malformed or unsupported
*/
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('The submitted charset '.$encoding.' is not supported by the mbstring extension.');
}
public function onCreate(): bool
{
$prefix = self::FILTERNAME.'.';
if (0 !== strpos($this->filtername, $prefix)) {
return false;
}
$encodings = substr($this->filtername, strlen($prefix));
if (1 !== preg_match(',^(?<input>[-\w]+)\/(?<output>[-\w]+)$,', $encodings, $matches)) {
return false;
}
try {
$this->input_encoding = self::filterEncoding($matches['input']);
$this->output_encoding = self::filterEncoding($matches['output']);
} catch (OutOfRangeException $e) {
return false;
}
return true;
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*/
public function filter($in, $out, &$consumed, $closing): int
{
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$bucket->data = @mb_convert_encoding($bucket->data, $this->output_encoding, $this->input_encoding);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
/**
* Convert Csv records collection into UTF-8.
*/
public function convert(iterable $records): iterable
{
if ($this->output_encoding === $this->input_encoding) {
return $records;
}
if (is_array($records)) {
return array_map($this, $records);
}
/* @var Traversable $records */
return new MapIterator($records, $this);
}
/**
* Enable using the class as a formatter for the {@link Writer}.
*/
public function __invoke(array $record): array
{
$outputRecord = [];
foreach ($record as $offset => $value) {
[$newOffset, $newValue] = $this->encodeField($value, $offset);
$outputRecord[$newOffset] = $newValue;
}
return $outputRecord;
}
/**
* Walker method to convert the offset and the value of a CSV record field.
*
* @param int|float|string|null $value can be a scalar type or null
* @param int|string $offset can be a string or an int
*/
protected function encodeField($value, $offset): array
{
if (null !== $value && !is_numeric($value)) {
$value = mb_convert_encoding($value, $this->output_encoding, $this->input_encoding);
}
if (!is_numeric($offset)) {
$offset = mb_convert_encoding($offset, $this->output_encoding, $this->input_encoding);
}
return [$offset, $value];
}
/**
* Sets the records input encoding charset.
*/
public function inputEncoding(string $encoding): self
{
$encoding = self::filterEncoding($encoding);
if ($encoding === $this->input_encoding) {
return $this;
}
$clone = clone $this;
$clone->input_encoding = $encoding;
return $clone;
}
/**
* Sets the records output encoding charset.
*/
public function outputEncoding(string $encoding): self
{
$encoding = self::filterEncoding($encoding);
if ($encoding === $this->output_encoding) {
return $this;
}
$clone = clone $this;
$clone->output_encoding = $encoding;
return $clone;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use function count;
/**
* Validates column consistency when inserting records into a CSV document.
*/
class ColumnConsistency
{
protected int $columns_count;
/**
* @throws InvalidArgument if the column count is lesser than -1
*/
public function __construct(int $columns_count = -1)
{
if ($columns_count < -1) {
throw InvalidArgument::dueToInvalidColumnCount($columns_count, __METHOD__);
}
$this->columns_count = $columns_count;
}
/**
* Returns the column count.
*/
public function getColumnCount(): int
{
return $this->columns_count;
}
/**
* Tell whether the submitted record is valid.
*/
public function __invoke(array $record): bool
{
$count = count($record);
if (-1 === $this->columns_count) {
$this->columns_count = $count;
return true;
}
return $count === $this->columns_count;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use InvalidArgumentException;
use php_user_filter;
use function array_map;
use function in_array;
use function str_replace;
use function strcspn;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_filter_register;
use function stream_get_filters;
use function strlen;
/**
* A stream filter to improve enclosure character usage.
*
* @see https://tools.ietf.org/html/rfc4180#section-2
* @see https://bugs.php.net/bug.php?id=38301
*/
class EncloseField extends php_user_filter
{
const FILTERNAME = 'convert.league.csv.enclosure';
/** Default sequence. */
protected string $sequence;
/** Characters that triggers enclosure in PHP. */
protected static string $force_enclosure = "\n\r\t ";
/**
* Static method to return the stream filter filtername.
*/
public static function getFiltername(): string
{
return self::FILTERNAME;
}
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
if (!in_array(self::FILTERNAME, stream_get_filters(), true)) {
stream_filter_register(self::FILTERNAME, self::class);
}
}
/**
* Static method to add the stream filter to a {@link Writer} object.
*
* @throws InvalidArgumentException if the sequence is malformed
* @throws Exception
*/
public static function addTo(Writer $csv, string $sequence): Writer
{
self::register();
if (!self::isValidSequence($sequence)) {
throw new InvalidArgumentException('The sequence must contain at least one character to force enclosure');
}
return $csv
->addFormatter(fn (array $record): array => array_map(fn (?string $value): string => $sequence.$value, $record))
->addStreamFilter(self::FILTERNAME, ['sequence' => $sequence]);
}
/**
* Filter type and sequence parameters.
*
* The sequence to force enclosure MUST contains one of the following character ("\n\r\t ")
*/
protected static function isValidSequence(string $sequence): bool
{
return strlen($sequence) != strcspn($sequence, self::$force_enclosure);
}
public function onCreate(): bool
{
return isset($this->params['sequence'])
&& self::isValidSequence($this->params['sequence']);
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*/
public function filter($in, $out, &$consumed, $closing): int
{
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$bucket->data = str_replace($this->params['sequence'], '', $bucket->data);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use InvalidArgumentException;
use function array_fill_keys;
use function array_keys;
use function array_map;
use function array_merge;
use function array_unique;
use function is_object;
use function is_string;
use function method_exists;
/**
* A Formatter to tackle CSV Formula Injection.
*
* @see http://georgemauer.net/2017/10/07/csv-injection.html
*/
class EscapeFormula
{
/** Spreadsheet formula starting character. */
const FORMULA_STARTING_CHARS = ['=', '-', '+', '@', "\t", "\r"];
/** Effective Spreadsheet formula starting characters. */
protected array $special_chars = [];
/** Escape character to escape each CSV formula field. */
protected string $escape;
/**
* @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 = "'", array $special_chars = [])
{
$this->escape = $escape;
if ([] !== $special_chars) {
$special_chars = $this->filterSpecialCharacters(...$special_chars);
}
$chars = array_unique(array_merge(self::FORMULA_STARTING_CHARS, $special_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 array<string>
*/
protected function filterSpecialCharacters(string ...$characters): array
{
foreach ($characters as $str) {
if (1 != strlen($str)) {
throw new InvalidArgumentException('The submitted string '.$str.' must be a single character');
}
}
return $characters;
}
/**
* Returns the list of character the instance will escape.
*
* @return array<string>
*/
public function getSpecialCharacters(): array
{
return array_keys($this->special_chars);
}
/**
* Returns the escape character.
*/
public function getEscape(): string
{
return $this->escape;
}
/**
* League CSV formatter hook.
*
* @see escapeRecord
*/
public function __invoke(array $record): array
{
return $this->escapeRecord($record);
}
/**
* Escape a CSV record.
*/
public function escapeRecord(array $record): array
{
return array_map([$this, 'escapeField'], $record);
}
/**
* Escape a CSV cell if its content is stringable.
*
* @param int|float|string|object|resource|array $cell the content of the cell
*
* @return mixed the escaped content
*/
protected function escapeField($cell)
{
if (!is_string($cell) && (!is_object($cell) || !method_exists($cell, '__toString'))) {
return $cell;
}
$str_cell = (string) $cell;
if (isset($str_cell[0], $this->special_chars[$str_cell[0]])) {
return $this->escape.$str_cell;
}
return $cell;
}
/**
* @deprecated since 9.7.2 will be removed in the next major release
* @codeCoverageIgnore
*
* Tells whether the submitted value is stringable.
*
* @param mixed $value value to check if it is stringable
*/
protected function isStringable($value): bool
{
return is_string($value)
|| (is_object($value) && method_exists($value, '__toString'));
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Exception as PhpException;
/**
* League Csv Base Exception.
*/
class Exception extends PhpException implements UnableToProcessCsv
{
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use DOMDocument;
use DOMElement;
use DOMException;
use function preg_match;
/**
* Converts tabular data into an HTML Table string.
*/
class HTMLConverter
{
/** table class attribute value. */
protected string $class_name = 'table-csv-data';
/** table id attribute value. */
protected string $id_value = '';
protected XMLConverter $xml_converter;
public static function create(): self
{
return new self();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see HTMLConverterTest::create()
*/
public function __construct()
{
$this->xml_converter = XMLConverter::create()
->rootElement('table')
->recordElement('tr')
->fieldElement('td')
;
}
/**
* Converts a tabular data collection into a HTML table string.
*
* @param string[] $header_record An optional array of headers outputted using the`<thead>` section
* @param string[] $footer_record An optional array of footers to output to the table using `<tfoot>` and `<th>` elements
*/
public function convert(iterable $records, array $header_record = [], array $footer_record = []): string
{
$doc = new DOMDocument('1.0');
if ([] === $header_record && [] === $footer_record) {
$table = $this->xml_converter->import($records, $doc);
$this->addHTMLAttributes($table);
$doc->appendChild($table);
/** @var string $content */
$content = $doc->saveHTML();
return $content;
}
$table = $doc->createElement('table');
$this->addHTMLAttributes($table);
$this->appendHeaderSection('thead', $header_record, $table);
$this->appendHeaderSection('tfoot', $footer_record, $table);
$table->appendChild($this->xml_converter->rootElement('tbody')->import($records, $doc));
$doc->appendChild($table);
/** @var string $content */
$content = $doc->saveHTML();
return $content;
}
/**
* Creates a DOMElement representing a HTML table heading section.
*/
protected function appendHeaderSection(string $node_name, array $record, DOMElement $table): void
{
if ([] === $record) {
return;
}
/** @var DOMDocument $ownerDocument */
$ownerDocument = $table->ownerDocument;
$node = $this->xml_converter
->rootElement($node_name)
->recordElement('tr')
->fieldElement('th')
->import([$record], $ownerDocument)
;
/** @var DOMElement $element */
foreach ($node->getElementsByTagName('th') as $element) {
$element->setAttribute('scope', 'col');
}
$table->appendChild($node);
}
/**
* Adds class and id attributes to an HTML tag.
*/
protected function addHTMLAttributes(DOMElement $node): void
{
$node->setAttribute('class', $this->class_name);
$node->setAttribute('id', $this->id_value);
}
/**
* HTML table class name setter.
*
* @throws DOMException if the id_value contains any type of whitespace
*/
public function table(string $class_name, string $id_value = ''): self
{
if (1 === 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.
*/
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.
*/
public function td(string $fieldname_attribute_name): self
{
$clone = clone $this;
$clone->xml_converter = $this->xml_converter->fieldElement('td', $fieldname_attribute_name);
return $clone;
}
}

99
pancake/system/vendor/league/csv/src/Info.php vendored Executable file
View File

@@ -0,0 +1,99 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use function array_fill_keys;
use function array_filter;
use function array_reduce;
use function array_unique;
use function count;
use function iterator_to_array;
use function strlen;
use function strpos;
use const COUNT_RECURSIVE;
final class Info implements ByteSequence
{
private const BOM_SEQUENCE_LIST = [
self::BOM_UTF32_BE,
self::BOM_UTF32_LE,
self::BOM_UTF16_BE,
self::BOM_UTF16_LE,
self::BOM_UTF8,
];
/**
* Returns the BOM sequence found at the start of the string.
*
* If no valid BOM sequence is found an empty string is returned
*/
public static function fetchBOMSequence(string $str): ?string
{
foreach (self::BOM_SEQUENCE_LIST as $sequence) {
if (0 === strpos($str, $sequence)) {
return $sequence;
}
}
return null;
}
/**
* 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 string[] $delimiters
*
* @return array<string, int>
*/
public static function getDelimiterStats(Reader $csv, array $delimiters, int $limit = 1): array
{
$delimiterFilter = static fn (string $value): bool => 1 === strlen($value);
$recordFilter = static fn (array $record): bool => 1 < count($record);
$stmt = Statement::create()->offset(0)->limit($limit);
$delimiterStats = static function (array $stats, string $delimiter) use ($csv, $stmt, $recordFilter): array {
$csv->setDelimiter($delimiter);
$foundRecords = array_filter(
iterator_to_array($stmt->process($csv)->getRecords(), false),
$recordFilter
);
$stats[$delimiter] = count($foundRecords, COUNT_RECURSIVE);
return $stats;
};
$currentDelimiter = $csv->getDelimiter();
$currentHeaderOffset = $csv->getHeaderOffset();
$csv->setHeaderOffset(null);
$stats = array_reduce(
array_unique(array_filter($delimiters, $delimiterFilter)),
$delimiterStats,
array_fill_keys($delimiters, 0)
);
$csv->setHeaderOffset($currentHeaderOffset);
$csv->setDelimiter($currentDelimiter);
return $stats;
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Throwable;
/**
* InvalidArgument Exception.
*/
class InvalidArgument extends Exception
{
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
*/
public function __construct(string $message = '', int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function dueToInvalidChunkSize(int $length, string $method): self
{
return new self($method.'() expects the length to be a positive integer '.$length.' given.');
}
public static function dueToInvalidHeaderFilename(string $filename): self
{
return new self('The filename `'.$filename.'` cannot contain the "/" and "\\" characters.');
}
public static function dueToInvalidDelimiterCharacter(string $delimiter, string $method): self
{
return new self($method.'() expects delimiter to be a single character; `'.$delimiter.'` given.');
}
public static function dueToInvalidEnclosureCharacter(string $enclosure, string $method): self
{
return new self($method.'() expects enclosure to be a single character; `'.$enclosure.'` given.');
}
public static function dueToInvalidEscapeCharacter(string $escape, string $method): self
{
return new self($method.'() expects escape to be a single character or the empty string; `'.$escape.'` given.');
}
public static function dueToInvalidColumnCount(int $columns_count, string $method): self
{
return new self($method.'() expects the column count to be greater or equal to -1 '.$columns_count.' given.');
}
public static function dueToInvalidHeaderOffset(int $offset, string $method): self
{
return new self($method.'() expects header offset to be greater or equal to 0; `'.$offset.'` given.');
}
public static function dueToInvalidRecordOffset(int $offset, string $method): self
{
return new self($method.'() expects the submitted offset to be a positive integer or 0, '.$offset.' given');
}
/**
* @param string|int $index
*/
public static function dueToInvalidColumnIndex($index, string $type, string $method): self
{
return new self($method.'() expects the '.$type.' index to be a valid string or integer, `'.$index.'` given');
}
public static function dueToInvalidLimit(int $limit, string $method): self
{
return new self($method.'() expects the limit to be greater or equal to -1, '.$limit.' given.');
}
public static function dueToInvalidSeekingPosition(int $position, string $method): self
{
return new self($method.'() can\'t seek stream to negative line '.$position);
}
public static function dueToStreamFilterNotFound(string $filtername): self
{
return new self('unable to locate filter `'.$filtername.'`');
}
public static function dueToInvalidThreshold(int $threshold, string $method): self
{
return new self($method.'() expects threshold to be null or a valid integer greater or equal to 1');
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use IteratorIterator;
use ReturnTypeWillChange;
use Traversable;
/**
* Map value from an iterator before yielding.
*
* @internal used internally to modify CSV content
*/
final class MapIterator extends IteratorIterator
{
/** @var callable The callback to apply on all InnerIterator current value. */
private $callable;
public function __construct(Traversable $iterator, callable $callable)
{
parent::__construct($iterator);
$this->callable = $callable;
}
/**
* @return mixed The value of the current element.
*/
#[ReturnTypeWillChange]
public function current()
{
return ($this->callable)(parent::current(), parent::key());
}
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use InvalidArgumentException;
use php_user_filter;
use function array_map;
use function in_array;
use function is_string;
use function str_replace;
use function strcspn;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_filter_register;
use function stream_get_filters;
use function strlen;
use const STREAM_FILTER_READ;
use const STREAM_FILTER_WRITE;
/**
* A stream filter to conform the CSV field to RFC4180.
*
* DEPRECATION WARNING! This class will be removed in the next major point release
*
* @deprecated since version 9.2.0
* @see AbstractCsv::setEscape
*
* @see https://tools.ietf.org/html/rfc4180#section-2
*/
class RFC4180Field extends php_user_filter
{
public const FILTERNAME = 'convert.league.csv.rfc4180';
/**
* The value being search for.
*
* @var string[]
*/
protected array $search;
/**
* The replacement value that replace found $search values.
*
* @var string[]
*/
protected array $replace;
/**
* Characters that triggers enclosure with PHP fputcsv.
*
*/
protected static string $force_enclosure = "\n\r\t ";
/**
* Static method to add the stream filter to a {@link AbstractCsv} object.
*/
public static function addTo(AbstractCsv $csv, string $whitespace_replace = ''): AbstractCsv
{
self::register();
$params = [
'enclosure' => $csv->getEnclosure(),
'escape' => $csv->getEscape(),
'mode' => $csv->getStreamFilterMode(),
];
if ($csv instanceof Writer && '' != $whitespace_replace) {
self::addFormatterTo($csv, $whitespace_replace);
$params['whitespace_replace'] = $whitespace_replace;
}
return $csv->addStreamFilter(self::FILTERNAME, $params);
}
/**
* Add a formatter to the {@link Writer} object to format the record
* field to avoid enclosure around a field with an empty space.
*/
public static function addFormatterTo(Writer $csv, string $whitespace_replace): Writer
{
if ('' == $whitespace_replace || strlen($whitespace_replace) != strcspn($whitespace_replace, self::$force_enclosure)) {
throw new InvalidArgumentException('The sequence contains a character that enforces enclosure or is a CSV control character or is the empty string.');
}
$mapper = fn ($value) => is_string($value)
? str_replace(' ', $whitespace_replace, $value)
: $value;
return $csv->addFormatter(fn (array $record): array => array_map($mapper, $record));
}
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
if (!in_array(self::FILTERNAME, stream_get_filters(), true)) {
stream_filter_register(self::FILTERNAME, self::class);
}
}
/**
* Static method to return the stream filter filtername.
*/
public static function getFiltername(): string
{
return self::FILTERNAME;
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*/
public function filter($in, $out, &$consumed, $closing): int
{
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$bucket->data = str_replace($this->search, $this->replace, $bucket->data);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
public function onCreate(): bool
{
if (!$this->isValidParams($this->params)) {
return false;
}
$this->search = [$this->params['escape'].$this->params['enclosure']];
$this->replace = [$this->params['enclosure'].$this->params['enclosure']];
if (STREAM_FILTER_WRITE != $this->params['mode']) {
return true;
}
$this->search = [$this->params['escape'].$this->params['enclosure']];
$this->replace = [$this->params['escape'].$this->params['enclosure'].$this->params['enclosure']];
if ($this->isValidSequence($this->params)) {
$this->search[] = $this->params['whitespace_replace'];
$this->replace[] = ' ';
}
return true;
}
/**
* Validate params property.
*/
protected function isValidParams(array $params): bool
{
static $mode_list = [STREAM_FILTER_READ => 1, STREAM_FILTER_WRITE => 1];
return isset($params['enclosure'], $params['escape'], $params['mode'], $mode_list[$params['mode']])
&& 1 == strlen($params['enclosure'])
&& 1 == strlen($params['escape']);
}
/**
* Is Valid White space replaced sequence.
*
* @return bool
*/
protected function isValidSequence(array $params)
{
return isset($params['whitespace_replace'])
&& strlen($params['whitespace_replace']) == strcspn($params['whitespace_replace'], self::$force_enclosure);
}
}

View File

@@ -0,0 +1,365 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use CallbackFilterIterator;
use Iterator;
use JsonSerializable;
use SplFileObject;
use function array_combine;
use function array_filter;
use function array_pad;
use function array_slice;
use function array_unique;
use function count;
use function is_array;
use function iterator_count;
use function iterator_to_array;
use function mb_strlen;
use function mb_substr;
use function strlen;
use function substr;
use const STREAM_FILTER_READ;
/**
* A class to parse and read records from a CSV document.
*/
class Reader extends AbstractCsv implements TabularDataReader, JsonSerializable
{
protected const STREAM_FILTER_MODE = STREAM_FILTER_READ;
protected ?int $header_offset = null;
protected int $nb_records = -1;
protected bool $is_empty_records_included = false;
/** @var array<string> header record. */
protected array $header = [];
public static function createFromPath(string $path, string $open_mode = 'r', $context = null)
{
return parent::createFromPath($path, $open_mode, $context);
}
protected function resetProperties(): void
{
$this->nb_records = -1;
$this->header = [];
}
/** Returns the header offset. */
public function getHeaderOffset(): ?int
{
return $this->header_offset;
}
public function getHeader(): array
{
if (null === $this->header_offset) {
return $this->header;
}
if ([] !== $this->header) {
return $this->header;
}
$this->header = $this->setHeader($this->header_offset);
return $this->header;
}
/**
* Determine the CSV record header.
*
* @throws Exception If the header offset is set and no record is found or is the empty array
*
* @return array<string>
*/
protected function setHeader(int $offset): array
{
$header = $this->seekRow($offset);
if (in_array($header, [[], [null]], true)) {
throw SyntaxError::dueToHeaderNotFound($offset);
}
if (0 !== $offset) {
return $header;
}
$header = $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
if ([''] === $header) {
throw SyntaxError::dueToHeaderNotFound($offset);
}
return $header;
}
/** Returns the row at a given offset. */
protected function seekRow(int $offset): array
{
foreach ($this->getDocument() as $index => $record) {
if ($offset === $index) {
return $record;
}
}
return [];
}
/**
* Returns the document as an Iterator.
*/
protected function getDocument(): Iterator
{
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$this->document->rewind();
return $this->document;
}
/**
* Strip the BOM sequence from a record.
*
* @param string[] $record
*
* @return array<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;
}
public function fetchColumnByName(string $name): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchColumnByName($name);
}
public function fetchColumnByOffset(int $offset = 0): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchColumnByOffset($offset);
}
public function fetchColumn($index = 0): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchColumn($index);
}
public function fetchOne(int $nth_record = 0): array
{
return ResultSet::createFromTabularDataReader($this)->fetchOne($nth_record);
}
public function fetchPairs($offset_index = 0, $value_index = 1): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchPairs($offset_index, $value_index);
}
public function count(): int
{
if (-1 === $this->nb_records) {
$this->nb_records = iterator_count($this->getRecords());
}
return $this->nb_records;
}
public function getIterator(): Iterator
{
return $this->getRecords();
}
public function jsonSerialize(): array
{
return iterator_to_array($this->getRecords(), false);
}
public function getRecords(array $header = []): Iterator
{
$header = $this->computeHeader($header);
$normalized = fn ($record): bool => is_array($record) && ($this->is_empty_records_included || $record != [null]);
$bom = '';
if (!$this->is_input_bom_included) {
$bom = $this->getInputBOM();
}
$document = $this->getDocument();
$records = $this->stripBOM(new CallbackFilterIterator($document, $normalized), $bom);
if (null !== $this->header_offset) {
$records = new CallbackFilterIterator($records, fn (array $record, int $offset): bool => $offset !== $this->header_offset);
}
if ($this->is_empty_records_included) {
return $this->combineHeader(new MapIterator(
$records,
fn (array $record): array => ([null] === $record) ? [] : $record
), $header);
}
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 array<string>
*/
protected function computeHeader(array $header)
{
if ([] === $header) {
$header = $this->getHeader();
}
if ($header !== ($filtered_header = array_filter($header, 'is_string'))) {
throw SyntaxError::dueToInvalidHeaderColumnNames();
}
if ($header !== array_unique($filtered_header)) {
throw SyntaxError::dueToDuplicateHeaderColumnNames($header);
}
return $header;
}
/**
* Combine the CSV header to each record if present.
*
* @param string[] $header
*/
protected function combineHeader(Iterator $iterator, array $header): Iterator
{
if ([] === $header) {
return $iterator;
}
$field_count = count($header);
$mapper = static 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);
}
/** @var array<string|null> $assocRecord */
$assocRecord = array_combine($header, $record);
return $assocRecord;
};
return new MapIterator($iterator, $mapper);
}
/**
* Strip the BOM sequence from the returned records if necessary.
*/
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;
}
$record = $this->removeBOM($record, $bom_length, $this->enclosure);
if ([''] === $record) {
return [null];
}
return $record;
};
return new CallbackFilterIterator(
new MapIterator($iterator, $mapper),
fn (array $record): bool => $this->is_empty_records_included || $record != [null]
);
}
/**
* 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(?int $offset): self
{
if ($offset === $this->header_offset) {
return $this;
}
if (null !== $offset && 0 > $offset) {
throw InvalidArgument::dueToInvalidHeaderOffset($offset, __METHOD__);
}
$this->header_offset = $offset;
$this->resetProperties();
return $this;
}
/**
* Enable skipping empty records.
*/
public function skipEmptyRecords(): self
{
if ($this->is_empty_records_included) {
$this->is_empty_records_included = false;
$this->nb_records = -1;
}
return $this;
}
/**
* Disable skipping empty records.
*/
public function includeEmptyRecords(): self
{
if (!$this->is_empty_records_included) {
$this->is_empty_records_included = true;
$this->nb_records = -1;
}
return $this;
}
/**
* Tells whether empty records are skipped by the instance.
*/
public function isEmptyRecordsIncluded(): bool
{
return $this->is_empty_records_included;
}
}

View File

@@ -0,0 +1,261 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use CallbackFilterIterator;
use Generator;
use Iterator;
use JsonSerializable;
use LimitIterator;
use function array_flip;
use function array_search;
use function is_string;
use function iterator_count;
use function iterator_to_array;
/**
* Represents the result set of a {@link Reader} processed by a {@link Statement}.
*/
class ResultSet implements TabularDataReader, JsonSerializable
{
/** The CSV records collection. */
protected Iterator $records;
/** @var array<string> The CSV records collection header. */
protected array $header = [];
public function __construct(Iterator $records, array $header)
{
$this->validateHeader($header);
$this->records = $records;
$this->header = $header;
}
/**
* @throws SyntaxError if the header syntax is invalid
*/
protected function validateHeader(array $header): void
{
if ($header !== ($filtered_header = array_filter($header, 'is_string'))) {
throw SyntaxError::dueToInvalidHeaderColumnNames();
}
if ($header !== array_unique($filtered_header)) {
throw SyntaxError::dueToDuplicateHeaderColumnNames($header);
}
}
public function __destruct()
{
unset($this->records);
}
/**
* Returns a new instance from an object implementing the TabularDataReader interface.
*/
public static function createFromTabularDataReader(TabularDataReader $reader): self
{
return new self($reader->getRecords(), $reader->getHeader());
}
/**
* Returns the header associated with the result set.
*
* @return array<string>
*/
public function getHeader(): array
{
return $this->header;
}
public function getIterator(): Iterator
{
return $this->getRecords();
}
public function getRecords(array $header = []): Iterator
{
$this->validateHeader($header);
$records = $this->combineHeader($header);
foreach ($records as $offset => $value) {
yield $offset => $value;
}
}
/**
* Combine the header to each record if present.
*/
protected function combineHeader(array $header): Iterator
{
if ($header === $this->header || [] === $header) {
return $this->records;
}
$field_count = count($header);
$mapper = static 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);
}
/** @var array<string|null> $assocRecord */
$assocRecord = array_combine($header, $record);
return $assocRecord;
};
return new MapIterator($this->records, $mapper);
}
public function count(): int
{
return iterator_count($this->records);
}
public function jsonSerialize(): array
{
return iterator_to_array($this->records, false);
}
public function fetchOne(int $nth_record = 0): array
{
if ($nth_record < 0) {
throw InvalidArgument::dueToInvalidRecordOffset($nth_record, __METHOD__);
}
$iterator = new LimitIterator($this->records, $nth_record, 1);
$iterator->rewind();
$result = $iterator->current();
if (!is_array($result)) {
return [];
}
return $result;
}
/**
* @throws Exception
*/
public function fetchColumnByName(string $name): Iterator
{
return $this->yieldColumn(
$this->getColumnIndexByValue($name, 'name', __METHOD__)
);
}
/**
* @throws Exception
*/
public function fetchColumnByOffset(int $offset): Iterator
{
return $this->yieldColumn(
$this->getColumnIndexByKey($offset, 'offset', __METHOD__)
);
}
public function fetchColumn($index = 0): Iterator
{
return $this->yieldColumn(
$this->getColumnIndex($index, 'offset', __METHOD__)
);
}
/**
* @param string|int $offset
*/
protected function yieldColumn($offset): Generator
{
$iterator = new MapIterator(
new CallbackFilterIterator($this->records, fn (array $record): bool => isset($record[$offset])),
fn (array $record): string => $record[$offset]
);
foreach ($iterator as $key => $value) {
yield $key => $value;
}
}
/**
* Filter a column name against the header if any.
*
* @param string|int $field the field name or the field index
*
* @throws InvalidArgument if the field is invalid or not found
*
* @return string|int
*/
protected function getColumnIndex($field, string $type, string $method)
{
if (is_string($field)) {
return $this->getColumnIndexByValue($field, $type, $method);
}
return $this->getColumnIndexByKey($field, $type, $method);
}
/**
* Returns the selected column name.
*
* @throws InvalidArgument if the column is not found
*/
protected function getColumnIndexByValue(string $value, string $type, string $method): string
{
if (false === array_search($value, $this->header, true)) {
throw InvalidArgument::dueToInvalidColumnIndex($value, $type, $method);
}
return $value;
}
/**
* Returns the selected column name according to its offset.
*
* @throws InvalidArgument if the field is invalid or not found
*
* @return int|string
*/
protected function getColumnIndexByKey(int $index, string $type, string $method)
{
if ($index < 0) {
throw InvalidArgument::dueToInvalidColumnIndex($index, $type, $method);
}
if ([] === $this->header) {
return $index;
}
$value = array_search($index, array_flip($this->header), true);
if (false === $value) {
throw InvalidArgument::dueToInvalidColumnIndex($index, $type, $method);
}
return $value;
}
public function fetchPairs($offset_index = 0, $value_index = 1): Iterator
{
$offset = $this->getColumnIndex($offset_index, 'offset', __METHOD__);
$value = $this->getColumnIndex($value_index, 'value', __METHOD__);
$iterator = new MapIterator(
new CallbackFilterIterator($this->records, fn (array $record): bool => isset($record[$offset])),
fn (array $record): array => [$record[$offset], $record[$value] ?? null]
);
/** @var array{0:int|string, 1:string|null} $pair */
foreach ($iterator as $pair) {
yield $pair[0] => $pair[1];
}
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use ArrayIterator;
use CallbackFilterIterator;
use Iterator;
use LimitIterator;
use function array_reduce;
/**
* Criteria to filter a {@link Reader} object.
*/
class Statement
{
/** @var array<callable> Callables to filter the iterator. */
protected array $where = [];
/** @var array<callable> Callables to sort the iterator. */
protected array $order_by = [];
/** iterator Offset. */
protected int $offset = 0;
/** iterator maximum length. */
protected int $limit = -1;
/**
* @throws Exception
*/
public static function create(callable $where = null, int $offset = 0, int $limit = -1): self
{
$stmt = new self();
if (null !== $where) {
$stmt = $stmt->where($where);
}
return $stmt->offset($offset)->limit($limit);
}
/**
* Set the Iterator filter method.
*/
public function where(callable $where): self
{
$clone = clone $this;
$clone->where[] = $where;
return $clone;
}
/**
* Set an Iterator sorting callable function.
*/
public function orderBy(callable $order_by): self
{
$clone = clone $this;
$clone->order_by[] = $order_by;
return $clone;
}
/**
* Set LimitIterator Offset.
*
* @throws Exception if the offset is lesser than 0
*/
public function offset(int $offset): self
{
if (0 > $offset) {
throw InvalidArgument::dueToInvalidRecordOffset($offset, __METHOD__);
}
if ($offset === $this->offset) {
return $this;
}
$clone = clone $this;
$clone->offset = $offset;
return $clone;
}
/**
* Set LimitIterator Count.
*
* @throws Exception if the limit is lesser than -1
*/
public function limit(int $limit): self
{
if (-1 > $limit) {
throw InvalidArgument::dueToInvalidLimit($limit, __METHOD__);
}
if ($limit === $this->limit) {
return $this;
}
$clone = clone $this;
$clone->limit = $limit;
return $clone;
}
/**
* Execute the prepared Statement on the {@link Reader} object.
*
* @param array<string> $header an optional header to use instead of the CSV document header
*/
public function process(TabularDataReader $tabular_data, array $header = []): TabularDataReader
{
if ([] === $header) {
$header = $tabular_data->getHeader();
}
$iterator = $tabular_data->getRecords($header);
$iterator = array_reduce($this->where, [$this, 'filter'], $iterator);
$iterator = $this->buildOrderBy($iterator);
return new ResultSet(new LimitIterator($iterator, $this->offset, $this->limit), $header);
}
/**
* Filters elements of an Iterator using a callback function.
*/
protected function filter(Iterator $iterator, callable $callable): CallbackFilterIterator
{
return new CallbackFilterIterator($iterator, $callable);
}
/**
* Sort the Iterator.
*/
protected function buildOrderBy(Iterator $iterator): Iterator
{
if ([] === $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;
};
$it = new ArrayIterator();
foreach ($iterator as $offset => $value) {
$it[$offset] = $value;
}
$it->uasort($compare);
return $it;
}
}

View File

@@ -0,0 +1,447 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use ReturnTypeWillChange;
use SeekableIterator;
use SplFileObject;
use TypeError;
use function array_keys;
use function array_walk_recursive;
use function fclose;
use function feof;
use function fflush;
use function fgetcsv;
use function fgets;
use function fopen;
use function fpassthru;
use function fputcsv;
use function fread;
use function fseek;
use function fwrite;
use function get_resource_type;
use function gettype;
use function is_array;
use function is_resource;
use function rewind;
use function stream_filter_append;
use function stream_filter_remove;
use function stream_get_meta_data;
use function strlen;
use const PHP_VERSION_ID;
use const SEEK_SET;
/**
* An object oriented API to handle a PHP stream resource.
*
* @internal used internally to iterate over a stream resource
*/
final class Stream implements SeekableIterator
{
/** @var array<string, array<resource>> Attached filters. */
private array $filters = [];
/** @var resource */
private $stream;
private bool $should_close_stream = false;
/** @var mixed can be a null false or a scalar type value. Current iterator value. */
private $value;
/** Current iterator key. */
private int $offset;
/** Flags for the Document.*/
private int $flags = 0;
private string $delimiter = ',';
private string $enclosure = '"';
private string $escape = '\\';
private bool $is_seekable = false;
/**
* @param mixed $stream stream type resource
*/
public function __construct($stream)
{
if (!is_resource($stream)) {
throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.');
}
if ('stream' !== ($type = get_resource_type($stream))) {
throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given');
}
$this->is_seekable = stream_get_meta_data($stream)['seekable'];
$this->stream = $stream;
}
public function __destruct()
{
array_walk_recursive($this->filters, fn ($filter): bool => @stream_filter_remove($filter));
if ($this->should_close_stream && is_resource($this->stream)) {
fclose($this->stream);
}
unset($this->stream);
}
public function __clone()
{
throw UnavailableStream::dueToForbiddenCloning(self::class);
}
public function __debugInfo(): array
{
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 resource|null $context
*
* @throws Exception if the stream resource can not be created
*/
public static function createFromPath(string $path, string $open_mode = 'r', $context = null): self
{
$args = [$path, $open_mode];
if (null !== $context) {
$args[] = false;
$args[] = $context;
}
$resource = @fopen(...$args);
if (!is_resource($resource)) {
throw UnavailableStream::dueToPathNotFound($path);
}
$instance = new self($resource);
$instance->should_close_stream = true;
return $instance;
}
/**
* Return a new instance from a string.
*/
public static function createFromString(string $content = ''): self
{
/** @var resource $resource */
$resource = fopen('php://temp', 'r+');
fwrite($resource, $content);
$instance = new self($resource);
$instance->should_close_stream = true;
return $instance;
}
/**
* returns the URI of the underlying stream.
*
* @see https://www.php.net/manual/en/splfileinfo.getpathname.php
*/
public function getPathname(): string
{
return stream_get_meta_data($this->stream)['uri'];
}
/**
* append a filter.
*
* @see http://php.net/manual/en/function.stream-filter-append.php
*
* @throws InvalidArgument if the filter can not be appended
*/
public function appendFilter(string $filtername, int $read_write, array $params = null): void
{
$res = @stream_filter_append($this->stream, $filtername, $read_write, $params ?? []);
if (!is_resource($res)) {
throw InvalidArgument::dueToStreamFilterNotFound($filtername);
}
$this->filters[$filtername][] = $res;
}
/**
* Set CSV control.
*
* @see http://php.net/manual/en/SplFileObject.setcsvcontrol.php
*/
public function setCsvControl(string $delimiter = ',', string $enclosure = '"', string $escape = '\\'): void
{
[$this->delimiter, $this->enclosure, $this->escape] = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
}
/**
* Filter Csv control characters.
*
* @throws InvalidArgument If the Csv control character is not one character only.
*/
private function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array
{
if (1 !== strlen($delimiter)) {
throw InvalidArgument::dueToInvalidDelimiterCharacter($delimiter, $caller);
}
if (1 !== strlen($enclosure)) {
throw InvalidArgument::dueToInvalidEnclosureCharacter($enclosure, $caller);
}
if (1 === strlen($escape) || ('' === $escape && 70400 <= PHP_VERSION_ID)) {
return [$delimiter, $enclosure, $escape];
}
throw InvalidArgument::dueToInvalidEscapeCharacter($escape, $caller);
}
/**
* Set CSV control.
*
* @see http://php.net/manual/en/SplFileObject.getcsvcontrol.php
*
* @return array<string>
*/
public function getCsvControl(): array
{
return [$this->delimiter, $this->enclosure, $this->escape];
}
/**
* Set CSV stream flags.
*
* @see http://php.net/manual/en/SplFileObject.setflags.php
*/
public function setFlags(int $flags): void
{
$this->flags = $flags;
}
/**
* Write a field array as a CSV line.
*
* @see http://php.net/manual/en/SplFileObject.fputcsv.php
*
* @return int|false
*/
public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\', string $eol = "\n")
{
$controls = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
if (80100 <= PHP_VERSION_ID) {
$controls[] = $eol;
}
return fputcsv($this->stream, $fields, ...$controls);
}
/**
* Get line number.
*
* @see http://php.net/manual/en/SplFileObject.key.php
*/
public function key(): int
{
return $this->offset;
}
/**
* Read next line.
*
* @see http://php.net/manual/en/SplFileObject.next.php
*/
public function next(): void
{
$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(): void
{
if (!$this->is_seekable) {
throw UnavailableFeature::dueToMissingStreamSeekability();
}
rewind($this->stream);
$this->offset = 0;
$this->value = false;
if (0 !== ($this->flags & SplFileObject::READ_AHEAD)) {
$this->current();
}
}
/**
* Not at EOF.
*
* @see http://php.net/manual/en/SplFileObject.valid.php
*/
public function valid(): bool
{
if (0 !== ($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 The value of the current element.
*/
#[ReturnTypeWillChange]
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|false
*/
private function getCurrentRecord()
{
$flag = 0 !== ($this->flags & SplFileObject::SKIP_EMPTY);
do {
$ret = fgetcsv($this->stream, 0, $this->delimiter, $this->enclosure, $this->escape);
} while ($flag && is_array($ret) && null === $ret[0]);
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): void
{
if ($position < 0) {
throw InvalidArgument::dueToInvalidSeekingPosition($position, __METHOD__);
}
$this->rewind();
while ($this->key() !== $position && $this->valid()) {
$this->current();
$this->next();
}
if (0 !== $position) {
$this->offset--;
}
$this->current();
}
/**
* Output all remaining data on a file pointer.
*
* @see http://php.net/manual/en/SplFileObject.fpatssthru.php
*
* @return int|false
*/
public function fpassthru()
{
return fpassthru($this->stream);
}
/**
* Read from file.
*
* @see http://php.net/manual/en/SplFileObject.fread.php
*
* @param int<0, max> $length The number of bytes to read
*
* @return string|false
*/
public function fread(int $length)
{
return fread($this->stream, $length);
}
/**
* Gets a line from file.
*
* @see http://php.net/manual/en/SplFileObject.fgets.php
*
* @return string|false
*/
public function fgets()
{
return fgets($this->stream);
}
/**
* Seek to a position.
*
* @see http://php.net/manual/en/SplFileObject.fseek.php
*
* @throws Exception if the stream resource is not seekable
*/
public function fseek(int $offset, int $whence = SEEK_SET): int
{
if (!$this->is_seekable) {
throw UnavailableFeature::dueToMissingStreamSeekability();
}
return fseek($this->stream, $offset, $whence);
}
/**
* Write to stream.
*
* @see http://php.net/manual/en/SplFileObject.fwrite.php
*
* @return int|false
*/
public function fwrite(string $str, int $length = null)
{
$args = [$this->stream, $str];
if (null !== $length) {
$args[] = $length;
}
return fwrite(...$args);
}
/**
* Flushes the output to a file.
*
* @see http://php.net/manual/en/SplFileObject.fwrite.php
*/
public function fflush(): bool
{
return fflush($this->stream);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Throwable;
/**
* SyntaxError Exception.
*/
class SyntaxError extends Exception
{
/**
* @var array<string>
*/
protected array $duplicateColumnNames = [];
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
*/
public function __construct(string $message = '', int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function dueToHeaderNotFound(int $offset): self
{
return new self('The header record does not exist or is empty at offset: `'.$offset.'`');
}
public static function dueToInvalidHeaderColumnNames(): self
{
return new self('The header record contains non string colum names.');
}
public static function dueToDuplicateHeaderColumnNames(array $header): self
{
$instance = new self('The header record contains duplicate column names.');
$instance->duplicateColumnNames = array_keys(array_filter(array_count_values($header), fn (int $value): bool => $value > 1));
return $instance;
}
public function duplicateColumnNames(): array
{
return $this->duplicateColumnNames;
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Countable;
use Iterator;
use IteratorAggregate;
/**
* Represents a Tabular data.
*
* @method Iterator fetchColumnByName(string $name) returns a column from its name
* @method Iterator fetchColumnByOffset(int $offset) returns a column from its offset
*/
interface TabularDataReader extends Countable, IteratorAggregate
{
/**
* Returns the number of records contained in the tabular data structure
* excluding the header record.
*/
public function count(): int;
/**
* Returns the tabular data records as an iterator object.
*
* Each record is represented as a simple array containing 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.
*/
public function getIterator(): Iterator;
/**
* Returns the header associated with the tabular data.
*
* The header must contains unique string or is an empty array
* if no header was specified.
*
* @return array<string>
*/
public function getHeader(): array;
/**
* Returns the tabular data records as an iterator object.
*
* Each record is represented as a simple array containing strings or null values.
*
* If the tabular data has a header record then each record is combined
* to the header record and the header record is removed from the iterator.
*
* If the tabular data is inconsistent. Missing record fields are
* filled with null values while extra record fields are strip from
* the returned object.
*
* @param array<string> $header an optional header to use instead of the CSV document header
*/
public function getRecords(array $header = []): Iterator;
/**
* Returns the nth record from the tabular data.
*
* By default if no index is provided the first record of the tabular data is returned
*
* @param int $nth_record the tabular data record offset
*
* @throws UnableToProcessCsv if argument is lesser than 0
*/
public function fetchOne(int $nth_record = 0): array;
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.8.0
*
* @see ::fetchColumnByName
* @see ::fetchColumnByOffset
*
* Returns a single column from the next record of the tabular data.
*
* By default if no value is supplied the first column is fetch
*
* @param string|int $index CSV column index
*
* @throws UnableToProcessCsv if the column index is invalid or not found
*/
public function fetchColumn($index = 0): Iterator;
/**
* Returns the next key-value pairs from the tabular data (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
*
* @throws UnableToProcessCsv if the column index is invalid or not found
*/
public function fetchPairs($offset_index = 0, $value_index = 1): Iterator;
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Throwable;
interface UnableToProcessCsv extends Throwable
{
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use Throwable;
/**
* StreamFilterSupportMissing Exception.
*/
class UnavailableFeature extends Exception
{
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
*/
public function __construct(string $message = '', int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public static function dueToUnsupportedStreamFilterApi(string $className): self
{
return new self('The stream filter API can not be used with a '.$className.' instance.');
}
public static function dueToMissingStreamSeekability(): self
{
return new self('stream does not support seeking');
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
final class UnavailableStream extends Exception
{
private function __construct(string $message)
{
parent::__construct($message);
}
public static function dueToPathNotFound(string $path): self
{
return new self('`'.$path.'`: failed to open stream: No such file or directory.');
}
public static function dueToForbiddenCloning(string $class_name): self
{
return new self('An object of class '.$class_name.' cannot be cloned.');
}
}

View File

@@ -0,0 +1,222 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use function array_reduce;
use function strlen;
use const PHP_VERSION_ID;
use const SEEK_CUR;
use const STREAM_FILTER_WRITE;
/**
* A class to insert records into a CSV Document.
*/
class Writer extends AbstractCsv
{
protected const STREAM_FILTER_MODE = STREAM_FILTER_WRITE;
/** @var array<callable> callable collection to format the record before insertion. */
protected array $formatters = [];
/** @var array<callable> callable collection to validate the record before insertion. */
protected array $validators = [];
protected string $newline = "\n";
protected int $flush_counter = 0;
protected ?int $flush_threshold = null;
protected function resetProperties(): void
{
}
/**
* Returns the current newline sequence characters.
*/
public function getNewline(): string
{
return $this->newline;
}
/**
* Get the flush threshold.
*/
public function getFlushThreshold(): ?int
{
return $this->flush_threshold;
}
/**
* Adds multiple records to the CSV document.
*
* @see Writer::insertOne
*/
public function insertAll(iterable $records): int
{
$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.
*
* A record is an array that can contains scalar types values, NULL values
* or objects implementing the __toString method.
*
* @throws CannotInsertRecord If the record can not be inserted
*/
public function insertOne(array $record): int
{
$record = array_reduce($this->formatters, fn (array $record, callable $formatter): array => $formatter($record), $record);
$this->validateRecord($record);
$bytes = $this->addRecord($record);
if (false === $bytes || 0 >= $bytes) {
throw CannotInsertRecord::triggerOnInsertion($record);
}
return $bytes + $this->consolidate();
}
/**
* Adds a single record to a CSV Document using PHP algorithm.
*
* @see https://php.net/manual/en/function.fputcsv.php
*
* @return int|false
*/
protected function addRecord(array $record)
{
if (PHP_VERSION_ID < 80100) {
return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
}
return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape, $this->newline);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.8.0
* @codeCoverageIgnore
*
* Format a record.
*
* The returned array must contain
* - scalar types values,
* - NULL values,
* - or objects implementing the __toString() method.
*/
protected function formatRecord(array $record, callable $formatter): array
{
return $formatter($record);
}
/**
* Validate a record.
*
* @throws CannotInsertRecord If the validation failed
*/
protected function validateRecord(array $record): void
{
foreach ($this->validators as $name => $validator) {
if (true !== $validator($record)) {
throw CannotInsertRecord::triggerOnValidation($name, $record);
}
}
}
/**
* Apply post insertion actions.
*/
protected function consolidate(): int
{
$bytes = 0;
if (80100 > PHP_VERSION_ID && "\n" !== $this->newline) {
$this->document->fseek(-1, SEEK_CUR);
/** @var int $newlineBytes */
$newlineBytes = $this->document->fwrite($this->newline, strlen($this->newline));
$bytes = $newlineBytes - 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.
*/
public function addFormatter(callable $formatter): self
{
$this->formatters[] = $formatter;
return $this;
}
/**
* Adds a record validator.
*/
public function addValidator(callable $validator, string $validator_name): self
{
$this->validators[$validator_name] = $validator;
return $this;
}
/**
* Sets the newline sequence.
*/
public function setNewline(string $newline): self
{
$this->newline = $newline;
return $this;
}
/**
* Set the flush threshold.
*
* @param ?int $threshold
*
* @throws InvalidArgument if the threshold is a integer lesser than 1
*/
public function setFlushThreshold(?int $threshold): self
{
if ($threshold === $this->flush_threshold) {
return $this;
}
if (null !== $threshold && 1 > $threshold) {
throw InvalidArgument::dueToInvalidThreshold($threshold, __METHOD__);
}
$this->flush_threshold = $threshold;
$this->flush_counter = 0;
$this->document->fflush();
return $this;
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use DOMAttr;
use DOMDocument;
use DOMElement;
use DOMException;
/**
* Converts tabular data into a DOMDocument object.
*/
class XMLConverter
{
/**
* XML Root name.
*/
protected string $root_name = 'csv';
/**
* XML Node name.
*/
protected string $record_name = 'row';
/**
* XML Item name.
*/
protected string $field_name = 'cell';
/**
* XML column attribute name.
*/
protected string $column_attr = '';
/**
* XML offset attribute name.
*/
protected string $offset_attr = '';
public static function create(): self
{
return new self();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see XMLConverter::create()
*/
public function __construct()
{
}
/**
* Convert a Record collection into a DOMDocument.
*/
public function convert(iterable $records): DOMDocument
{
$doc = new DOMDocument('1.0');
$node = $this->import($records, $doc);
$doc->appendChild($node);
return $doc;
}
/**
* Create a new DOMElement related to the given DOMDocument.
*
* **DOES NOT** attach to the DOMDocument
*/
public function import(iterable $records, DOMDocument $doc): DOMElement
{
$root = $doc->createElement($this->root_name);
foreach ($records as $offset => $record) {
$node = $this->recordToElement($doc, $record, $offset);
$root->appendChild($node);
}
return $root;
}
/**
* Convert a CSV record into a DOMElement and
* adds its offset as DOMElement attribute.
*/
protected function recordToElement(DOMDocument $doc, array $record, int $offset): DOMElement
{
$node = $doc->createElement($this->record_name);
foreach ($record as $node_name => $value) {
$item = $this->fieldToElement($doc, (string) $value, $node_name);
$node->appendChild($item);
}
if ('' !== $this->offset_attr) {
$node->setAttribute($this->offset_attr, (string) $offset);
}
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 int|string $node_name
*/
protected function fieldToElement(DOMDocument $doc, string $value, $node_name): DOMElement
{
$item = $doc->createElement($this->field_name);
$item->appendChild($doc->createTextNode($value));
if ('' !== $this->column_attr) {
$item->setAttribute($this->column_attr, (string) $node_name);
}
return $item;
}
/**
* XML root element setter.
*/
public function rootElement(string $node_name): self
{
$clone = clone $this;
$clone->root_name = $this->filterElementName($node_name);
return $clone;
}
/**
* Filter XML element name.
*
* @throws DOMException If the Element name is invalid
*/
protected function filterElementName(string $value): string
{
return (new DOMElement($value))->tagName;
}
/**
* XML Record element setter.
*/
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
*/
protected function filterAttributeName(string $value): string
{
if ('' === $value) {
return $value;
}
return (new DOMAttr($value))->name;
}
/**
* XML Field element setter.
*/
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;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
*
* @deprecated since version 9.7.0
* @see Info::fetchBOMSequence()
*
* Returns the BOM sequence found at the start of the string.
*
* If no valid BOM sequence is found an empty string is returned
*/
function bom_match(string $str): string
{
return Info::fetchBOMSequence($str) ?? '';
}
/**
* @param array<string> $delimiters
*
* @return array<string,int>
* @deprecated since version 9.7.0
* @see Info::getDelimiterStats()
*
* 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
*
*/
function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array
{
return Info::getDelimiterStats($csv, $delimiters, $limit);
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('League\Csv\bom_match')) {
require __DIR__.'/functions.php';
}