[增添]添加了datasource的setting数据库以及默认值

This commit is contained in:
makotocc0107
2024-08-27 09:57:44 +08:00
parent d111dfaea4
commit 72eb990970
10955 changed files with 978898 additions and 0 deletions

20
vendor/league/csv/LICENSE vendored Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 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.

16
vendor/league/csv/autoload.php vendored Normal file
View File

@@ -0,0 +1,16 @@
<?php
require __DIR__ . '/src/functions_include.php';
spl_autoload_register(static function (string $class_name): void {
$prefix = 'League\Csv\\';
if (!str_starts_with($class_name, $prefix)) {
return;
}
$file = __DIR__ . '/src/' . str_replace('\\', '/', substr($class_name, 11)) . '.php';
if (is_readable($file)) {
require $file;
}
});

84
vendor/league/csv/composer.json vendored Normal file
View File

@@ -0,0 +1,84 @@
{
"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": "^8.1.2",
"ext-filter": "*"
},
"require-dev": {
"ext-dom": "*",
"ext-xdebug": "*",
"doctrine/collections": "^2.2.2",
"friendsofphp/php-cs-fixer": "^3.57.1",
"phpbench/phpbench": "^1.2.15",
"phpstan/phpstan": "^1.11.1",
"phpstan/phpstan-deprecation-rules": "^1.2.0",
"phpstan/phpstan-phpunit": "^1.4.0",
"phpstan/phpstan-strict-rules": "^1.6.0",
"phpunit/phpunit": "^10.5.16 || ^11.1.3",
"symfony/var-dumper": "^6.4.6 || ^7.0.7"
},
"autoload": {
"psr-4": {
"League\\Csv\\": "src"
},
"files": ["src/functions_include.php"]
},
"scripts": {
"benchmark": "phpbench run src --report=default",
"phpcs": "PHP_CS_FIXER_IGNORE_ENV=1 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 -c phpstan.neon --ansi --memory-limit=192M",
"phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text",
"phpunit:min": "phpunit --no-coverage",
"test": [
"@phpunit",
"@phpstan",
"@phpcs"
]
},
"scripts-descriptions": {
"benchmark": "Runs benchmarks on writing and reader CSV documents",
"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 the HTMLConverter classes",
"ext-mbstring": "Needed to ease transcoding CSV using mb stream filters"
},
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"config": {
"sort-packages": true
}
}

490
vendor/league/csv/src/AbstractCsv.php vendored Normal file
View File

@@ -0,0 +1,490 @@
<?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 RuntimeException;
use SplFileObject;
use Stringable;
use Throwable;
use function filter_var;
use function get_class;
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 array<string, bool> collection of stream filters. */
protected array $stream_filters = [];
protected ?Bom $input_bom = null;
protected ?Bom $output_bom = null;
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
*/
protected function __construct(protected readonly SplFileObject|Stream $document)
{
[$this->delimiter, $this->enclosure, $this->escape] = $this->document->getCsvControl();
$this->resetProperties();
}
/**
* Reset dynamic object properties to improve performance.
*/
protected function resetProperties(): void
{
}
/**
* @throws UnavailableStream
*/
public function __clone()
{
throw UnavailableStream::dueToForbiddenCloning(static::class);
}
/**
* Returns a new instance from a SplFileObject.
*/
public static function createFromFileObject(SplFileObject $file): static
{
return new static($file);
}
/**
* Returns a new instance from a PHP resource stream.
*
* @param resource $stream
*/
public static function createFromStream($stream): static
{
return new static(Stream::createFromResource($stream));
}
/**
* Returns a new instance from a string.
*/
public static function createFromString(Stringable|string $content = ''): static
{
return new static(Stream::createFromString((string) $content));
}
/**
* Returns a new instance from a file path.
*
* @param resource|null $context the resource context
*
* @throws UnavailableStream
*/
public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): static
{
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?->value ?? '';
}
/**
* Returns the BOM sequence of the given CSV.
*/
public function getInputBOM(): string
{
if (null === $this->input_bom) {
$this->document->setFlags(SplFileObject::READ_CSV);
$this->input_bom = Bom::tryFromSequence($this->document);
}
return $this->input_bom?->value ?? '';
}
/**
* 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;
}
/**
* Tells whether the specified stream filter is attached 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.
*
* @throws Exception if the number of bytes is less than 1
*/
public function chunk(int $length): Generator
{
if ($length < 1) {
throw InvalidArgument::dueToInvalidChunkSize($length, __METHOD__);
}
$this->document->rewind();
$this->document->setFlags(0);
if (-1 === $this->document->fseek(strlen($this->getInputBOM()))) {
throw new RuntimeException('Unable to seek the document.');
}
yield from str_split($this->getOutputBOM().$this->document->fread($length), $length);
while ($this->document->valid()) {
yield $this->document->fread($length);
}
}
/**
* 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.
*
* Returns the number of characters read from the handle and passed through to the output.
*
* @throws Exception
*/
public function output(?string $filename = null): int
{
if (null !== $filename) {
$this->sendHeaders($filename);
}
$this->document->rewind();
$this->document->setFlags(0);
if (!$this->is_input_bom_included && -1 === $this->document->fseek(strlen($this->getInputBOM()))) {
throw new RuntimeException('Unable to seek the document.');
}
$stream = Stream::createFromString($this->getOutputBOM());
$stream->rewind();
$res1 = $stream->fpassthru();
if (false === $res1) {
throw new RuntimeException('Unable to output the document.');
}
$res2 = $this->document->fpassthru();
if (false === $res2) {
throw new RuntimeException('Unable to output the document.');
}
return $res1 + $res2;
}
/**
* 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 (1 === preg_match('/[^\x20-\x7E]/', $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.
*/
public function setDelimiter(string $delimiter): static
{
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.
*/
public function setEnclosure(string $enclosure): static
{
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.
*/
public function setEscape(string $escape): static
{
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.
*/
public function skipInputBOM(): static
{
$this->is_input_bom_included = false;
return $this;
}
/**
* Disables skipping Input BOM.
*/
public function includeInputBOM(): static
{
$this->is_input_bom_included = true;
return $this;
}
/**
* Sets the BOM sequence to prepend the CSV on output.
*
* @throws InvalidArgument if the given non-empty string is not a valid BOM sequence
*/
public function setOutputBOM(Bom|string|null $str): static
{
try {
$this->output_bom = match (true) {
$str instanceof Bom => $str,
null === $str,
'' === $str => null,
default => Bom::fromSequence($str),
};
return $this;
} catch (Throwable $exception) {
throw InvalidArgument::dueToInvalidBOMCharacter(__METHOD__, $exception);
}
}
/**
* Append a stream filter.
*
* @throws InvalidArgument If the stream filter API can not be appended
* @throws UnavailableFeature If the stream filter API can not be used
*/
public function addStreamFilter(string $filtername, null|array $params = null): static
{
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;
}
/**
* 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
* @codeCoverageIgnore
*
* 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
* @codeCoverageIgnore
*
* Tells whether the stream filter capabilities can be used.
*/
public function supportsStreamFilter(): bool
{
return $this->document instanceof Stream;
}
/**
* 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
* @codeCoverageIgnore
*/
public function getContent(): string
{
return $this->toString();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.1.0
* @see AbstractCsv::toString
* @codeCoverageIgnore
*
* Retrieves the CSV content
*/
public function __toString(): string
{
return $this->toString();
}
}

144
vendor/league/csv/src/Bom.php vendored Normal file
View File

@@ -0,0 +1,144 @@
<?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 SplFileObject;
use Stringable;
use Throwable;
use ValueError;
enum Bom: string
{
case Utf32Le = "\xFF\xFE\x00\x00";
case Utf32Be = "\x00\x00\xFE\xFF";
case Utf16Be = "\xFE\xFF";
case Utf16Le = "\xFF\xFE";
case Utf8 = "\xEF\xBB\xBF";
public static function fromSequence(mixed $sequence): self
{
return self::tryFromSequence($sequence)
?? throw new ValueError('No BOM sequence could be found on the given sequence.');
}
public static function tryFromSequence(mixed $sequence): ?self
{
$str = match (true) {
$sequence instanceof SplFileObject,
$sequence instanceof Stream => self::getContents($sequence, 4, 0),
is_resource($sequence) => stream_get_contents($sequence, 4, 0),
$sequence instanceof Stringable,
is_string($sequence) => substr((string) $sequence, 0, 4),
default => $sequence,
};
if (!is_string($str) || '' === rtrim($str)) {
return null;
}
foreach (self::cases() as $bom) {
if (str_starts_with($str, $bom->value)) {
return $bom;
}
}
return null;
}
private static function getContents(Stream|SplFileObject $sequence, int $length, int $offset): ?string
{
$position = $sequence->ftell();
if (false === $position) {
return null;
}
try {
$sequence->fseek($offset);
$str = $sequence->fread($length);
$sequence->fseek($position);
if (false === $str) {
return null;
}
return $str;
} catch (Throwable) {
return null;
}
}
public static function fromEncoding(string $name): self
{
return self::tryFromEncoding($name)
?? throw new ValueError('Unknown or unsupported BOM name `'.$name.'`.');
}
/**
* @see https://unicode.org/faq/utf_bom.html#gen7
*/
public static function tryFromEncoding(string $name): ?self
{
return match (strtoupper(str_replace(['_', '-'], '', $name))) {
'UTF8' => self::Utf8,
'UTF16',
'UTF16BE' => self::Utf16Be,
'UTF16LE' => self::Utf16Le,
'UTF32',
'UTF32BE' => self::Utf32Be,
'UTF32LE' => self::Utf32Le,
default => null,
};
}
public function length(): int
{
return strlen($this->value);
}
public function encoding(): string
{
return match ($this) {
self::Utf16Le => 'UTF-16LE',
self::Utf16Be => 'UTF-16BE',
self::Utf32Le => 'UTF-32LE',
self::Utf32Be => 'UTF-32BE',
self::Utf8 => 'UTF-8',
};
}
public function isUtf8(): bool
{
return match ($this) {
self::Utf8 => true,
default => false,
};
}
public function isUtf16(): bool
{
return match ($this) {
self::Utf16Le,
self::Utf16Be => true,
default => false,
};
}
public function isUtf32(): bool
{
return match ($this) {
self::Utf32Le,
self::Utf32Be => true,
default => false,
};
}
}

27
vendor/league/csv/src/ByteSequence.php vendored Normal file
View File

@@ -0,0 +1,27 @@
<?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.
*
* @deprecated since version 9.16.0
* @see Bom
*/
interface ByteSequence
{
public const BOM_UTF8 = "\xEF\xBB\xBF";
public const BOM_UTF16_BE = "\xFE\xFF";
public const BOM_UTF16_LE = "\xFF\xFE";
public const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
}

View File

@@ -0,0 +1,64 @@
<?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 validate the data. */
protected string $name = '';
/**
* Creates 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;
}
/**
* Creates 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;
}
/**
* Returns the validator name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns the invalid data submitted.
*/
public function getRecord(): array
{
return $this->record;
}
}

View File

@@ -0,0 +1,234 @@
<?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 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 strtolower;
use function substr;
/**
* Converts resource stream or tabular data content charset.
*/
class CharsetConverter extends php_user_filter
{
public const FILTERNAME = 'convert.league.csv';
public const BOM_SEQUENCE = 'bom_sequence';
public const SKIP_BOM_SEQUENCE = 'skip_bom_sequence';
protected string $input_encoding = 'UTF-8';
protected string $output_encoding = 'UTF-8';
protected bool $skipBomSequence = false;
/**
* Static method to add the stream filter to a {@link Reader} object to handle BOM skipping.
*/
public static function addBOMSkippingTo(Reader $document, string $output_encoding = 'UTF-8'): Reader
{
self::register();
$document->addStreamFilter(
self::getFiltername((Bom::tryFrom($document->getInputBOM()) ?? Bom::Utf8)->encoding(), $output_encoding),
[self::BOM_SEQUENCE => self::SKIP_BOM_SEQUENCE]
);
return $document;
}
/**
* Static method to add the stream filter to a {@link AbstractCsv} object.
*/
public static function addTo(AbstractCsv $csv, string $input_encoding, string $output_encoding, ?array $params = null): AbstractCsv
{
self::register();
return $csv->addStreamFilter(self::getFiltername($input_encoding, $output_encoding), $params);
}
/**
* 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 (!str_starts_with($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']);
$this->skipBomSequence = is_array($this->params)
&& isset($this->params[self::BOM_SEQUENCE])
&& self::SKIP_BOM_SEQUENCE === $this->params[self::BOM_SEQUENCE];
} catch (OutOfRangeException) {
return false;
}
return true;
}
public function filter($in, $out, &$consumed, bool $closing): int
{
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$alreadyRun = false;
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$content = $bucket->data;
if (!$alreadyRun && $this->skipBomSequence && null !== ($bom = Bom::tryFromSequence($content))) {
$content = substr($content, $bom->length());
}
$alreadyRun = true;
$bucket->data = mb_convert_encoding($content, $this->output_encoding, $this->input_encoding);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
restore_error_handler();
return PSFS_PASS_ON;
}
/**
* Converts Csv records collection into UTF-8.
*/
public function convert(iterable $records): iterable
{
return match (true) {
$this->output_encoding === $this->input_encoding => $records,
is_array($records) => array_map($this, $records),
default => MapIterator::fromIterable($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.
*/
protected function encodeField(int|float|string|null $value, int|string $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,56 @@
<?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
{
/**
* @throws InvalidArgument if the column count is less than -1
*/
public function __construct(
protected int $columns_count = -1
) {
if ($this->columns_count < -1) {
throw InvalidArgument::dueToInvalidColumnCount($this->columns_count, __METHOD__);
}
}
/**
* Returns the column count.
*/
public function getColumnCount(): int
{
return $this->columns_count;
}
/**
* Tells 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;
}
}

122
vendor/league/csv/src/EncloseField.php vendored Normal file
View File

@@ -0,0 +1,122 @@
<?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.
*
* DEPRECATION WARNING! This class will be removed in the next major point release
*
* @deprecated since version 9.10.0
* @see Writer::forceEnclosure()
*
* @see https://tools.ietf.org/html/rfc4180#section-2
* @see https://bugs.php.net/bug.php?id=38301
*/
class EncloseField extends php_user_filter
{
public 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 contain 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 is_array($this->params)
&& isset($this->params['sequence'])
&& self::isValidSequence($this->params['sequence']);
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
*/
public function filter($in, $out, &$consumed, bool $closing): int
{
/** @var array $params */
$params = $this->params;
/** @var string $sequence */
$sequence = $params['sequence'];
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$bucket->data = str_replace($sequence, '', $bucket->data);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}

160
vendor/league/csv/src/EscapeFormula.php vendored Normal file
View File

@@ -0,0 +1,160 @@
<?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 Stringable;
use function array_fill_keys;
use function array_keys;
use function array_map;
use function is_string;
/**
* A Formatter to tackle CSV Formula Injection.
*
* @see http://georgemauer.net/2017/10/07/csv-injection.html
*/
class EscapeFormula
{
/** Spreadsheet formula starting character. */
public const FORMULA_STARTING_CHARS = ['=', '-', '+', '@', "\t", "\r"];
/** Effective Spreadsheet formula starting characters. */
protected array $special_chars = [];
/**
* @param string $escape escape character to escape each CSV formula field
* @param array<string> $special_chars additional spreadsheet formula starting characters
*/
public function __construct(
protected string $escape = "'",
array $special_chars = []
) {
$this->special_chars = array_fill_keys([
...self::FORMULA_STARTING_CHARS,
...$this->filterSpecialCharacters(...$special_chars),
], 1);
}
/**
* Filter submitted special 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;
}
/**
* Escapes a CSV record.
*/
public function escapeRecord(array $record): array
{
return array_map($this->escapeField(...), $record);
}
public function unescapeRecord(array $record): array
{
return array_map($this->unescapeField(...), $record);
}
/**
* Escapes a CSV cell if its content is stringable.
*/
protected function escapeField(mixed $cell): mixed
{
$strOrNull = match (true) {
is_string($cell) => $cell,
$cell instanceof Stringable => (string) $cell,
default => null,
};
return match (true) {
null == $strOrNull,
!isset($strOrNull[0], $this->special_chars[$strOrNull[0]]) => $cell,
default => $this->escape.$strOrNull,
};
}
protected function unescapeField(mixed $cell): mixed
{
$strOrNull = match (true) {
is_string($cell) => $cell,
$cell instanceof Stringable => (string) $cell,
default => null,
};
return match (true) {
null === $strOrNull,
!isset($strOrNull[0], $strOrNull[1]),
$strOrNull[0] !== $this->escape,
!isset($this->special_chars[$strOrNull[1]]) => $cell,
default => substr($strOrNull, 1),
};
}
/**
* @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(mixed $value): bool
{
return is_string($value) || $value instanceof Stringable;
}
/**
* @deprecated since 9.11.0 will be removed in the next major release
* @codeCoverageIgnore
*
* League CSV formatter hook.
*
* @see escapeRecord
*/
public function __invoke(array $record): array
{
return $this->escapeRecord($record);
}
}

23
vendor/league/csv/src/Exception.php vendored Normal file
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
{
}

395
vendor/league/csv/src/FragmentFinder.php vendored Normal file
View File

@@ -0,0 +1,395 @@
<?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_filter;
use function array_map;
use function array_reduce;
use function count;
use function explode;
use function filter_var;
use function preg_match;
use function range;
use const FILTER_VALIDATE_INT;
/**
* @phpstan-type selection array{selection:string, start:int<-1, max>, end:?int, length:int, columns:array<int>}
*/
class FragmentFinder
{
private const REGEXP_URI_FRAGMENT = ',^(?<type>row|cell|col)=(?<selections>.*)$,i';
private const REGEXP_ROWS_COLUMNS_SELECTION = '/^(?<start>\d+)(-(?<end>\d+|\*))?$/';
private const REGEXP_CELLS_SELECTION = '/^(?<csr>\d+),(?<csc>\d+)(-(?<end>((?<cer>\d+),(?<cec>\d+))|\*))?$/';
private const TYPE_ROW = 'row';
private const TYPE_COLUMN = 'col';
private const TYPE_UNKNOWN = 'unknown';
public static function create(): self
{
return new self();
}
/**
* @throws SyntaxError
*
* @return iterable<int, TabularDataReader>
*/
public function findAll(string $expression, TabularDataReader $tabularDataReader): iterable
{
return $this->find($this->parseExpression($expression, $tabularDataReader), $tabularDataReader);
}
/**
* @throws SyntaxError
*/
public function findFirst(string $expression, TabularDataReader $tabularDataReader): ?TabularDataReader
{
$fragment = $this->find($this->parseExpression($expression, $tabularDataReader), $tabularDataReader)[0];
return match ([]) {
$fragment->first() => null,
default => $fragment,
};
}
/**
* @throws SyntaxError
* @throws FragmentNotFound if the expression can not be parsed
*/
public function findFirstOrFail(string $expression, TabularDataReader $tabularDataReader): TabularDataReader
{
$parsedExpression = $this->parseExpression($expression, $tabularDataReader);
if ([] !== array_filter($parsedExpression['selections'], fn (array $selection) => -1 === $selection['start'])) {
throw new FragmentNotFound('The expression `'.$expression.'` contains an invalid or an unsupported selection for the tabular data.');
}
$fragment = $this->find($parsedExpression, $tabularDataReader)[0];
return match ([]) {
$fragment->first() => throw new FragmentNotFound('No fragment found in the tabular data with the expression `'.$expression.'`.'),
default => $fragment,
};
}
/**
* @param array{type:string, selections:non-empty-array<selection>} $parsedExpression
*
* @throws SyntaxError
*
* @return array<int, TabularDataReader>
*/
private function find(array $parsedExpression, TabularDataReader $tabularDataReader): array
{
['type' => $type, 'selections' => $selections] = $parsedExpression;
$selections = array_filter($selections, fn (array $selection) => -1 !== $selection['start']);
if ([] === $selections) {
return [ResultSet::createFromRecords()];
}
if (self::TYPE_ROW === $type) {
$rowFilter = fn (array $record, int $offset): bool => [] !== array_filter(
$selections,
fn (array $selection) =>
$offset >= $selection['start'] &&
(null === $selection['end'] || $offset <= $selection['end'])
);
return [
Statement::create()
->where($rowFilter)
->process($tabularDataReader),
];
}
if (self::TYPE_COLUMN === $type) {
$columns = array_reduce(
$selections,
fn (array $columns, array $selection) => [...$columns, ...$selection['columns']],
[]
);
return [match ([]) {
$columns => ResultSet::createFromRecords(),
default => Statement::create()->select(...$columns)->process($tabularDataReader),
}];
}
return array_map(
fn (array $selection) => Statement::create()
->offset($selection['start'])
->limit($selection['length'])
->select(...$selection['columns'])
->process($tabularDataReader),
$selections
);
}
/**
* @return array{type:string, selections:non-empty-array<selection>}
*/
private function parseExpression(string $expression, TabularDataReader $tabularDataReader): array
{
if (1 !== preg_match(self::REGEXP_URI_FRAGMENT, $expression, $matches)) {
return [
'type' => self::TYPE_UNKNOWN,
'selections' => [
[
'selection' => $expression,
'start' => -1,
'end' => null,
'length' => -1,
'columns' => [],
],
],
];
}
$type = strtolower($matches['type']);
/** @var non-empty-array<selection> $res */
$res = array_reduce(
explode(';', $matches['selections']),
fn (array $selections, string $selection): array => [...$selections, match ($type) {
self::TYPE_ROW => $this->parseRowSelection($selection),
self::TYPE_COLUMN => $this->parseColumnSelection($selection, $tabularDataReader),
default => $this->parseCellSelection($selection, $tabularDataReader),
}],
[]
);
return [
'type' => $type,
'selections' => $res,
];
}
/**
* @return selection
*/
private function parseRowSelection(string $selection): array
{
[$start, $end] = $this->parseRowColumnSelection($selection);
return match (true) {
-1 === $start,
null === $end => [
'selection' => $selection,
'start' => $start,
'end' => $start,
'length' => 1,
'columns' => [],
],
'*' === $end => [
'selection' => $selection,
'start' => $start,
'end' => null,
'length' => -1,
'columns' => [],
],
default => [
'selection' => $selection,
'start' => $start,
'end' => $end,
'length' => $end - $start + 1,
'columns' => [],
],
};
}
/**
* @return selection
*/
private function parseColumnSelection(string $selection, TabularDataReader $tabularDataReader): array
{
[$start, $end] = $this->parseRowColumnSelection($selection);
$header = $tabularDataReader->getHeader();
if ([] === $header) {
$header = $tabularDataReader->first();
}
$nbColumns = count($header);
return match (true) {
-1 === $start,
$start >= $nbColumns => [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => -1,
'columns' => [],
],
null === $end => [
'selection' => $selection,
'start' => 0,
'end' => null,
'length' => -1,
'columns' => [$start],
],
'*' === $end,
$end > ($nbColumns - 1) => [
'selection' => $selection,
'start' => 0,
'end' => null,
'length' => -1,
'columns' => range($start, $nbColumns - 1),
],
default => [
'selection' => $selection,
'start' => 0,
'end' => $end,
'length' => -1,
'columns' => range($start, $end),
],
};
}
/**
* @return array{int<-1, max>, int|null|'*'}
*/
private function parseRowColumnSelection(string $selection): array
{
if (1 !== preg_match(self::REGEXP_ROWS_COLUMNS_SELECTION, $selection, $found)) {
return [-1, 0];
}
$start = $found['start'];
$end = $found['end'] ?? null;
$start = filter_var($start, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $start) {
return [-1, 0];
}
--$start;
if (null === $end || '*' === $end) {
return [$start, $end];
}
$end = filter_var($end, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $end) {
return [-1, 0];
}
--$end;
if ($end <= $start) {
return [-1, 0];
}
return [$start, $end];
}
/**
* @return selection
*/
private function parseCellSelection(string $selection, TabularDataReader $tabularDataReader): array
{
if (1 !== preg_match(self::REGEXP_CELLS_SELECTION, $selection, $found)) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
$cellStartRow = filter_var($found['csr'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$cellStartCol = filter_var($found['csc'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $cellStartRow || false === $cellStartCol) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
--$cellStartRow;
--$cellStartCol;
$header = $tabularDataReader->getHeader();
if ([] === $header) {
$header = $tabularDataReader->first();
}
$nbColumns = count($header);
if ($cellStartCol > $nbColumns - 1) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
$cellEnd = $found['end'] ?? null;
if (null === $cellEnd) {
return [
'selection' => $selection,
'start' => $cellStartRow,
'end' => null,
'length' => 1,
'columns' => [$cellStartCol],
];
}
if ('*' === $cellEnd) {
return [
'selection' => $selection,
'start' => $cellStartRow,
'end' => null,
'length' => -1,
'columns' => range($cellStartCol, $nbColumns - 1),
];
}
$cellEndRow = filter_var($found['cer'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$cellEndCol = filter_var($found['cec'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (false === $cellEndRow || false === $cellEndCol) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
--$cellEndRow;
--$cellEndCol;
if ($cellEndRow < $cellStartRow || $cellEndCol < $cellStartCol) {
return [
'selection' => $selection,
'start' => -1,
'end' => null,
'length' => 1,
'columns' => [],
];
}
return [
'selection' => $selection,
'start' => $cellStartRow,
'end' => $cellEndRow,
'length' => $cellEndRow - $cellStartRow + 1,
'columns' => range($cellStartCol, ($cellEndCol > $nbColumns - 1) ? $nbColumns - 1 : $cellEndCol),
];
}
}

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 RuntimeException;
final class FragmentNotFound extends RuntimeException implements UnableToProcessCsv
{
}

162
vendor/league/csv/src/HTMLConverter.php vendored Normal file
View File

@@ -0,0 +1,162 @@
<?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.
*
* @throws DOMException
* @see HTMLConverterTest::create()
* @deprecated since version 9.7.0
*/
public function __construct()
{
$this->xml_converter = XMLConverter::create()
->rootElement('table')
->recordElement('tr')
->fieldElement('td')
;
}
/**
* Converts a tabular data collection into an HTML table string.
*
* @param array<string> $header_record An optional array of headers outputted using the `<thead>` and `<th>` elements
* @param array<string> $footer_record An optional array of footers outputted using the `<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);
return (string) $doc->saveHTML();
}
/**
* Creates a DOMElement representing an HTML table heading section.
*
* @throws DOMException
*/
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;
}
}

86
vendor/league/csv/src/Info.php vendored Normal file
View File

@@ -0,0 +1,86 @@
<?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 strlen;
use const COUNT_RECURSIVE;
final class Info implements ByteSequence
{
/**
* Returns the BOM sequence found at the start of the string.
*
* If no valid BOM sequence is found an empty string is returned
*
* @deprecated since version 9.16.0
* @see Bom::tryFromSequence()
* @codeCoverageIgnore
*/
public static function fetchBOMSequence(string $str): ?string
{
return Bom::tryFromSequence($str)?->value;
}
/**
* 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 array<string> $delimiters
*
* @return array<string, int>
*/
public static function getDelimiterStats(Reader $csv, array $delimiters, int $limit = 1): array
{
$stmt = Statement::create()->offset(0)->limit($limit);
$delimiterStats = function (array $stats, string $delimiter) use ($csv, $stmt): array {
$csv->setDelimiter($delimiter);
$foundRecords = [];
foreach ($stmt->process($csv) as $record) {
if (1 < count($record)) {
$foundRecords[] = $record;
}
}
$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, fn (string $value): bool => 1 === strlen($value))),
$delimiterStats,
array_fill_keys($delimiters, 0)
);
$csv->setHeaderOffset($currentHeaderOffset);
$csv->setDelimiter($currentDelimiter);
return $stats;
}
}

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 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 an empty string; `'.$escape.'` given.');
}
public static function dueToInvalidBOMCharacter(string $method, Throwable $exception): self
{
return new self($method.'() expects a valid Byte Order Mark.', 0, $exception);
}
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');
}
public static function dueToInvalidColumnIndex(string|int $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 dueToInvalidOrder(string $order, string $method): self
{
return new self($method.'() expects `ASC` or `DESC` in a case-insensitive way, '.$order.' given.');
}
public static function dueToInvalidOperator(string $operator, string $method): self
{
return new self($method.'() expects valid comparison operator in a case-insensitive way, '.$operator.' 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');
}
}

48
vendor/league/csv/src/MapIterator.php vendored Normal file
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;
use ArrayIterator;
use IteratorIterator;
use Traversable;
/**
* Maps 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;
}
public static function fromIterable(iterable $iterator, callable $callable): self
{
return match (true) {
is_array($iterator) => new self(new ArrayIterator($iterator), $callable),
default => new self($iterator, $callable),
};
}
public function current(): mixed
{
return ($this->callable)(parent::current(), parent::key());
}
}

View File

@@ -0,0 +1,87 @@
<?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\Query\Constraint;
use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query;
use ReflectionException;
use Traversable;
/**
* Enable filtering a record based on the value of a one of its cell.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record offset WILL NOT BE taken into account
*/
final class Column implements Query\Predicate
{
/**
* @throws Query\QueryException
*/
private function __construct(
public readonly string|int $column,
public readonly Comparison|Closure $operator,
public readonly mixed $value,
) {
if (!$this->operator instanceof Closure) {
$this->operator->accept($this->value);
}
}
/**
* @throws Query\QueryException
*/
public static function filterOn(
string|int $column,
Comparison|Closure|string $operator,
mixed $value = null,
): self {
if ($operator instanceof Closure) {
return new self($column, $operator, null);
}
return new self(
$column,
is_string($operator) ? Comparison::fromOperator($operator) : $operator,
$value
);
}
/**
* @throws ReflectionException
* @throws Query\QueryException
*/
public function __invoke(mixed $value, int|string $key): bool
{
$subject = Query\Row::from($value)->value($this->column);
if ($this->operator instanceof Closure) {
return ($this->operator)($subject);
}
return $this->operator->compare($subject, $this->value);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}
}

View File

@@ -0,0 +1,157 @@
<?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\Query\Constraint;
use League\Csv\Query\QueryException;
use function array_is_list;
use function count;
use function in_array;
use function is_array;
use function is_scalar;
use function is_string;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function strtoupper;
use function trim;
enum Comparison: string
{
case Equals = '=';
case NotEquals = '!=';
case GreaterThan = '>';
case GreaterThanOrEqual = '>=';
case LesserThan = '<';
case LesserThanOrEqual = '<=';
case Between = 'BETWEEN';
case NotBetween = 'NBETWEEN';
case Regexp = 'REGEXP';
case NotRegexp = 'NREGEXP';
case In = 'IN';
case NotIn = 'NIN';
case Contains = 'CONTAINS';
case NotContain = 'NCONTAIN';
case StartsWith = 'STARTS_WITH';
case EndsWith = 'ENDS_WITH';
public static function tryFromOperator(string $operator): ?self
{
$operator = strtoupper(trim($operator));
return match ($operator) {
'<>', 'NEQ', 'IS NOT', 'NOT EQUAL' => self::NotEquals,
'EQ', 'IS', 'EQUAL', 'EQUALS' => self::Equals,
'GT', 'GREATER THAN' => self::GreaterThan,
'GTE', 'GREATER THAN OR EQUAL' => self::GreaterThanOrEqual,
'LT', 'LESSER THAN' => self::LesserThan,
'LTE', 'LESSER THAN OR EQUAL' => self::LesserThanOrEqual,
'NOT_REGEXP', 'NOT REGEXP' => self::NotRegexp,
'NOT_CONTAIN', 'NOT CONTAIN', 'DOES_NOT_CONTAIN', 'DOES NOT CONTAIN' => self::NotContain,
'NOT_IN', 'NOT IN' => self::NotIn,
'NOT_BETWEEN', 'NOT BETWEEN' => self::Between,
'STARTS WITH', 'START WITH' => self::StartsWith,
'ENDS WITH', 'END WITH' => self::EndsWith,
default => self::tryFrom($operator),
};
}
/**
* @throws QueryException
*/
public static function fromOperator(string $operator): self
{
return self::tryFromOperator($operator) ?? throw QueryException::dueToUnknownOperator($operator);
}
/**
* Values comparison.
*
* The method return true if the values satisfy the comparison operator, otherwise false is returned.
*
* @throws QueryException
*/
public function compare(mixed $subject, mixed $reference): bool
{
$this->accept($reference);
return match ($this) {
self::Equals => self::isSingleValue($subject) ? $subject === $reference : $subject == $reference,
self::NotEquals => self::isSingleValue($subject) ? $subject !== $reference : $subject != $reference,
self::GreaterThan => $subject > $reference,
self::GreaterThanOrEqual => $subject >= $reference,
self::LesserThan => $subject < $reference,
self::LesserThanOrEqual => $subject <= $reference,
self::Between => $subject >= $reference[0] && $subject <= $reference[1], /* @phpstan-ignore-line */
self::NotBetween => $subject < $reference[0] || $subject > $reference[1], /* @phpstan-ignore-line */
self::In => in_array($subject, $reference, self::isSingleValue($subject)), /* @phpstan-ignore-line */
self::NotIn => !in_array($subject, $reference, self::isSingleValue($subject)), /* @phpstan-ignore-line */
self::Regexp => is_string($subject) && 1 === preg_match($reference, $subject), /* @phpstan-ignore-line */
self::NotRegexp => is_string($subject) && 1 !== preg_match($reference, $subject), /* @phpstan-ignore-line */
self::Contains => str_contains($subject, $reference), /* @phpstan-ignore-line */
self::NotContain => is_string($subject) && !str_contains($subject, $reference), /* @phpstan-ignore-line */
self::StartsWith => is_string($subject) && str_starts_with($subject, $reference), /* @phpstan-ignore-line */
self::EndsWith => is_string($subject) && str_ends_with($subject, $reference), /* @phpstan-ignore-line */
};
}
private static function isSingleValue(mixed $value): bool
{
return is_scalar($value) || null === $value;
}
/**
* Assert if the reference value can be used with the Enum operator.
*
* @throws QueryException
*/
public function accept(mixed $reference): void
{
match ($this) {
self::Between,
self::NotBetween => match (true) {
!is_array($reference),
!array_is_list($reference),
2 !== count($reference) => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be an list containing 2 values, the minimum and maximum values.'),
default => true,
},
self::In,
self::NotIn => match (true) {
!is_array($reference) => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be an array.'),
default => true,
},
self::Regexp,
self::NotRegexp => match (true) {
!is_string($reference),
'' === $reference,
false === @preg_match($reference, '') => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be a valid regular expression pattern string.'),
default => true,
},
self::Contains,
self::NotContain,
self::StartsWith,
self::EndsWith => match (true) {
!is_string($reference),
'' === $reference => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be a non empty string.'),
default => true,
},
self::Equals,
self::NotEquals,
self::GreaterThanOrEqual,
self::GreaterThan,
self::LesserThanOrEqual,
self::LesserThan => true,
};
}
}

View File

@@ -0,0 +1,153 @@
<?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\Query\Constraint;
use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query\Predicate;
use League\Csv\Query\PredicateCombinator;
use Traversable;
use function array_reduce;
/**
* @phpstan-import-type Condition from PredicateCombinator
* @phpstan-import-type ConditionExtended from PredicateCombinator
*/
final class Criteria implements PredicateCombinator
{
/**
* @param Condition $predicate
*/
private function __construct(private readonly Predicate|Closure $predicate)
{
}
/**
* Creates a new instance with predicates join using the logical AND operator.
*
* @param ConditionExtended ...$predicates
*/
public static function all(Predicate|Closure|callable ...$predicates): self
{
return new self(function (mixed $value, int|string $key) use ($predicates): bool {
foreach ($predicates as $predicate) {
if (!$predicate($value, $key)) {
return false;
}
}
return true;
});
}
/**
* Creates a new instance with predicates join using the logical NOT operator.
*
* @param ConditionExtended ...$predicates
*/
public static function none(Predicate|Closure|callable ...$predicates): self
{
return new self(function (mixed $value, int|string $key) use ($predicates): bool {
foreach ($predicates as $predicate) {
if ($predicate($value, $key)) {
return false;
}
}
return true;
});
}
/**
* Creates a new instance with predicates join using the logical OR operator.
*
* @param ConditionExtended ...$predicates
*/
public static function any(Predicate|Closure|callable ...$predicates): self
{
return new self(function (mixed $value, int|string $key) use ($predicates): bool {
foreach ($predicates as $predicate) {
if ($predicate($value, $key)) {
return true;
}
}
return false;
});
}
/**
* Creates a new instance with predicates join using the logical XOR operator.
*
* @param ConditionExtended ...$predicates
*/
public static function xany(Predicate|Closure|callable ...$predicates): self
{
return new self(fn (mixed $value, int|string $key): bool => array_reduce(
$predicates,
fn (bool $bool, Predicate|Closure|callable $predicate) => $predicate($value, $key) xor $bool,
false
));
}
public function __invoke(mixed $value, int|string $key): bool
{
return ($this->predicate)($value, $key);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}
/**
* @param ConditionExtended ...$predicates
*/
public function and(Predicate|Closure|callable ...$predicates): self
{
return self::all($this->predicate, ...$predicates);
}
/**
* @param ConditionExtended ...$predicates
*/
public function not(Predicate|Closure|callable ...$predicates): self
{
return self::none($this->predicate, ...$predicates);
}
/**
* @param ConditionExtended ...$predicates
*/
public function or(Predicate|Closure|callable ...$predicates): self
{
return self::any($this->predicate, ...$predicates);
}
/**
* @param ConditionExtended ...$predicates
*/
public function xor(Predicate|Closure|callable ...$predicates): self
{
return self::xany($this->predicate, ...$predicates);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace League\Csv\Query\Constraint;
use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query;
use Traversable;
/**
* Enable filtering a record based on its offset.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record value WILL NOT BE taken into account
*/
final class Offset implements Query\Predicate
{
/**
* @throws Query\QueryException
*/
private function __construct(
public readonly Comparison|Closure $operator,
public readonly mixed $value,
) {
if (!$this->operator instanceof Closure) {
$this->operator->accept($this->value);
}
}
/**
* @throws Query\QueryException
*/
public static function filterOn(
Comparison|Closure|string $operator,
mixed $value = null,
): self {
if ($operator instanceof Closure) {
return new self($operator, null);
}
return new self(
is_string($operator) ? Comparison::fromOperator($operator) : $operator,
$value
);
}
/**
* @throws Query\QueryException
*/
public function __invoke(mixed $value, int|string $key): bool
{
if ($this->operator instanceof Closure) {
return ($this->operator)($key);
}
return $this->operator->compare($key, $this->value);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}
}

View File

@@ -0,0 +1,103 @@
<?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\Query\Constraint;
use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query\Predicate;
use League\Csv\Query\Row;
use League\Csv\Query\QueryException;
use ReflectionException;
use Traversable;
use function array_filter;
use function is_array;
use function is_int;
use function is_string;
use const ARRAY_FILTER_USE_BOTH;
/**
* Enable filtering a record by comparing the values of two of its column.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record offset WILL NOT BE taken into account
*/
final class TwoColumns implements Predicate
{
/**
* @throws QueryException
*/
private function __construct(
public readonly string|int $first,
public readonly Comparison|Closure $operator,
public readonly array|string|int $second,
) {
if ($this->operator instanceof Closure && is_array($this->second)) {
throw new QueryException('The second column must be a string if the operator is a callback.');
}
if (is_array($this->second)) {
$res = array_filter($this->second, fn (mixed $value): bool => !is_string($value) && !is_int($value));
if ([] !== $res) {
throw new QueryException('The second column must be a string, an integer or a list of strings and/or integer when the operator is not a callback.');
}
}
}
/**
* @throws QueryException
*/
public static function filterOn(
string|int $firstColumn,
Comparison|Closure|string $operator,
array|string|int $secondColumn
): self {
if (is_string($operator)) {
$operator = Comparison::fromOperator($operator);
}
return new self($firstColumn, $operator, $secondColumn);
}
/**
* @throws QueryException
* @throws ReflectionException
*/
public function __invoke(mixed $value, int|string $key): bool
{
$val = match (true) {
is_array($this->second) => array_values(Row::from($value)->select(...$this->second)),
default => Row::from($value)->value($this->second),
};
if ($this->operator instanceof Closure) {
return ($this->operator)(Row::from($value)->value($this->first), $val);
}
return Column::filterOn($this->first, $this->operator, $val)($value, $key);
}
public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}
}

57
vendor/league/csv/src/Query/Limit.php vendored Normal file
View File

@@ -0,0 +1,57 @@
<?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\Query;
use ArrayIterator;
use Iterator;
use IteratorIterator;
use LimitIterator;
use Traversable;
final class Limit
{
private function __construct(
public readonly int $offset,
public readonly int $length,
){
if (0 > $this->offset) {
throw new QueryException(self::class.' expects the offset to be greater or equal to 0, '.$this->offset.' given.');
}
if (-1 > $this->length) {
throw new QueryException(self::class.' expects the length to be greater or equal to -1, '.$this->length.' given.');
}
}
public static function new(int $offset, int $length): self
{
return new self($offset, $length);
}
/**
* Allows iteration over a limited subset of items in an iterable structure.
*/
public function slice(iterable $value): LimitIterator
{
return new LimitIterator(
match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
},
$this->offset,
$this->length,
);
}
}

View File

@@ -0,0 +1,111 @@
<?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\Query\Ordering;
use ArrayIterator;
use Closure;
use Iterator;
use League\Csv\Query\Row;
use League\Csv\Query\Sort;
use League\Csv\Query\QueryException;
use OutOfBoundsException;
use ReflectionException;
use function is_array;
use function is_string;
use function iterator_to_array;
use function strtoupper;
use function trim;
/**
* Enable sorting a record based on the value of a one of its cell.
*/
final class Column implements Sort
{
private const ASCENDING = 'ASC';
private const DESCENDING = 'DESC';
/**
* @param Closure(mixed, mixed): int $callback
*/
private function __construct(
public readonly string $direction,
public readonly string|int $column,
public readonly Closure $callback,
) {
}
/**
* @param ?Closure(mixed, mixed): int $callback
*
* @throws QueryException
*/
public static function sortOn(
string|int $column,
string|int $direction,
?Closure $callback = null
): self {
$operator = match (true) {
SORT_ASC === $direction => self::ASCENDING,
SORT_DESC === $direction => self::DESCENDING,
is_string($direction) => match (strtoupper(trim($direction))) {
'ASC', 'ASCENDING', 'UP' => self::ASCENDING,
'DESC', 'DESCENDING', 'DOWN' => self::DESCENDING,
default => throw new QueryException('Unknown or unsupported ordering operator value: '.$direction),
},
default => throw new QueryException('Unknown or unsupported ordering operator value: '.$direction),
};
return new self(
$operator,
$column,
$callback ?? static fn (mixed $first, mixed $second): int => $first <=> $second
);
}
/**
* @throws ReflectionException
* @throws QueryException
*/
public function __invoke(mixed $valueA, mixed $valueB): int
{
$first = Row::from($valueA)->value($this->column);
$second = Row::from($valueB)->value($this->column);
return match ($this->direction) {
self::ASCENDING => ($this->callback)($first, $second),
default => ($this->callback)($second, $first),
};
}
public function sort(iterable $value): Iterator
{
$class = new class () extends ArrayIterator {
public function seek(int $offset): void
{
try {
parent::seek($offset);
} catch (OutOfBoundsException) {
return;
}
}
};
$it = new $class(!is_array($value) ? iterator_to_array($value) : $value);
$it->uasort($this);
return $it;
}
}

View File

@@ -0,0 +1,124 @@
<?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\Query\Ordering;
use ArrayIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query\Sort;
use League\Csv\Query\SortCombinator;
use OutOfBoundsException;
use Traversable;
use function array_map;
/**
* Enable sorting a record based on multiple column.
*
* The class can be used with PHP's usort and uasort functions.
*
* @phpstan-import-type Ordering from SortCombinator
* @phpstan-import-type OrderingExtended from SortCombinator
*/
final class MultiSort implements SortCombinator
{
/** @var array<Ordering> */
private readonly array $sorts;
/**
* @param OrderingExtended ...$sorts
*/
private function __construct(Sort|Closure|callable ...$sorts)
{
$this->sorts = array_map(
static fn (Sort|Closure|callable $sort): Sort|Closure => $sort instanceof Closure || $sort instanceof Sort ? $sort : $sort(...),
$sorts
);
}
/**
* @param OrderingExtended ...$sorts
*/
public static function all(Sort|Closure|callable ...$sorts): self
{
return new self(...$sorts);
}
/**
* @param OrderingExtended ...$sorts
*/
public function append(Sort|Closure|callable ...$sorts): self
{
if ([] === $sorts) {
return $this;
}
return new self(...$this->sorts, ...$sorts);
}
/**
* @param OrderingExtended ...$sorts
*/
public function prepend(Sort|Closure|callable ...$sorts): self
{
if ([] === $sorts) {
return $this;
}
return (new self(...$sorts))->append(...$this->sorts);
}
public function __invoke(mixed $valueA, mixed $valueB): int
{
foreach ($this->sorts as $sort) {
if (0 !== ($result = $sort($valueA, $valueB))) {
return $result;
}
}
return $result ?? 0;
}
public function sort(iterable $value): Iterator
{
if ([] === $this->sorts) {
return match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
};
}
$class = new class () extends ArrayIterator {
public function seek(int $offset): void
{
try {
parent::seek($offset);
} catch (OutOfBoundsException) {
return;
}
}
};
if (!is_array($value)) {
$value = iterator_to_array($value);
}
$it = new $class($value);
$it->uasort($this);
return $it;
}
}

View File

@@ -0,0 +1,43 @@
<?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\Query;
use Iterator;
/**
* Enable filtering a record based on its value and/or its offset.
*
* The class can be used directly with PHP's
* <ol>
* <li>array_filter with the ARRAY_FILTER_USE_BOTH flag.</li>
* <li>CallbackFilterIterator class.</li>
* </ol>
*/
interface Predicate
{
/**
* The class predicate method.
*
* Evaluates each element of an iterable structure based on its value and its offset.
* The method must return true if the predicate is satisfied, false otherwise.
*/
public function __invoke(mixed $value, string|int $key): bool;
/**
* Filters elements of an iterable structure using the class predicate method.
*
* @see Predicate::__invoke
*/
public function filter(iterable $value): Iterator;
}

View File

@@ -0,0 +1,71 @@
<?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\Query;
use Closure;
/**
* @phpstan-type Condition Predicate|Closure(mixed, array-key): bool
* @phpstan-type ConditionExtended Predicate|Closure(mixed, array-key): bool|callable(mixed, array-key): bool
*/
interface PredicateCombinator extends Predicate
{
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the AND Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function and(Predicate|Closure ...$predicates): self;
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the OR Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function or(Predicate|Closure ...$predicates): self;
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the NOT Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function not(Predicate|Closure ...$predicates): self;
/**
* Return an instance with the specified predicates
* joined together and with the current predicate
* using the XOR Logical operator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified changes.
*
* @param Condition ...$predicates
*/
public function xor(Predicate|Closure ...$predicates): self;
}

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\Query;
use League\Csv\UnableToProcessCsv;
use Exception;
final class QueryException extends Exception implements UnableToProcessCsv
{
public static function dueToUnknownColumn(string|int $column, array|object $value): self
{
return match (true) {
is_object($value) => match (is_int($column)) {
true => new self('The object property name can not be the integer`' . $column . '`.'),
default => new self('The object property name `' . $column . '` could not be retrieved from the object.'),
},
default => match (is_string($column)) {
true => new self('The column `' . $column . '` does not exist in the input array.'),
default => new self('The column with the offset `' . $column . '` does not exist in the input array.'),
},
};
}
public static function dueToMissingColumn(): self
{
return new self('No valid column were found with the given data.');
}
public static function dueToUnknownOperator(string $operator): self
{
return new self('Unknown or unsupported comparison operator `'.$operator.'`');
}
}

178
vendor/league/csv/src/Query/Row.php vendored Normal file
View File

@@ -0,0 +1,178 @@
<?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\Query;
use ArrayAccess;
use ReflectionException;
use ReflectionObject;
use TypeError;
use function array_is_list;
use function array_key_exists;
use function array_map;
use function array_values;
use function count;
use function explode;
use function implode;
use function is_array;
use function is_int;
use function is_object;
use function lcfirst;
use function str_replace;
final class Row
{
public static function from(mixed $value): Row
{
return new self(match (true) {
is_object($value),
is_array($value) => $value,
default => throw new TypeError('The value must be an array or an object; received '.gettype($value).'.'),
});
}
private function __construct(private readonly array|object $row)
{
}
/**
* Tries to retrieve a single value from a record.
*
* @throws ReflectionException
* @throws QueryException If the value can not be retrieved
* @see Row::select()
*
*/
public function value(string|int $key): mixed
{
return $this->select($key)[$key];
}
/**
* Tries to retrieve multiple values from a record.
*
* If the value is an array and the key is an integer the content will be retrieved
* from the array_values array form. Negative offset are supported.
* If the value is an object, the key MUST be a string.
*
* @throws ReflectionException
* @throws QueryException If the value can not be retrieved
*
* @return non-empty-array<array-key, mixed>
*/
public function select(string|int ...$key): array
{
return match (true) {
is_object($this->row) => self::getObjectPropertyValue($this->row, ...$key),
default => self::getArrayEntry($this->row, ...$key),
};
}
/**
* @throws QueryException
*
* @return non-empty-array<array-key, mixed>
*/
private function getArrayEntry(array $row, string|int ...$keys): array
{
$res = [];
$arrValues = array_values($row);
foreach ($keys as $key) {
if (array_key_exists($key, $res)) {
continue;
}
$offset = $key;
if (is_int($offset)) {
if (!array_is_list($row)) {
$row = $arrValues;
}
if ($offset < 0) {
$offset += count($row);
}
}
$res[$key] = array_key_exists($offset, $row) ? $row[$offset] : throw QueryException::dueToUnknownColumn($key, $row);
}
return [] !== $res ? $res : throw QueryException::dueToMissingColumn();
}
/**
* @throws ReflectionException
* @throws QueryException
*
* @return non-empty-array<array-key, mixed>
*/
private static function getObjectPropertyValue(object $row, string|int ...$keys): array
{
$res = [];
$object = new ReflectionObject($row);
foreach ($keys as $key) {
if (array_key_exists($key, $res)) {
continue;
}
if (is_int($key)) {
throw QueryException::dueToUnknownColumn($key, $row);
}
if ($object->hasProperty($key) && $object->getProperty($key)->isPublic()) {
$res[$key] = $object->getProperty($key)->getValue($row);
continue;
}
$methodNameList = [$key];
if (($camelCasedKey = self::camelCase($key)) !== $key) {
$methodNameList[] = $camelCasedKey;
}
$methodNameList[] = self::camelCase($key, 'get');
foreach ($methodNameList as $methodName) {
if ($object->hasMethod($methodName)
&& $object->getMethod($methodName)->isPublic()
&& 1 > $object->getMethod($methodName)->getNumberOfRequiredParameters()
) {
$res[$key] = $object->getMethod($methodName)->invoke($row);
continue 2;
}
}
if (method_exists($row, '__call')) {
$res[$key] = $object->getMethod('__call')->invoke($row, $methodNameList[1]);
continue;
}
if ($row instanceof ArrayAccess && $row->offsetExists($key)) {
$res[$key] = $row->offsetGet($key);
continue;
}
throw QueryException::dueToUnknownColumn($key, $row);
}
return [] !== $res ? $res : throw QueryException::dueToMissingColumn();
}
private static function camelCase(string $value, string $prefix = ''): string
{
if ('' !== $prefix) {
$prefix .= '_';
}
return lcfirst(implode('', array_map(
ucfirst(...),
explode(' ', str_replace(['-', '_'], ' ', $prefix.$value))
)));
}
}

43
vendor/league/csv/src/Query/Sort.php vendored Normal file
View File

@@ -0,0 +1,43 @@
<?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\Query;
use Iterator;
/**
* Enable sorting a record based on its value.
*
* The class can be used directly with PHP's
* <ol>
* <li>usort and uasort.</li>
* <li>ArrayIterator::uasort.</li>
* <li>ArrayObject::uasort.</li>
* </ol>
*/
interface Sort
{
/**
* The class comparison method.
*
* The method must return an integer less than, equal to, or greater than zero
* if the first argument is considered to be respectively less than, equal to,
* or greater than the second.
*/
public function __invoke(mixed $valueA, mixed $valueB): int;
/**
* Sort an iterable structure with the class comparison method and maintain index association.
*/
public function sort(iterable $value): Iterator;
}

View File

@@ -0,0 +1,46 @@
<?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\Query;
use Closure;
use League\Csv\Query\Sort;
/**
* @phpstan-type Ordering Sort|Closure(mixed, mixed): int
* @phpstan-type OrderingExtended Sort|Closure(mixed, mixed): int|callable(mixed, mixed): int
*/
interface SortCombinator extends Sort
{
/**
* Return an instance with the specified sorting algorithm
* added after the currently registered sorting algorithms.
*
* This method MUST retain the state of the current instance,
* and return an instance that contains the specified changes.
*
* @param Ordering ...$sorts
*/
public function append(Sort|Closure ...$sorts): self;
/**
* Return an instance with the specified sorting algorithm
* added before the currently registered sorting algorithms.
*
* This method MUST retain the state of the current instance,
* and return an instance that contains the specified changes.
*
* @param Ordering ...$sorts
*/
public function prepend(Sort|Closure ...$sorts): self;
}

192
vendor/league/csv/src/RFC4180Field.php vendored Normal file
View File

@@ -0,0 +1,192 @@
<?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 TypeError;
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 array<string>
*/
protected array $search = [];
/**
* The replacement value that replace found $search values.
*
* @var array<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->supportsStreamFilterOnWrite() ? STREAM_FILTER_WRITE : STREAM_FILTER_READ,
];
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 an 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
*/
public function filter($in, $out, &$consumed, bool $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 (!is_array($this->params)) {
throw new TypeError('The filter parameters must be an array.');
}
static $mode_list = [STREAM_FILTER_READ => 1, STREAM_FILTER_WRITE => 1];
$state = isset($this->params['enclosure'], $this->params['escape'], $this->params['mode'], $mode_list[$this->params['mode']])
&& 1 === strlen($this->params['enclosure'])
&& 1 === strlen($this->params['escape']);
if (false === $state) {
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;
}
/**
* @codeCoverageIgnore
* 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.
*/
protected function isValidSequence(array $params): bool
{
return isset($params['whitespace_replace'])
&& strlen($params['whitespace_replace']) === strcspn($params['whitespace_replace'], self::$force_enclosure);
}
}

619
vendor/league/csv/src/Reader.php vendored Normal file
View File

@@ -0,0 +1,619 @@
<?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 Closure;
use Iterator;
use JsonSerializable;
use League\Csv\Serializer\Denormalizer;
use League\Csv\Serializer\MappingFailed;
use League\Csv\Serializer\TypeCastingFailed;
use SplFileObject;
use function array_filter;
use function array_unique;
use function is_array;
use function iterator_count;
use function strlen;
use function substr;
use const STREAM_FILTER_READ;
/**
* A class to parse and read records from a CSV document.
*
* @template TValue of array
*/
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 = [];
/** @var array<callable> callable collection to format the record before reading. */
protected array $formatters = [];
public static function createFromPath(string $path, string $open_mode = 'r', $context = null): static
{
return parent::createFromPath($path, $open_mode, $context);
}
/**
* Adds a record formatter.
*/
public function addFormatter(callable $formatter): self
{
$this->formatters[] = $formatter;
return $this;
}
/**
* 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
*/
public function setHeaderOffset(?int $offset): static
{
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;
}
/**
* Enables skipping empty records.
*/
public function skipEmptyRecords(): static
{
if ($this->is_empty_records_included) {
$this->is_empty_records_included = false;
$this->nb_records = -1;
}
return $this;
}
/**
* Disables skipping empty records.
*/
public function includeEmptyRecords(): static
{
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;
}
protected function resetProperties(): void
{
parent::resetProperties();
$this->nb_records = -1;
$this->header = [];
}
/**
* Returns the header offset.
*/
public function getHeaderOffset(): ?int
{
return $this->header_offset;
}
/**
* @throws SyntaxError
*
* Returns the header record.
*/
public function getHeader(): array
{
return match (true) {
null === $this->header_offset,
[] !== $this->header => $this->header,
default => ($this->header = $this->setHeader($this->header_offset)),
};
}
/**
* Determines the CSV record header.
*
* @throws SyntaxError 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
{
$inputBom = null;
$header = $this->seekRow($offset);
if (0 === $offset) {
$inputBom = Bom::tryFrom($this->getInputBOM());
$header = $this->removeBOM(
$header,
!$this->is_input_bom_included ? $inputBom?->length() ?? 0 : 0,
$this->enclosure
);
}
return match (true) {
[] === $header,
[null] === $header,
[false] === $header,
[''] === $header && 0 === $offset && null !== $inputBom => throw SyntaxError::dueToHeaderNotFound($offset),
default => $header,
};
}
/**
* @throws Exception
*
* Returns the row at a given offset.
*/
protected function seekRow(int $offset): array
{
$this->getDocument()->seek($offset);
$record = $this->document->current();
return match (true) {
false === $record => [],
default => (array) $record,
};
}
/**
* @throws Exception
*
* Returns the document as an Iterator.
*/
protected function getDocument(): SplFileObject|Stream
{
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$this->document->rewind();
return $this->document;
}
/**
* Strips the BOM sequence from a record.
*
* @param array<string> $record
*
* @return array<string>
*/
protected function removeBOM(array $record, int $bom_length, string $enclosure): array
{
if ([] === $record || !is_string($record[0]) || 0 === $bom_length || strlen($record[0]) < $bom_length) {
return $record;
}
$record[0] = 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 fetchColumn(string|int $index = 0): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchColumn($index);
}
/**
* @throws Exception
*/
public function fetchColumnByName(string $name): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchColumnByName($name);
}
/**
* @throws Exception
*/
public function fetchColumnByOffset(int $offset = 0): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchColumnByOffset($offset);
}
public function value(int|string $column = 0): mixed
{
return ResultSet::createFromTabularDataReader($this)->value($column);
}
/**
* @throws Exception
*/
public function first(): array
{
return ResultSet::createFromTabularDataReader($this)->first();
}
/**
* @throws Exception
*/
public function nth(int $nth_record): array
{
return ResultSet::createFromTabularDataReader($this)->nth($nth_record);
}
/**
* @param class-string $className
*
* @throws Exception
*/
public function nthAsObject(int $nth, string $className, array $header = []): ?object
{
return ResultSet::createFromTabularDataReader($this)->nthAsObject($nth, $className, $header);
}
/**
* @param class-string $className
*
* @throws Exception
*/
public function firstAsObject(string $className, array $header = []): ?object
{
return ResultSet::createFromTabularDataReader($this)->firstAsObject($className, $header);
}
public function fetchPairs($offset_index = 0, $value_index = 1): Iterator
{
return ResultSet::createFromTabularDataReader($this)->fetchPairs($offset_index, $value_index);
}
/**
* @throws Exception
*/
public function count(): int
{
if (-1 === $this->nb_records) {
$this->nb_records = iterator_count($this->getRecords());
}
return $this->nb_records;
}
/**
* @throws Exception
*/
public function getIterator(): Iterator
{
return $this->getRecords();
}
/**
* @throws Exception
*/
public function jsonSerialize(): array
{
return array_values([...$this->getRecords()]);
}
/**
* @param Closure(array<mixed>, array-key=): (void|bool|null) $callback
*/
public function each(Closure $callback): bool
{
return ResultSet::createFromTabularDataReader($this)->each($callback);
}
/**
* @param Closure(array<mixed>, array-key=): bool $callback
*/
public function exists(Closure $callback): bool
{
return ResultSet::createFromTabularDataReader($this)->exists($callback);
}
/**
* @param Closure(TInitial|null, array<mixed>, array-key=): TInitial $callback
* @param TInitial|null $initial
*
* @template TInitial
*
* @return TInitial|null
*/
public function reduce(Closure $callback, mixed $initial = null): mixed
{
return ResultSet::createFromTabularDataReader($this)->reduce($callback, $initial);
}
/**
* @param positive-int $recordsCount
*
* @throws InvalidArgument
*
* @return iterable<TabularDataReader>
*/
public function chunkBy(int $recordsCount): iterable
{
return ResultSet::createFromTabularDataReader($this)->chunkBy($recordsCount);
}
/**
* @param array<string> $headers
*/
public function mapHeader(array $headers): TabularDataReader
{
return Statement::create()->process($this, $headers);
}
/**
* @param \League\Csv\Query\Predicate|Closure(array, array-key): bool $predicate
*
* @throws Exception
* @throws SyntaxError
*/
public function filter(Query\Predicate|Closure $predicate): TabularDataReader
{
return Statement::create()->where($predicate)->process($this);
}
/**
* @param int<0, max> $offset
* @param int<-1, max> $length
*
* @throws Exception
* @throws SyntaxError
*/
public function slice(int $offset, int $length = -1): TabularDataReader
{
return Statement::create()->offset($offset)->limit($length)->process($this);
}
/**
* @param Closure(mixed, mixed): int $orderBy
*
* @throws Exception
* @throws SyntaxError
*/
public function sorted(Query\Sort|Closure $orderBy): TabularDataReader
{
return Statement::create()->orderBy($orderBy)->process($this);
}
public function matching(string $expression): iterable
{
return FragmentFinder::create()->findAll($expression, $this);
}
public function matchingFirst(string $expression): ?TabularDataReader
{
return FragmentFinder::create()->findFirst($expression, $this);
}
/**
* @throws SyntaxError
* @throws FragmentNotFound
*/
public function matchingFirstOrFail(string $expression): TabularDataReader
{
return FragmentFinder::create()->findFirstOrFail($expression, $this);
}
public function select(string|int ...$columns): TabularDataReader
{
return ResultSet::createFromTabularDataReader($this)->select(...$columns);
}
/**
* @param array<string> $header
*
* @throws Exception
*
* @return Iterator<array-key, TValue>
*/
public function getRecords(array $header = []): Iterator
{
return $this->combineHeader(
$this->prepareRecords(),
$this->prepareHeader($header)
);
}
/**
* @template T of object
* @param class-string<T> $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*
* @return iterator<T>
*/
public function getRecordsAsObject(string $className, array $header = []): Iterator
{
/** @var array<string> $header */
$header = $this->prepareHeader($header);
return Denormalizer::assignAll(
$className,
$this->combineHeader($this->prepareRecords(), $header),
$header
);
}
/**
* @throws Exception
*/
protected function prepareRecords(): Iterator
{
$normalized = fn ($record): bool => is_array($record) && ($this->is_empty_records_included || $record !== [null]);
$bom = null;
if (!$this->is_input_bom_included) {
$bom = Bom::tryFrom($this->getInputBOM());
}
$records = $this->stripBOM(new CallbackFilterIterator($this->getDocument(), $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) {
$records = new MapIterator($records, fn (array $record): array => ([null] === $record) ? [] : $record);
}
return $records;
}
/**
* Strips the BOM sequence from the returned records if necessary.
*/
protected function stripBOM(Iterator $iterator, ?Bom $bom): Iterator
{
if (null === $bom) {
return $iterator;
}
$bomLength = $bom->length();
$mapper = function (array $record, int $index) use ($bomLength): array {
if (0 !== $index) {
return $record;
}
$record = $this->removeBOM($record, $bomLength, $this->enclosure);
return match ($record) {
[''] => [null],
default => $record,
};
};
return new CallbackFilterIterator(
new MapIterator($iterator, $mapper),
fn (array $record): bool => $this->is_empty_records_included || $record !== [null]
);
}
/**
* @param array<string> $header
*
* @throws SyntaxError
*
* @return array<int|string>
*/
protected function prepareHeader($header = []): array
{
if ($header !== (array_filter($header, is_string(...)))) {
throw SyntaxError::dueToInvalidHeaderColumnNames();
}
return $this->computeHeader($header);
}
/**
* Returns the header to be used for iteration.
*
* @param array<int|string> $header
*
* @throws SyntaxError If the header contains non unique column name
*
* @return array<int|string>
*/
protected function computeHeader(array $header): array
{
if ([] === $header) {
$header = $this->getHeader();
}
return match (true) {
$header !== array_unique($header) => throw SyntaxError::dueToDuplicateHeaderColumnNames($header),
[] !== array_filter(array_keys($header), fn (string|int $value) => !is_int($value) || $value < 0) => throw new SyntaxError('The header mapper indexes should only contain positive integer or 0.'),
default => $header,
};
}
protected function combineHeader(Iterator $iterator, array $header): Iterator
{
$formatter = fn (array $record): array => array_reduce(
$this->formatters,
fn (array $record, callable $formatter): array => $formatter($record),
$record
);
return match ([]) {
$header => new MapIterator($iterator, $formatter(...)),
default => new MapIterator($iterator, function (array $record) use ($header, $formatter): array {
$assocRecord = [];
foreach ($header as $offset => $headerName) {
$assocRecord[$headerName] = $record[$offset] ?? null;
}
return $formatter($assocRecord);
}),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Reader::nth()
* @deprecated since version 9.9.0
* @codeCoverageIgnore
*/
public function fetchOne(int $nth_record = 0): array
{
return $this->nth($nth_record);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Reader::getRecordsAsObject()
* @deprecated Since version 9.15.0
* @codeCoverageIgnore
*
* @param class-string $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*/
public function getObjects(string $className, array $header = []): Iterator
{
return $this->getRecordsAsObject($className, $header);
}
}

562
vendor/league/csv/src/ResultSet.php vendored Normal file
View File

@@ -0,0 +1,562 @@
<?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 Closure;
use Generator;
use Iterator;
use IteratorIterator;
use JsonSerializable;
use League\Csv\Serializer\Denormalizer;
use League\Csv\Serializer\MappingFailed;
use League\Csv\Serializer\TypeCastingFailed;
use LimitIterator;
use Traversable;
use function array_filter;
use function array_flip;
use function array_key_exists;
use function array_reduce;
use function array_search;
use function array_values;
use function is_string;
use function iterator_count;
/**
* Represents the result set of a {@link Reader} processed by a {@link Statement}.
*
* @template TValue of array
*/
class ResultSet implements TabularDataReader, JsonSerializable
{
/** @var array<string> */
protected array $header;
/* @var Iterator<array-key, array<array-key, mixed>> */
protected Iterator $records;
/**
* @param Iterator|array<array-key, array<array-key, mixed>> $records
* @param array<string> $header
*
* @throws SyntaxError
*/
public function __construct(Iterator|array $records, array $header = [])
{
if ($header !== array_filter($header, is_string(...))) {
throw SyntaxError::dueToInvalidHeaderColumnNames();
}
$this->header = array_values($this->validateHeader($header));
$this->records = match (true) {
$records instanceof Iterator => $records,
default => new ArrayIterator($records),
};
}
/**
* @throws SyntaxError if the header syntax is invalid
*/
protected function validateHeader(array $header): array
{
return match (true) {
$header !== array_unique($header) => throw SyntaxError::dueToDuplicateHeaderColumnNames($header),
[] !== array_filter(array_keys($header), fn (string|int $value) => !is_int($value) || $value < 0) => throw new SyntaxError('The header mapper indexes should only contain positive integer or 0.'),
default => $header,
};
}
public function __destruct()
{
unset($this->records);
}
/**
* Returns a new instance from an object implementing the TabularDataReader interface.
*
* @throws SyntaxError
*/
public static function createFromTabularDataReader(TabularDataReader $reader): self
{
return new self($reader->getRecords(), $reader->getHeader());
}
/**
* Returns a new instance from a collection without header.
*
* @throws SyntaxError
*/
public static function createFromRecords(iterable $records = []): self
{
return new self(match (true) {
$records instanceof Iterator => $records,
$records instanceof Traversable => new IteratorIterator($records),
default => new ArrayIterator($records),
});
}
/**
* Returns the header associated with the result set.
*
* @return array<string>
*/
public function getHeader(): array
{
return $this->header;
}
/**
* @throws SyntaxError
*/
public function getIterator(): Iterator
{
return $this->getRecords();
}
/**
* @param Closure(array<mixed>, array-key=): mixed $callback
*/
public function each(Closure $callback): bool
{
foreach ($this as $offset => $record) {
if (false === $callback($record, $offset)) {
return false;
}
}
return true;
}
/**
* @param Closure(array<mixed>, array-key=): bool $callback
*/
public function exists(Closure $callback): bool
{
foreach ($this as $offset => $record) {
if (true === $callback($record, $offset)) {
return true;
}
}
return false;
}
/**
* @param Closure(TInitial|null, array<mixed>, array-key=): TInitial $callback
* @param TInitial|null $initial
*
* @template TInitial
*
* @return TInitial|null
*/
public function reduce(Closure $callback, mixed $initial = null): mixed
{
foreach ($this as $offset => $record) {
$initial = $callback($initial, $record, $offset);
}
return $initial;
}
/**
* @param positive-int $recordsCount
*
* @throws InvalidArgument
*
* @return iterable<TabularDataReader>
*/
public function chunkBy(int $recordsCount): iterable
{
if ($recordsCount < 1) {
throw InvalidArgument::dueToInvalidChunkSize($recordsCount, __METHOD__);
}
$header = $this->getHeader();
$records = [];
$nbRecords = 0;
foreach ($this->getRecords() as $record) {
$records[] = $record;
++$nbRecords;
if ($nbRecords === $recordsCount) {
yield new self($records, $header);
$records = [];
$nbRecords = 0;
}
}
if ([] !== $records) {
yield new self($records, $header);
}
}
/**
* @param array<string> $headers
*/
public function mapHeader(array $headers): TabularDataReader
{
return Statement::create()->process($this, $headers);
}
public function filter(Query\Predicate|Closure $predicate): TabularDataReader
{
return Statement::create()->where($predicate)->process($this);
}
public function slice(int $offset, ?int $length = null): TabularDataReader
{
return Statement::create()->offset($offset)->limit($length ?? -1)->process($this);
}
public function sorted(Query\Sort|Closure $orderBy): TabularDataReader
{
return Statement::create()->orderBy($orderBy)->process($this);
}
public function select(string|int ...$columns): TabularDataReader
{
if ([] === $columns) {
return $this;
}
$recordsHeader = $this->getHeader();
$hasHeader = [] !== $recordsHeader;
$selectColumn = function (array $header, string|int $field) use ($recordsHeader, $hasHeader): array {
if (is_string($field)) {
$index = array_search($field, $recordsHeader, true);
if (false === $index) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$index] = $field;
return $header;
}
if ($hasHeader && !array_key_exists($field, $recordsHeader)) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$field] = $recordsHeader[$field] ?? $field;
return $header;
};
/** @var array<string> $header */
$header = array_reduce($columns, $selectColumn, []);
$callback = function (array $record) use ($header): array {
$element = [];
$row = array_values($record);
foreach ($header as $offset => $headerName) {
$element[$headerName] = $row[$offset] ?? null;
}
return $element;
};
return new self(new MapIterator($this, $callback), $hasHeader ? $header : []);
}
public function matching(string $expression): iterable
{
return FragmentFinder::create()->findAll($expression, $this);
}
public function matchingFirst(string $expression): ?TabularDataReader
{
return FragmentFinder::create()->findFirst($expression, $this);
}
/**
* @throws SyntaxError
* @throws FragmentNotFound
*/
public function matchingFirstOrFail(string $expression): TabularDataReader
{
return FragmentFinder::create()->findFirstOrFail($expression, $this);
}
/**
* @param array<string> $header
*
* @throws Exception
*
* @return Iterator<array-key, TValue>
*/
public function getRecords(array $header = []): Iterator
{
return $this->combineHeader($this->prepareHeader($header));
}
/**
* @template T of object
* @param class-string<T> $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
* @return iterator<T>
*/
public function getRecordsAsObject(string $className, array $header = []): Iterator
{
$header = $this->prepareHeader($header);
return Denormalizer::assignAll(
$className,
$this->combineHeader($header),
$header
);
}
/**
* @param array<string> $header
*
* @throws SyntaxError
* @return array<string>
*/
protected function prepareHeader(array $header): array
{
if ($header !== array_filter($header, is_string(...))) {
throw SyntaxError::dueToInvalidHeaderColumnNames();
}
$header = $this->validateHeader($header);
if ([] === $header) {
$header = $this->header;
}
return $header;
}
/**
* Combines the header to each record if present.
*
* @param array<array-key, string|int> $header
*
* @return Iterator<array-key, TValue>
*/
protected function combineHeader(array $header): Iterator
{
return match (true) {
[] === $header => $this->records,
default => new MapIterator($this->records, function (array $record) use ($header): array {
$assocRecord = [];
$row = array_values($record);
foreach ($header as $offset => $headerName) {
$assocRecord[$headerName] = $row[$offset] ?? null;
}
return $assocRecord;
}),
};
}
public function count(): int
{
return iterator_count($this->records);
}
public function jsonSerialize(): array
{
return array_values([...$this->records]);
}
public function first(): array
{
return $this->nth(0);
}
public function value(int|string $column = 0): mixed
{
return match (true) {
is_string($column) => $this->first()[$column] ?? null,
default => array_values($this->first())[$column] ?? null,
};
}
public function nth(int $nth_record): array
{
if ($nth_record < 0) {
throw InvalidArgument::dueToInvalidRecordOffset($nth_record, __METHOD__);
}
$iterator = new LimitIterator($this->getIterator(), $nth_record, 1);
$iterator->rewind();
/** @var array|null $result */
$result = $iterator->current();
return $result ?? [];
}
/**
* @param class-string $className
*
* @throws InvalidArgument
*/
public function nthAsObject(int $nth, string $className, array $header = []): ?object
{
$header = $this->prepareHeader($header);
$record = $this->nth($nth);
if ([] === $record) {
return null;
}
if ([] === $header || $this->header === $header) {
return Denormalizer::assign($className, $record);
}
$row = array_values($record);
$record = [];
foreach ($header as $offset => $headerName) {
$record[$headerName] = $row[$offset] ?? null;
}
return Denormalizer::assign($className, $record);
}
/**
* @param class-string $className
*
* @throws InvalidArgument
*/
public function firstAsObject(string $className, array $header = []): ?object
{
return $this->nthAsObject(0, $className, $header);
}
public function fetchColumn(string|int $index = 0): Iterator
{
return $this->yieldColumn(
$this->getColumnIndex($index, 'offset', __METHOD__)
);
}
/**
* @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__)
);
}
protected function yieldColumn(string|int $offset): Generator
{
yield from new MapIterator(
new CallbackFilterIterator($this->records, fn (array $record): bool => isset($record[$offset])),
fn (array $record): string => $record[$offset]
);
}
/**
* Filters a column name against the header if any.
*
* @throws InvalidArgument if the field is invalid or not found
*/
protected function getColumnIndex(string|int $field, string $type, string $method): string|int
{
return match (true) {
is_string($field) => $this->getColumnIndexByValue($field, $type, $method),
default => $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
{
return match (true) {
false === array_search($value, $this->header, true) => throw InvalidArgument::dueToInvalidColumnIndex($value, $type, $method),
default => $value,
};
}
/**
* Returns the selected column name according to its offset.
*
* @throws InvalidArgument if the field is invalid or not found
*/
protected function getColumnIndexByKey(int $index, string $type, string $method): int|string
{
return match (true) {
$index < 0 => throw InvalidArgument::dueToInvalidColumnIndex($index, $type, $method),
[] === $this->header => $index,
false !== ($value = array_search($index, array_flip($this->header), true)) => $value,
default => throw InvalidArgument::dueToInvalidColumnIndex($index, $type, $method),
};
}
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];
}
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see ResultSet::nth()
* @deprecated since version 9.9.0
* @codeCoverageIgnore
*/
public function fetchOne(int $nth_record = 0): array
{
return $this->nth($nth_record);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Reader::getRecordsAsObject()
* @deprecated Since version 9.15.0
* @codeCoverageIgnore
*
* @param class-string $className
* @param array<string> $header
*
* @throws Exception
* @throws MappingFailed
* @throws TypeCastingFailed
*/
public function getObjects(string $className, array $header = []): Iterator
{
return $this->getRecordsAsObject($className, $header);
}
}

View File

@@ -0,0 +1,28 @@
<?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\Serializer;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class AfterMapping
{
/** @var array<string> $methods */
public readonly array $methods;
public function __construct(string ...$methods)
{
$this->methods = $methods;
}
}

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.
*/
namespace League\Csv\Serializer;
use function in_array;
enum ArrayShape: string
{
case List = 'list';
case Csv = 'csv';
case Json = 'json';
public function equals(mixed $value): bool
{
return $value instanceof self
&& $value === $this;
}
public function isOneOf(self ...$types): bool
{
return in_array($this, $types, true);
}
}

View File

@@ -0,0 +1,324 @@
<?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\Serializer;
use Closure;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use Throwable;
use function array_key_exists;
use function class_exists;
/**
* @internal Container for registering Closure as type and/or type alias casting
* @template TValue
*/
final class CallbackCasting implements TypeCasting
{
/** @var array<string, Closure(?string, bool, mixed...): mixed> */
private static array $types = [];
/** @var array<string, array<string, Closure(?string, bool, mixed...): mixed>> */
private static array $aliases = [];
private string $type;
private readonly bool $isNullable;
/** @var Closure(?string, bool, mixed...): mixed */
private Closure $callback;
private array $options = [];
private string $message;
public function __construct(
ReflectionProperty|ReflectionParameter $reflectionProperty,
private readonly ?string $alias = null
) {
[$this->type, $this->isNullable] = self::resolve($reflectionProperty);
$this->message = match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
$reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.',
};
$this->callback = fn (?string $value, bool $isNullable, mixed ...$arguments): ?string => $value;
}
/**
* @throws MappingFailed
*/
public function setOptions(?string $type = null, mixed ...$options): void
{
if (null === $this->alias) {
if (Type::Mixed->value === $this->type && null !== $type) {
$this->type = $type;
}
if (array_key_exists($this->type, self::$types)) {
$this->callback = self::$types[$this->type];
$this->options = $options;
return;
}
throw new MappingFailed($this->message);
}
if (Type::Mixed->value === $this->type) {
$this->type = self::aliases()[$this->alias];
}
/** @var Closure $callback */
$callback = self::$aliases[$this->type][$this->alias];
$this->callback = $callback;
$this->options = $options;
}
/**
* @return TValue
*/
public function toVariable(?string $value): mixed
{
try {
return ($this->callback)($value, $this->isNullable, ...$this->options);
} catch (Throwable $exception) {
if ($exception instanceof TypeCastingFailed) {
throw $exception;
}
if (null === $value) {
throw TypeCastingFailed::dueToNotNullableType($this->type, $exception);
}
throw TypeCastingFailed::dueToInvalidValue(match (true) {
'' === $value => 'empty string',
default => $value,
}, $this->type, $exception);
}
}
/**
* @param Closure(?string, bool, mixed...): TValue $callback
*/
public static function register(string $type, Closure $callback, ?string $alias = null): void
{
if (null === $alias) {
self::$types[$type] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
};
return;
}
if (1 !== preg_match('/^@\w+$/', $alias)) {
throw new MappingFailed("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
}
foreach (self::$aliases as $aliases) {
foreach ($aliases as $registeredAlias => $__) {
if ($alias === $registeredAlias) {
throw new MappingFailed("The alias `$alias` is already registered. Please choose another name.");
}
}
}
self::$aliases[$type][$alias] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
};
}
public static function unregisterType(string $type): bool
{
if (!array_key_exists($type, self::$types)) {
return false;
}
unset(self::$types[$type]);
return true;
}
public static function unregisterTypes(): void
{
self::$types = [];
}
public static function unregisterAlias(string $alias): bool
{
if (1 !== preg_match('/^@\w+$/', $alias)) {
return false;
}
foreach (self::$aliases as $type => $aliases) {
foreach ($aliases as $registeredAlias => $__) {
if ($registeredAlias === $alias) {
unset(self::$aliases[$type][$registeredAlias]);
return true;
}
}
}
return false;
}
public static function unregisterAliases(): void
{
self::$aliases = [];
}
public static function unregisterAll(): void
{
self::$types = [];
self::$aliases = [];
}
public static function supportsAlias(?string $alias): bool
{
return null !== $alias && array_key_exists($alias, self::aliases());
}
public static function supportsType(?string $type): bool
{
return null !== $type && array_key_exists($type, self::$types);
}
/**
* @return array<string>
*/
public static function types(): array
{
return array_keys(self::$types);
}
/**
* @return array<string, string>
*/
public static function aliases(): array
{
$res = [];
foreach (self::$aliases as $registeredType => $aliases) {
foreach ($aliases as $registeredAlias => $__) {
$res[$registeredAlias] = $registeredType;
}
}
return $res;
}
public static function supports(ReflectionParameter|ReflectionProperty $reflectionProperty, ?string $alias = null): bool
{
$propertyTypeList = self::getTypes($reflectionProperty->getType());
if ([] === $propertyTypeList && self::supportsAlias($alias)) {
return true;
}
foreach ($propertyTypeList as $propertyType) {
$type = $propertyType->getName();
if (null === $alias) {
if (array_key_exists($type, self::$types)) {
return true;
}
continue;
}
if ((self::aliases()[$alias] ?? null) === $type || (Type::Mixed->value === $type && self::supportsAlias($alias))) {
return true;
}
}
return false;
}
/**
* @throws MappingFailed
*
* @return array{0:string, 1:bool}
*/
private static function resolve(ReflectionParameter|ReflectionProperty $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed->value, true];
}
$types = self::getTypes($reflectionProperty->getType());
$type = null;
$isNullable = false;
$hasMixed = false;
foreach ($types as $foundType) {
if (!$isNullable && $foundType->allowsNull()) {
$isNullable = true;
}
if (null === $type) {
if (
array_key_exists($foundType->getName(), self::$types)
|| array_key_exists($foundType->getName(), self::$aliases)
) {
$type = $foundType;
}
if (true !== $hasMixed && Type::Mixed->value === $foundType->getName()) {
$hasMixed = true;
}
}
}
return match (true) {
$type instanceof ReflectionNamedType => [$type->getName(), $isNullable],
$hasMixed => [Type::Mixed->value, true],
default => throw new MappingFailed(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
$reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.',
}),
};
}
/**
* @return array<ReflectionNamedType>
*/
private static function getTypes(?ReflectionType $type): array
{
return match (true) {
$type instanceof ReflectionNamedType => [$type],
$type instanceof ReflectionUnionType => array_filter(
$type->getTypes(),
fn (ReflectionType $innerType) => $innerType instanceof ReflectionNamedType
),
default => [],
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.13.0
* @see CallbackCasting::unregisterType()
* @codeCoverageIgnore
*/
public static function unregister(string $type): bool
{
return self::unregisterType($type);
}
}

View File

@@ -0,0 +1,169 @@
<?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\Serializer;
use JsonException;
use ReflectionParameter;
use ReflectionProperty;
use function explode;
use function is_array;
use function json_decode;
use function str_getcsv;
use function strlen;
use const FILTER_REQUIRE_ARRAY;
use const JSON_THROW_ON_ERROR;
/**
* @implements TypeCasting<?array>
*/
final class CastToArray implements TypeCasting
{
private readonly Type $type;
private readonly bool $isNullable;
private ArrayShape $shape;
private int $filterFlag;
/** @var non-empty-string */
private string $separator = ',';
private string $delimiter = '';
private string $enclosure = '"';
/** @var int<1, max> $depth */
private int $depth = 512;
private int $flags = 0;
private ?array $default = null;
/**
* @throws MappingFailed
*/
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->isNullable] = $this->init($reflectionProperty);
$this->shape = ArrayShape::List;
$this->filterFlag = Type::String->filterFlag();
}
/**
* @param non-empty-string $delimiter
* @param non-empty-string $separator
* @param int<1, max> $depth
*
* @throws MappingFailed
*/
public function setOptions(
?array $default = null,
ArrayShape|string $shape = ArrayShape::List,
string $separator = ',',
string $delimiter = ',',
string $enclosure = '"',
int $depth = 512,
int $flags = 0,
Type|string $type = Type::String,
): void {
if (!$shape instanceof ArrayShape) {
$shape = ArrayShape::tryFrom($shape) ?? throw new MappingFailed('Unable to resolve the array shape; Verify your options arguments.');
}
if (!$type instanceof Type) {
$type = Type::tryFrom($type) ?? throw new MappingFailed('Unable to resolve the array value type; Verify your options arguments.');
}
$this->shape = $shape;
$this->depth = $depth;
$this->separator = $separator;
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->flags = $flags;
$this->default = $default;
$this->filterFlag = match (true) {
1 > $this->depth && $this->shape->equals(ArrayShape::Json) => throw new MappingFailed('the json depth can not be less than 1.'),
1 > strlen($this->separator) && $this->shape->equals(ArrayShape::List) => throw new MappingFailed('expects separator to be a non-empty string for list conversion; empty string given.'),
1 !== strlen($this->delimiter) && $this->shape->equals(ArrayShape::Csv) => throw new MappingFailed('expects delimiter to be a single character for CSV conversion; `'.$this->delimiter.'` given.'),
1 !== strlen($this->enclosure) && $this->shape->equals(ArrayShape::Csv) => throw new MappingFailed('expects enclosure to be a single character; `'.$this->enclosure.'` given.'),
default => $this->resolveFilterFlag($type),
};
}
public function toVariable(?string $value): ?array
{
if (null === $value) {
return match (true) {
$this->isNullable,
Type::Mixed->equals($this->type) => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->type->value),
};
}
if ('' === $value) {
return [];
}
try {
$result = match ($this->shape) {
ArrayShape::Json => json_decode($value, true, $this->depth, $this->flags | JSON_THROW_ON_ERROR),
ArrayShape::List => filter_var(explode($this->separator, $value), $this->filterFlag, FILTER_REQUIRE_ARRAY),
ArrayShape::Csv => filter_var(str_getcsv($value, $this->delimiter, $this->enclosure, ''), $this->filterFlag, FILTER_REQUIRE_ARRAY),
};
if (!is_array($result)) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->type->value);
}
return $result;
} catch (JsonException $exception) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->type->value, $exception);
}
}
/**
* @throws MappingFailed if the type is not supported
*/
private function resolveFilterFlag(?Type $type): int
{
return match (true) {
$this->shape->equals(ArrayShape::Json) => Type::String->filterFlag(),
$type instanceof Type && $type->isOneOf(Type::Bool, Type::True, Type::False, Type::String, Type::Float, Type::Int) => $type->filterFlag(),
default => throw new MappingFailed('Only scalar type are supported for `array` value casting.'),
};
}
/**
* @return array{0:Type, 1:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Array, Type::Iterable)) {
$type = $found;
}
}
if (null === $type) {
throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'array', 'iterable', 'mixed');
}
return [$type[0], $isNullable];
}
}

View File

@@ -0,0 +1,89 @@
<?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\Serializer;
use ReflectionParameter;
use ReflectionProperty;
use function filter_var;
/**
* @implements TypeCasting<?bool>
*/
final class CastToBool implements TypeCasting
{
private readonly bool $isNullable;
private readonly Type $type;
private ?bool $default = null;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->isNullable] = $this->init($reflectionProperty);
}
public function setOptions(?bool $default = null): void
{
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(?string $value): ?bool
{
$returnValue = match (true) {
null !== $value => filter_var($value, Type::Bool->filterFlag()),
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType('boolean'),
};
return match (true) {
Type::True->equals($this->type) && true !== $returnValue && !$this->isNullable,
Type::False->equals($this->type) && false !== $returnValue && !$this->isNullable => throw TypeCastingFailed::dueToInvalidValue(match (true) {
null === $value => 'null',
'' === $value => 'empty string',
default => $value,
}, $this->type->value),
default => $returnValue,
};
}
/**
* @return array{0:Type, 1:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Bool, Type::True, Type::False)) {
$type = $found;
}
}
if (null === $type) {
throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'bool', 'mixed');
}
return [$type[0], $isNullable];
}
}

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\Serializer;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use ReflectionClass;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
use function class_exists;
use function is_string;
/**
* @implements TypeCasting<DateTimeImmutable|DateTime|null>
*/
final class CastToDate implements TypeCasting
{
/** @var class-string */
private string $class;
private readonly bool $isNullable;
private DateTimeImmutable|DateTime|null $default = null;
private readonly Type $type;
private readonly string $propertyName;
private ?DateTimeZone $timezone = null;
private ?string $format = null;
/**
* @throws MappingFailed
*/
public function __construct(
ReflectionProperty|ReflectionParameter $reflectionProperty,
) {
[$this->type, $this->class, $this->isNullable] = $this->init($reflectionProperty);
$this->propertyName = $reflectionProperty->getName();
}
/**
* @param ?class-string $className
*
* @throws MappingFailed
*/
public function setOptions(
?string $default = null,
?string $format = null,
DateTimeZone|string|null $timezone = null,
?string $className = null
): void {
$this->class = match (true) {
!interface_exists($this->class) && !Type::Mixed->equals($this->type) => $this->class,
DateTimeInterface::class === $this->class && null === $className => DateTimeImmutable::class,
interface_exists($this->class) && null !== $className && class_exists($className) && (new ReflectionClass($className))->implementsInterface($this->class) => $className,
default => throw new MappingFailed('`'.$this->propertyName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'),
};
try {
$this->format = $format;
$this->timezone = is_string($timezone) ? new DateTimeZone($timezone) : $timezone;
$this->default = (null !== $default) ? $this->cast($default) : $default;
} catch (Throwable $exception) {
throw new MappingFailed('The `timezone` and/or `format` options used for `'.self::class.'` are invalud.', 0, $exception);
}
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(?string $value): DateTimeImmutable|DateTime|null
{
return match (true) {
null !== $value && '' !== $value => $this->cast($value),
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->class),
};
}
/**
* @throws TypeCastingFailed
*/
private function cast(string $value): DateTimeImmutable|DateTime
{
try {
$date = null !== $this->format ?
($this->class)::createFromFormat($this->format, $value, $this->timezone) :
new ($this->class)($value, $this->timezone);
if (false === $date) {
throw TypeCastingFailed::dueToInvalidValue($value, $this->class);
}
} catch (Throwable $exception) {
if ($exception instanceof TypeCastingFailed) {
throw $exception;
}
throw TypeCastingFailed::dueToInvalidValue($value, $this->class, $exception);
}
return $date;
}
/**
* @throws MappingFailed
*
* @return array{0:Type, 1:class-string<DateTimeInterface>, 2:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, DateTimeInterface::class, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Date)) {
$type = $found;
}
}
if (null === $type) {
throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, DateTimeInterface::class, 'mixed');
}
/** @var class-string<DateTimeInterface> $className */
$className = $type[1]->getName();
return [$type[0], $className, $isNullable];
}
}

View File

@@ -0,0 +1,127 @@
<?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\Serializer;
use BackedEnum;
use ReflectionEnum;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
use UnitEnum;
/**
* @implements TypeCasting<BackedEnum|UnitEnum|null>
*/
class CastToEnum implements TypeCasting
{
private readonly bool $isNullable;
private readonly Type $type;
private ?UnitEnum $default = null;
private readonly string $propertyName;
/** @var class-string<UnitEnum|BackedEnum> */
private string $class;
/**
* @throws MappingFailed
*/
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->class, $this->isNullable] = $this->init($reflectionProperty);
$this->propertyName = $reflectionProperty->getName();
}
/**
* @param ?class-string<UnitEnum|BackedEnum> $className *
*
* @throws MappingFailed
*/
public function setOptions(?string $default = null, ?string $className = null): void
{
if (Type::Mixed->equals($this->type) || in_array($this->class, [BackedEnum::class , UnitEnum::class], true)) {
if (null === $className || !enum_exists($className)) {
throw new MappingFailed('`'.$this->propertyName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.');
}
$this->class = $className;
}
try {
$this->default = (null !== $default) ? $this->cast($default) : $default;
} catch (TypeCastingFailed $exception) {
throw new MappingFailed(message:'The `default` option is invalid.', previous: $exception);
}
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(?string $value): BackedEnum|UnitEnum|null
{
return match (true) {
null !== $value => $this->cast($value),
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->class),
};
}
/**
* @throws TypeCastingFailed
*/
private function cast(string $value): BackedEnum|UnitEnum
{
try {
$enum = new ReflectionEnum($this->class);
if (!$enum->isBacked()) {
return $enum->getCase($value)->getValue();
}
$backedValue = 'int' === $enum->getBackingType()->getName() ? filter_var($value, Type::Int->filterFlag()) : $value;
return $this->class::from($backedValue); /* @phpstan-ignore-line */
} catch (Throwable $exception) {
throw throw TypeCastingFailed::dueToInvalidValue($value, $this->class, $exception);
}
}
/**
* @return array{0:Type, 1:class-string<UnitEnum|BackedEnum>, 2:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, UnitEnum::class, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Enum)) {
$type = $found;
}
}
if (null === $type) {
throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'enum', 'mixed');
}
/** @var class-string<UnitEnum|BackedEnum> $className */
$className = $type[1]->getName();
return [$type[0], $className, $isNullable];
}
}

View File

@@ -0,0 +1,83 @@
<?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\Serializer;
use ReflectionParameter;
use ReflectionProperty;
use function filter_var;
/**
* @implements TypeCasting<?float>
*/
final class CastToFloat implements TypeCasting
{
private readonly bool $isNullable;
private ?float $default = null;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
$this->isNullable = $this->init($reflectionProperty);
}
public function setOptions(int|float|null $default = null): void
{
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(?string $value): ?float
{
if (null === $value) {
return match ($this->isNullable) {
true => $this->default,
false => throw TypeCastingFailed::dueToNotNullableType('float'),
};
}
$float = filter_var($value, Type::Float->filterFlag());
return match ($float) {
false => throw TypeCastingFailed::dueToInvalidValue($value, Type::Float->value),
default => $float,
};
}
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): bool
{
if (null === $reflectionProperty->getType()) {
return true;
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Float)) {
$type = $found;
}
}
if (null === $type) {
throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'float', 'null', 'mixed');
}
return $isNullable;
}
}

View File

@@ -0,0 +1,83 @@
<?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\Serializer;
use ReflectionParameter;
use ReflectionProperty;
use function filter_var;
/**
* @implements TypeCasting<?int>
*/
final class CastToInt implements TypeCasting
{
private readonly bool $isNullable;
private ?int $default = null;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
$this->isNullable = $this->init($reflectionProperty);
}
public function setOptions(?int $default = null): void
{
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(?string $value): ?int
{
if (null === $value) {
return match ($this->isNullable) {
true => $this->default,
false => throw TypeCastingFailed::dueToNotNullableType('integer'),
};
}
$int = filter_var($value, Type::Int->filterFlag());
return match ($int) {
false => throw TypeCastingFailed::dueToInvalidValue($value, Type::Int->value),
default => $int,
};
}
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): bool
{
if (null === $reflectionProperty->getType()) {
return true;
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::Mixed, Type::Int, Type::Float)) {
$type = $found;
}
}
if (null === $type) {
throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'int', 'float', 'null', 'mixed');
}
return $isNullable;
}
}

View File

@@ -0,0 +1,86 @@
<?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\Serializer;
use ReflectionParameter;
use ReflectionProperty;
/**
* @implements TypeCasting<?string>
*/
final class CastToString implements TypeCasting
{
private readonly bool $isNullable;
private readonly Type $type;
private ?string $default = null;
public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty)
{
[$this->type, $this->isNullable] = $this->init($reflectionProperty);
}
public function setOptions(?string $default = null): void
{
$this->default = $default;
}
/**
* @throws TypeCastingFailed
*/
public function toVariable(?string $value): ?string
{
$returnedValue = match(true) {
null !== $value => $value,
$this->isNullable => $this->default,
default => throw TypeCastingFailed::dueToNotNullableType($this->type->value),
};
return match (true) {
Type::Null->equals($this->type) && null !== $returnedValue => throw TypeCastingFailed::dueToInvalidValue(match (true) {
null === $value => 'null',
'' === $value => 'empty string',
default => $value,
}, $this->type->value),
default => $returnedValue,
};
}
/**
* @return array{0:Type, 1:bool}
*/
private function init(ReflectionProperty|ReflectionParameter $reflectionProperty): array
{
if (null === $reflectionProperty->getType()) {
return [Type::Mixed, true];
}
$type = null;
$isNullable = false;
foreach (Type::list($reflectionProperty) as $found) {
if (!$isNullable && $found[1]->allowsNull()) {
$isNullable = true;
}
if (null === $type && $found[0]->isOneOf(Type::String, Type::Mixed, Type::Null)) {
$type = $found;
}
}
if (null === $type) {
throw throw MappingFailed::dueToTypeCastingUnsupportedType($reflectionProperty, $this, 'string', 'mixed', 'null');
}
return [$type[0], $isNullable];
}
}

View File

@@ -0,0 +1,25 @@
<?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\Serializer;
use ReflectionProperty;
use RuntimeException;
final class DenormalizationFailed extends RuntimeException implements SerializationFailed
{
public static function dueToUninitializedProperty(ReflectionProperty $reflectionProperty): self
{
return new self('The property '.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().' is not initialized; its value is missing from the source data.');
}
}

View File

@@ -0,0 +1,487 @@
<?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\Serializer;
use Closure;
use Iterator;
use League\Csv\MapIterator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
use function array_search;
use function array_values;
use function count;
use function is_int;
final class Denormalizer
{
private static bool $emptyStringAsNull = true;
private readonly ReflectionClass $class;
/** @var array<ReflectionProperty> */
private readonly array $properties;
/** @var array<PropertySetter> */
private readonly array $propertySetters;
/** @var array<ReflectionMethod> */
private readonly array $postMapCalls;
/**
* @param class-string $className
* @param array<string> $propertyNames
*
* @throws MappingFailed
*/
public function __construct(string $className, array $propertyNames = [])
{
$this->class = $this->setClass($className);
$this->properties = $this->class->getProperties();
$this->propertySetters = $this->setPropertySetters($propertyNames);
$this->postMapCalls = $this->setPostMapCalls();
}
/**
* Enable converting empty string to the null value.
*/
public static function allowEmptyStringAsNull(): void
{
self::$emptyStringAsNull = true;
}
/**
* Disable converting empty string to the null value.
*/
public static function disallowEmptyStringAsNull(): void
{
self::$emptyStringAsNull = false;
}
/**
* Register a global type conversion callback to convert a field into a specific type.
*
* @throws MappingFailed
*/
public static function registerType(string $type, Closure $callback): void
{
CallbackCasting::register($type, $callback);
}
/**
* Unregister a global type conversion callback to convert a field into a specific type.
*
* @param string $type
*
* @return bool
*/
public static function unregisterType(string $type): bool
{
return CallbackCasting::unregisterType($type);
}
public static function unregisterAllTypes(): void
{
CallbackCasting::unregisterTypes();
}
/**
* Register a callback to convert a field into a specific type.
*
* @throws MappingFailed
*/
public static function registerAlias(string $alias, string $type, Closure $callback): void
{
CallbackCasting::register($type, $callback, $alias);
}
public static function unregisterAlias(string $alias): bool
{
return CallbackCasting::unregisterAlias($alias);
}
public static function unregisterAllAliases(): void
{
CallbackCasting::unregisterAliases();
}
public static function unregisterAll(): void
{
CallbackCasting::unregisterAll();
}
/**
* @return array<string>
*/
public static function types(): array
{
$default = [...array_column(Type::cases(), 'value'), ...CallbackCasting::types()];
return array_values(array_unique($default));
}
/**
* @return array<string, string>
*/
public static function aliases(): array
{
return CallbackCasting::aliases();
}
public static function supportsAlias(string $alias): bool
{
return CallbackCasting::supportsAlias($alias);
}
/**
* @param class-string $className
* @param array<?string> $record
*
* @throws DenormalizationFailed
* @throws MappingFailed
* @throws ReflectionException
* @throws TypeCastingFailed
*/
public static function assign(string $className, array $record): object
{
return (new self($className, array_keys($record)))->denormalize($record);
}
/**
* @param class-string $className
* @param array<string> $propertyNames
*
* @throws MappingFailed
* @throws TypeCastingFailed
*/
public static function assignAll(string $className, iterable $records, array $propertyNames = []): Iterator
{
return (new self($className, $propertyNames))->denormalizeAll($records);
}
public function denormalizeAll(iterable $records): Iterator
{
return MapIterator::fromIterable($records, $this->denormalize(...));
}
/**
* @throws DenormalizationFailed
* @throws ReflectionException
* @throws TypeCastingFailed
*/
public function denormalize(array $record): object
{
$object = $this->class->newInstanceWithoutConstructor();
$this->hydrate($object, $record);
$this->assertObjectIsInValidState($object);
foreach ($this->postMapCalls as $accessor) {
$accessor->invoke($object);
}
return $object;
}
/**
* @param array<?string> $record
*
* @throws ReflectionException
* @throws TypeCastingFailed
*/
private function hydrate(object $object, array $record): void
{
$record = array_values($record);
foreach ($this->propertySetters as $propertySetter) {
$value = $record[$propertySetter->offset];
if (is_string($value) && '' === trim($value) && self::$emptyStringAsNull) {
$value = null;
}
$propertySetter($object, $value);
}
}
/**
* @throws DenormalizationFailed
*/
private function assertObjectIsInValidState(object $object): void
{
foreach ($this->properties as $property) {
if (!$property->isInitialized($object)) {
throw DenormalizationFailed::dueToUninitializedProperty($property);
}
}
}
/**
* @param class-string $className
*
* @throws MappingFailed
*/
private function setClass(string $className): ReflectionClass
{
if (!class_exists($className)) {
throw new MappingFailed('The class `'.$className.'` can not be denormalized; The class does not exist or could not be found.');
}
$class = new ReflectionClass($className);
if ($class->isInternal() && $class->isFinal()) {
throw new MappingFailed('The class `'.$className.'` can not be denormalized; PHP internal class marked as final can not be instantiated without using the constructor.');
}
return $class;
}
/**
* @param array<string> $propertyNames
*
* @throws MappingFailed
*
* @return array<PropertySetter>
*/
private function setPropertySetters(array $propertyNames): array
{
$propertySetters = [];
$methodNames = array_map(fn (string|int $propertyName) => is_int($propertyName) ? null : 'set'.ucfirst($propertyName), $propertyNames);
foreach ([...$this->properties, ...$this->class->getMethods()] as $accessor) {
$attributes = $accessor->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF);
$propertySetter = match (count($attributes)) {
0 => $this->autoDiscoverPropertySetter($accessor, $propertyNames, $methodNames),
1 => $this->findPropertySetter($attributes[0]->newInstance(), $accessor, $propertyNames),
default => throw new MappingFailed('Using more than one `'.MapCell::class.'` attribute on a class property or method is not supported.'),
};
if (null !== $propertySetter) {
$propertySetters[] = $propertySetter;
}
}
return match ([]) {
$propertySetters => throw new MappingFailed('No property or method from `'.$this->class->getName().'` could be used for denormalization.'),
default => $propertySetters,
};
}
private function setPostMapCalls(): array
{
$methods = [];
$attributes = $this->class->getAttributes(AfterMapping::class, ReflectionAttribute::IS_INSTANCEOF);
$nbAttributes = count($attributes);
if (0 === $nbAttributes) {
return $methods;
}
if (1 < $nbAttributes) {
throw new MappingFailed('Using more than one `'.AfterMapping::class.'` attribute on a class property or method is not supported.');
}
/** @var AfterMapping $postMap */
$postMap = $attributes[0]->newInstance();
foreach ($postMap->methods as $method) {
try {
$accessor = $this->class->getMethod($method);
} catch (ReflectionException $exception) {
throw new MappingFailed('The method `'.$method.'` is not defined on the `'.$this->class->getName().'` class.', 0, $exception);
}
if (0 !== $accessor->getNumberOfRequiredParameters()) {
throw new MappingFailed('The method `'.$this->class->getName().'::'.$accessor->getName().'` has too many required parameters.');
}
$methods[] = $accessor;
}
return $methods;
}
/**
* @param array<string> $propertyNames
* @param array<?string> $methodNames
*
* @throws MappingFailed
*/
private function autoDiscoverPropertySetter(ReflectionMethod|ReflectionProperty $accessor, array $propertyNames, array $methodNames): ?PropertySetter
{
if ($accessor->isStatic() || !$accessor->isPublic()) {
return null;
}
if ($accessor instanceof ReflectionMethod) {
if ($accessor->isConstructor()) {
return null;
}
if ([] === $accessor->getParameters()) {
return null;
}
if (1 < $accessor->getNumberOfRequiredParameters()) {
return null;
}
}
/** @var int|false $offset */
/** @var ReflectionParameter|ReflectionProperty $reflectionProperty */
[$offset, $reflectionProperty] = match (true) {
$accessor instanceof ReflectionMethod => [array_search($accessor->getName(), $methodNames, true), $accessor->getParameters()[0]],
$accessor instanceof ReflectionProperty => [array_search($accessor->getName(), $propertyNames, true), $accessor],
};
return match (true) {
false === $offset,
null === $reflectionProperty->getType() => null,
default => new PropertySetter(
$accessor,
$offset,
$this->resolveTypeCasting($reflectionProperty)
),
};
}
/**
* @param array<string> $propertyNames
*
* @throws MappingFailed
*/
private function findPropertySetter(MapCell $cell, ReflectionMethod|ReflectionProperty $accessor, array $propertyNames): ?PropertySetter
{
if ($cell->ignore) {
return null;
}
$typeCaster = $this->resolveTypeCaster($cell, $accessor);
$offset = $cell->column ?? match (true) {
$accessor instanceof ReflectionMethod => $this->getMethodFirstArgument($accessor)->getName(),
$accessor instanceof ReflectionProperty => $accessor->getName(),
};
if (!is_int($offset)) {
if ([] === $propertyNames) {
throw new MappingFailed('offset as string are only supported if the property names list is not empty.');
}
/** @var int<0, max>|false $index */
$index = array_search($offset, $propertyNames, true);
if (false === $index) {
throw new MappingFailed('The `'.$offset.'` property could not be found in the property names list; Please verify your property names list.');
}
$offset = $index;
}
$reflectionProperty = match (true) {
$accessor instanceof ReflectionMethod => $accessor->getParameters()[0],
$accessor instanceof ReflectionProperty => $accessor,
};
return match (true) {
0 > $offset => throw new MappingFailed('offset integer position can only be positive or equals to 0; received `'.$offset.'`'),
[] !== $propertyNames && $offset > count($propertyNames) - 1 => throw new MappingFailed('offset integer position can not exceed property names count.'),
null === $typeCaster => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($reflectionProperty, $cell->options)),
default => new PropertySetter($accessor, $offset, $this->getTypeCasting($reflectionProperty, $typeCaster, $cell->options)),
};
}
/**
* @throws MappingFailed
*/
private function getMethodFirstArgument(ReflectionMethod $reflectionMethod): ReflectionParameter
{
$arguments = $reflectionMethod->getParameters();
return match (true) {
[] === $arguments => throw new MappingFailed('The method `'.$reflectionMethod->getDeclaringClass()->getName().'::'.$reflectionMethod->getName().'` does not use parameters.'),
1 < $reflectionMethod->getNumberOfRequiredParameters() => throw new MappingFailed('The method `'.$reflectionMethod->getDeclaringClass()->getName().'::'.$reflectionMethod->getName().'` has too many required parameters.'),
default => $arguments[0]
};
}
/**
* @throws MappingFailed
*/
private function getTypeCasting(
ReflectionProperty|ReflectionParameter $reflectionProperty,
string $typeCaster,
array $options
): TypeCasting {
try {
/** @var TypeCasting $cast */
$cast = match (str_starts_with($typeCaster, CallbackCasting::class.'@')) {
true => new CallbackCasting($reflectionProperty, substr($typeCaster, strlen(CallbackCasting::class))),
false => new $typeCaster($reflectionProperty),
};
$cast->setOptions(...$options);
return $cast;
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}
/**
* @throws MappingFailed
*/
private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $reflectionProperty, array $options = []): TypeCasting
{
$castResolver = function (ReflectionProperty|ReflectionParameter $reflectionProperty, $options): CallbackCasting {
$cast = new CallbackCasting($reflectionProperty);
$cast->setOptions(...$options);
return $cast;
};
try {
return match (true) {
CallbackCasting::supports($reflectionProperty) => $castResolver($reflectionProperty, $options),
default => Type::resolve($reflectionProperty, $options),
};
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}
public function resolveTypeCaster(MapCell $cell, ReflectionMethod|ReflectionProperty $accessor): ?string
{
/** @var ?class-string<TypeCasting> $typeCaster */
$typeCaster = $cell->cast;
if (null === $typeCaster) {
return null;
}
if (class_exists($typeCaster)) {
if (!(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class)) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}
return $typeCaster;
}
if ($accessor instanceof ReflectionMethod) {
$accessor = $accessor->getParameters()[0];
}
if (!CallbackCasting::supports($accessor, $typeCaster)) {
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
}
return CallbackCasting::class.$typeCaster;
}
}

View File

@@ -0,0 +1,31 @@
<?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\Serializer;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
final class MapCell
{
/**
* @param class-string|string|null $cast
*/
public function __construct(
public readonly string|int|null $column = null,
public readonly ?string $cast = null,
public readonly array $options = [],
public readonly bool $ignore = false,
) {
}
}

View File

@@ -0,0 +1,56 @@
<?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\Serializer;
use LogicException;
use ReflectionParameter;
use ReflectionProperty;
use Throwable;
final class MappingFailed extends LogicException implements SerializationFailed
{
public static function dueToUnsupportedType(ReflectionProperty|ReflectionParameter $reflectionProperty): self
{
$suffix = 'is missing; register it using the `'.Denormalizer::class.'` class.';
return new self(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The type definition for the method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` first argument `'.$reflectionProperty->getName().'` '.$suffix,
$reflectionProperty instanceof ReflectionProperty => 'The property type definition for `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` '.$suffix,
});
}
public static function dueToTypeCastingUnsupportedType(
ReflectionProperty|ReflectionParameter $reflectionProperty,
TypeCasting $typeCasting,
string ...$types
): self {
$suffix = 'is invalid; `'.implode('` or `', $types).'` type must be used with the `'.$typeCasting::class.'`.';
return new self(match (true) {
$reflectionProperty instanceof ReflectionParameter => 'The type for the method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` first argument `'.$reflectionProperty->getName().'` '.$suffix,
$reflectionProperty instanceof ReflectionProperty => 'The property type for `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` '.$suffix,
});
}
public static function dueToInvalidCastingArguments(?Throwable $exception = null): self
{
return new self('Unable to load the casting mechanism. Please verify your casting arguments', 0, $exception);
}
public static function dueToInvalidTypeCastingClass(string $typeCaster): self
{
return new self('`'.$typeCaster.'` must be an resolvable class implementing the `'.TypeCasting::class.'` interface or a supported alias.');
}
}

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\Serializer;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;
/**
* @internal
*/
final class PropertySetter
{
public function __construct(
private readonly ReflectionMethod|ReflectionProperty $accessor,
public readonly int $offset,
private readonly TypeCasting $cast,
) {
}
/**
* @throws ReflectionException
*/
public function __invoke(object $object, ?string $value): void
{
$typeCastedValue = $this->cast->toVariable($value);
match (true) {
$this->accessor instanceof ReflectionMethod => $this->accessor->invoke($object, $typeCastedValue),
$this->accessor instanceof ReflectionProperty => $this->accessor->setValue($object, $typeCastedValue),
};
}
}

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\Serializer;
use Throwable;
interface SerializationFailed extends Throwable
{
}

View File

@@ -0,0 +1,163 @@
<?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\Serializer;
use DateTimeInterface;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use Throwable;
use UnitEnum;
use function class_exists;
use function enum_exists;
use function in_array;
use const FILTER_UNSAFE_RAW;
use const FILTER_VALIDATE_BOOL;
use const FILTER_VALIDATE_FLOAT;
use const FILTER_VALIDATE_INT;
enum Type: string
{
case Bool = 'bool';
case True = 'true';
case False = 'false';
case Null = 'null';
case Int = 'int';
case Float = 'float';
case String = 'string';
case Mixed = 'mixed';
case Array = 'array';
case Iterable = 'iterable';
case Enum = UnitEnum::class;
case Date = DateTimeInterface::class;
public function equals(mixed $value): bool
{
return $value instanceof self
&& $value === $this;
}
public function isOneOf(self ...$types): bool
{
return in_array($this, $types, true);
}
public function filterFlag(): int
{
return match ($this) {
self::Bool,
self::True,
self::False => FILTER_VALIDATE_BOOL,
self::Int => FILTER_VALIDATE_INT,
self::Float => FILTER_VALIDATE_FLOAT,
default => FILTER_UNSAFE_RAW,
};
}
public static function resolve(ReflectionProperty|ReflectionParameter $reflectionProperty, array $arguments = []): TypeCasting
{
try {
$cast = match (self::tryFromAccessor($reflectionProperty)) {
self::Mixed, self::Null, self::String => new CastToString($reflectionProperty),
self::Iterable, self::Array => new CastToArray($reflectionProperty),
self::False, self::True, self::Bool => new CastToBool($reflectionProperty),
self::Float => new CastToFloat($reflectionProperty),
self::Int => new CastToInt($reflectionProperty),
self::Date => new CastToDate($reflectionProperty),
self::Enum => new CastToEnum($reflectionProperty),
null => throw MappingFailed::dueToUnsupportedType($reflectionProperty),
};
$cast->setOptions(...$arguments);
return $cast;
} catch (MappingFailed $exception) {
throw $exception;
} catch (Throwable $exception) {
throw MappingFailed::dueToInvalidCastingArguments($exception);
}
}
/**
* @return list<array{0:Type, 1: ReflectionNamedType}>
*/
public static function list(ReflectionParameter|ReflectionProperty $reflectionProperty): array
{
$reflectionType = $reflectionProperty->getType() ?? throw MappingFailed::dueToUnsupportedType($reflectionProperty);
$foundTypes = static function (array $res, ReflectionType $reflectionType) {
if (!$reflectionType instanceof ReflectionNamedType) {
return $res;
}
$type = self::tryFromName($reflectionType->getName());
if (null !== $type) {
$res[] = [$type, $reflectionType];
}
return $res;
};
return match (true) {
$reflectionType instanceof ReflectionNamedType => $foundTypes([], $reflectionType),
$reflectionType instanceof ReflectionUnionType => array_reduce($reflectionType->getTypes(), $foundTypes, []),
default => [],
};
}
public static function tryFromName(string $propertyType): ?self
{
$interfaceExists = interface_exists($propertyType);
return match (true) {
enum_exists($propertyType),
$interfaceExists && (new ReflectionClass($propertyType))->implementsInterface(UnitEnum::class) => self::Enum,
$interfaceExists && (new ReflectionClass($propertyType))->implementsInterface(DateTimeInterface::class),
class_exists($propertyType) && (new ReflectionClass($propertyType))->implementsInterface(DateTimeInterface::class) => self::Date,
default => self::tryFrom($propertyType),
};
}
public static function tryFromAccessor(ReflectionProperty|ReflectionParameter $reflectionProperty): ?self
{
$type = $reflectionProperty->getType();
if (null === $type) {
return Type::Mixed;
}
if ($type instanceof ReflectionNamedType) {
return self::tryFromName($type->getName());
}
if (!$type instanceof ReflectionUnionType) {
return null;
}
foreach ($type->getTypes() as $innerType) {
if (!$innerType instanceof ReflectionNamedType) {
continue;
}
$result = self::tryFromName($innerType->getName());
if ($result instanceof self) {
return $result;
}
}
return null;
}
}

View File

@@ -0,0 +1,36 @@
<?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\Serializer;
/**
* @template TValue
*/
interface TypeCasting
{
/**
* @throws TypeCastingFailed
*
* @return TValue
*/
public function toVariable(?string $value): mixed;
/**
* Accepts additional parameters to configure the class
* Parameters should be scalar value, null or array containing
* only scalar value and null.
*
* @throws MappingFailed
*/
public function setOptions(): void;
}

View File

@@ -0,0 +1,30 @@
<?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\Serializer;
use RuntimeException;
use Throwable;
final class TypeCastingFailed extends RuntimeException implements SerializationFailed
{
public static function dueToNotNullableType(string $type, ?Throwable $exception = null): self
{
return new self('The `null` value can not be cast to an `'.$type.'`; the property type is not nullable.', 0, $exception);
}
public static function dueToInvalidValue(string $value, string $type, ?Throwable $previous = null): self
{
return new self('Unable to cast the given data `'.$value.'` to a `'.$type.'`.', 0, $previous);
}
}

456
vendor/league/csv/src/Statement.php vendored Normal file
View File

@@ -0,0 +1,456 @@
<?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 Closure;
use Iterator;
use OutOfBoundsException;
use ReflectionException;
use ReflectionFunction;
use function array_key_exists;
use function array_reduce;
use function array_search;
use function array_values;
use function is_string;
/**
* Criteria to filter a {@link TabularDataReader} object.
*
* @phpstan-import-type ConditionExtended from Query\PredicateCombinator
* @phpstan-import-type OrderingExtended from Query\SortCombinator
*/
class Statement
{
/** @var array<ConditionExtended> Callables to filter the iterator. */
protected array $where = [];
/** @var array<OrderingExtended> Callables to sort the iterator. */
protected array $order_by = [];
/** iterator Offset. */
protected int $offset = 0;
/** iterator maximum length. */
protected int $limit = -1;
/** @var array<string|int> */
protected array $select = [];
/**
* @param ?callable(array, array-key): bool $where, Deprecated argument use Statement::where instead
* @param int $offset, Deprecated argument use Statement::offset instead
* @param int $limit, Deprecated argument use Statement::limit instead
*
* @throws Exception
* @throws InvalidArgument
* @throws ReflectionException
*/
public static function create(?callable $where = null, int $offset = 0, int $limit = -1): self
{
$stmt = new self();
if (null !== $where) {
$stmt = $stmt->where($where);
}
if (0 !== $offset) {
$stmt = $stmt->offset($offset);
}
if (-1 !== $limit) {
$stmt = $stmt->limit($limit);
}
return $stmt;
}
/**
* Sets the Iterator element columns.
*/
public function select(string|int ...$columns): self
{
if ($columns === $this->select) {
return $this;
}
$clone = clone $this;
$clone->select = $columns;
return $clone;
}
/**
* Sets the Iterator filter method.
*
* @param callable(array, array-key): bool $where
*
* @throws ReflectionException
* @throws InvalidArgument
*/
public function where(callable $where): self
{
$where = self::wrapSingleArgumentCallable($where);
$clone = clone $this;
$clone->where[] = $where;
return $clone;
}
/**
* Sanitize the number of required parameters for a predicate.
*
* To avoid BC break in 9.16+ version the predicate should have
* at least 1 required argument.
*
* @throws InvalidArgument
* @throws ReflectionException
*
* @return ConditionExtended
*/
final protected static function wrapSingleArgumentCallable(callable $where): callable
{
if ($where instanceof Query\Predicate) {
return $where;
}
$reflection = new ReflectionFunction($where instanceof Closure ? $where : $where(...));
return match ($reflection->getNumberOfRequiredParameters()) {
0 => throw new InvalidArgument('The where condition must be a callable with 2 required parameters.'),
1 => fn (mixed $record, int $key) => $where($record),
default => $where,
};
}
public function andWhere(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('and', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function orWhere(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('or', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function whereNot(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('not', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function xorWhere(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('xor', Query\Constraint\Column::filterOn($column, $operator, $value));
}
public function andWhereColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendWhere('and', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function orWhereColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendWhere('or', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function xorWhereColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendWhere('xor', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function whereNotColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendWhere('not', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}
public function andWhereOffset(Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('and', Query\Constraint\Offset::filterOn($operator, $value));
}
public function orWhereOffset(Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('or', Query\Constraint\Offset::filterOn($operator, $value));
}
public function xorWhereOffset(Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('xor', Query\Constraint\Offset::filterOn($operator, $value));
}
public function whereNotOffset(Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendWhere('not', Query\Constraint\Offset::filterOn($operator, $value));
}
/**
* @param 'and'|'not'|'or'|'xor' $joiner
*/
final protected function appendWhere(string $joiner, Query\Predicate $predicate): self
{
if ([] === $this->where) {
return $this->where(match ($joiner) {
'and' => $predicate,
'not' => Query\Constraint\Criteria::none($predicate),
'or' => Query\Constraint\Criteria::any($predicate),
'xor' => Query\Constraint\Criteria::xany($predicate),
});
}
$predicates = Query\Constraint\Criteria::all(...$this->where);
$clone = clone $this;
$clone->where = [match ($joiner) {
'and' => $predicates->and($predicate),
'not' => $predicates->not($predicate),
'or' => $predicates->or($predicate),
'xor' => $predicates->xor($predicate),
}];
return $clone;
}
/**
* Sets an Iterator sorting callable function.
*
* @param OrderingExtended $order_by
*/
public function orderBy(callable|Query\Sort|Closure $order_by): self
{
$clone = clone $this;
$clone->order_by[] = $order_by;
return $clone;
}
/**
* Ascending ordering of the tabular data according to a column value.
*
* The column value can be modified using the callback before ordering.
*/
public function orderByAsc(string|int $column, ?Closure $callback = null): self
{
return $this->orderBy(Query\Ordering\Column::sortOn($column, 'asc', $callback));
}
/**
* Descending ordering of the tabular data according to a column value.
*
* The column value can be modified using the callback before ordering.
*/
public function orderByDesc(string|int $column, ?Closure $callback = null): self
{
return $this->orderBy(Query\Ordering\Column::sortOn($column, 'desc', $callback));
}
/**
* Sets LimitIterator Offset.
*
* @throws Exception if the offset is less 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;
}
/**
* Sets LimitIterator Count.
*
* @throws Exception if the limit is less 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;
}
/**
* Executes the prepared Statement on the {@link TabularDataReader} object.
*
* @param array<string> $header an optional header to use instead of the tabular data header
*
* @throws InvalidArgument
* @throws SyntaxError
*/
public function process(TabularDataReader $tabular_data, array $header = []): TabularDataReader
{
if ([] === $header) {
$header = $tabular_data->getHeader();
}
$iterator = $tabular_data->getRecords($header);
if ([] !== $this->where) {
$iterator = Query\Constraint\Criteria::all(...$this->where)->filter($iterator);
}
if ([] !== $this->order_by) {
$iterator = Query\Ordering\MultiSort::all(...$this->order_by)->sort($iterator);
}
if (0 !== $this->offset || -1 !== $this->limit) {
$iterator = Query\Limit::new($this->offset, $this->limit)->slice($iterator);
}
return (new ResultSet($iterator, $header))->select(...$this->select);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws InvalidArgument
*
* @throws SyntaxError
* @see Statement::process()
* @deprecated Since version 9.16.0
*/
protected function applySelect(Iterator $records, array $recordsHeader, array $select): TabularDataReader
{
$hasHeader = [] !== $recordsHeader;
$selectColumn = function (array $header, string|int $field) use ($recordsHeader, $hasHeader): array {
if (is_string($field)) {
$index = array_search($field, $recordsHeader, true);
if (false === $index) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$index] = $field;
return $header;
}
if ($hasHeader && !array_key_exists($field, $recordsHeader)) {
throw InvalidArgument::dueToInvalidColumnIndex($field, 'offset', __METHOD__);
}
$header[$field] = $recordsHeader[$field] ?? $field;
return $header;
};
/** @var array<string> $header */
$header = array_reduce($select, $selectColumn, []);
$callback = function (array $record) use ($header): array {
$element = [];
$row = array_values($record);
foreach ($header as $offset => $headerName) {
$element[$headerName] = $row[$offset] ?? null;
}
return $element;
};
return new ResultSet(new MapIterator($records, $callback), $hasHeader ? $header : []);
}
/**
* Filters elements of an Iterator using a callback function.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Statement::applyFilter()
* @deprecated Since version 9.15.0
* @codeCoverageIgnore
*/
protected function filter(Iterator $iterator, callable $callable): CallbackFilterIterator
{
return new CallbackFilterIterator($iterator, $callable);
}
/**
* Filters elements of an Iterator using a callback function.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Statement::process()
* @deprecated Since version 9.16.0
* @codeCoverageIgnore
*/
protected function applyFilter(Iterator $iterator): Iterator
{
$filter = function (array $record, string|int $key): bool {
foreach ($this->where as $where) {
if (true !== $where($record, $key)) {
return false;
}
}
return true;
};
return new CallbackFilterIterator($iterator, $filter);
}
/**
* Sorts the Iterator.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Statement::process()
* @deprecated Since version 9.16.0
* @codeCoverageIgnore
*/
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;
};
$class = new class () extends ArrayIterator {
public function seek(int $offset): void
{
try {
parent::seek($offset);
} catch (OutOfBoundsException) {
return;
}
}
};
/** @var ArrayIterator<array-key, array<string|null>> $it */
$it = new $class([...$iterator]);
$it->uasort($compare);
return $it;
}
}

510
vendor/league/csv/src/Stream.php vendored Normal file
View File

@@ -0,0 +1,510 @@
<?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 RuntimeException;
use SeekableIterator;
use SplFileObject;
use Stringable;
use TypeError;
use ValueError;
use function array_keys;
use function array_walk_recursive;
use function fclose;
use function feof;
use function fflush;
use function fgetcsv;
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 restore_error_handler;
use function rewind;
use function set_error_handler;
use function stream_filter_append;
use function stream_filter_remove;
use function stream_get_meta_data;
use function strlen;
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 resource */
private $stream;
private bool $is_seekable;
private bool $should_close_stream = false;
/** @var mixed can be a null, false or a scalar type value. Current iterator value. */
private mixed $value = null;
/** Current iterator key. */
private int $offset = -1;
/** Flags for the Document. */
private int $flags = 0;
private string $delimiter = ',';
private string $enclosure = '"';
private string $escape = '\\';
/** @var array<string, array<resource>> Attached filters. */
private array $filters = [];
private int $maxLength = 0;
/**
* @param resource $stream stream type resource
*/
private function __construct($stream)
{
$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) {
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
fclose($this->stream);
restore_error_handler();
}
unset($this->stream);
}
public function __clone(): void
{
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),
];
}
public function ftell(): int|false
{
return ftell($this->stream);
}
/**
* Returns a new instance from a file path.
*
* @param resource|null $context
*
* @throws UnavailableStream 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;
}
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$resource = fopen(...$args);
restore_error_handler();
if (!is_resource($resource)) {
throw UnavailableStream::dueToPathNotFound($path);
}
$instance = new self($resource);
$instance->should_close_stream = true;
return $instance;
}
/**
* Returns a new instance from a string.
*/
public static function createFromString(Stringable|string $content = ''): self
{
/** @var resource $resource */
$resource = fopen('php://temp', 'r+');
fwrite($resource, (string) $content);
$instance = new self($resource);
$instance->should_close_stream = true;
return $instance;
}
public static function createFromResource(mixed $stream): self
{
return match (true) {
!is_resource($stream) => throw new TypeError('Argument passed must be a stream resource, '.gettype($stream).' given.'),
'stream' !== ($type = get_resource_type($stream)) => throw new TypeError('Argument passed must be a stream resource, '.$type.' resource given'),
default => new self($stream),
};
}
/**
* 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'];
}
/**
* Appends 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
{
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$res = stream_filter_append($this->stream, $filtername, $read_write, $params ?? []);
restore_error_handler();
if (!is_resource($res)) {
throw InvalidArgument::dueToStreamFilterNotFound($filtername);
}
$this->filters[$filtername][] = $res;
}
/**
* Sets CSV control.
*
* @see https://www.php.net/manual/en/splfileobject.setcsvcontrol.php
*
* @throws InvalidArgument
*/
public function setCsvControl(string $delimiter = ',', string $enclosure = '"', string $escape = '\\'): void
{
[$this->delimiter, $this->enclosure, $this->escape] = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
}
/**
* Filters CSV control characters.
*
* @throws InvalidArgument If the CSV control character is not exactly one character.
*
* @return array{0:string, 1:string, 2:string}
*/
private function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array
{
return match (true) {
1 !== strlen($delimiter) => throw InvalidArgument::dueToInvalidDelimiterCharacter($delimiter, $caller),
1 !== strlen($enclosure) => throw InvalidArgument::dueToInvalidEnclosureCharacter($enclosure, $caller),
1 !== strlen($escape) && '' !== $escape => throw InvalidArgument::dueToInvalidEscapeCharacter($escape, $caller),
default => [$delimiter, $enclosure, $escape],
};
}
/**
* Returns CSV control.
*
* @see https://www.php.net/manual/en/splfileobject.getcsvcontrol.php
*
* @return array<string>
*/
public function getCsvControl(): array
{
return [$this->delimiter, $this->enclosure, $this->escape];
}
/**
* Sets CSV stream flags.
*
* @see https://www.php.net/manual/en/splfileobject.setflags.php
*/
public function setFlags(int $flags): void
{
$this->flags = $flags;
}
/**
* Writes a field array as a CSV line.
*
* @see https://www.php.net/manual/en/splfileobject.fputcsv.php
*
* @throws InvalidArgument If the CSV control character is not exactly one character.
*/
public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\', string $eol = "\n"): int|false
{
return fputcsv(
$this->stream,
$fields,
...[...$this->filterControl($delimiter, $enclosure, $escape, __METHOD__), $eol]
);
}
/**
* Gets line number.
*
* @see https://www.php.net/manual/en/splfileobject.key.php
*/
public function key(): int
{
return $this->offset;
}
/**
* Reads next line.
*
* @see https://www.php.net/manual/en/splfileobject.next.php
*/
public function next(): void
{
$this->value = false;
$this->offset++;
}
/**
* Rewinds the file to the first line.
*
* @see https://www.php.net/manual/en/splfileobject.rewind.php
*
* @throws Exception if the stream resource is not seekable
* @throws RuntimeException if rewinding the stream fails.
*/
public function rewind(): void
{
if (!$this->is_seekable) {
throw UnavailableFeature::dueToMissingStreamSeekability();
}
if (false === rewind($this->stream)) {
throw new RuntimeException('Unable to rewind the document.');
}
$this->offset = 0;
$this->value = false;
if (SplFileObject::READ_AHEAD === ($this->flags & SplFileObject::READ_AHEAD)) {
$this->current();
}
}
/**
* Not at EOF.
*
* @see https://www.php.net/manual/en/splfileobject.valid.php
*/
public function valid(): bool
{
return match (true) {
SplFileObject::READ_AHEAD === ($this->flags & SplFileObject::READ_AHEAD) => false !== $this->current(),
default => !feof($this->stream),
};
}
/**
* Retrieves the current line of the file.
*
* @see https://www.php.net/manual/en/splfileobject.current.php
*/
public function current(): mixed
{
if (false !== $this->value) {
return $this->value;
}
$this->value = match (true) {
SplFileObject::READ_CSV === ($this->flags & SplFileObject::READ_CSV) => $this->getCurrentRecord(),
default => $this->getCurrentLine(),
};
return $this->value;
}
public function fgets(): string|false
{
$arg = [$this->stream];
if (0 < $this->maxLength) {
$arg[] = $this->maxLength;
}
return fgets(...$arg);
}
/**
* Sets the maximum length of a line to be read.
*
* @see https://www.php.net/manual/en/splfileobject.setmaxlinelen.php
*/
public function setMaxLineLen(int $maxLength): void
{
if (0 > $maxLength) {
throw new ValueError(' Argument #1 ($maxLength) must be greater than or equal to 0');
}
$this->maxLength = $maxLength;
}
/**
* Gets the maximum line length as set by setMaxLineLen.
*
* @see https://www.php.net/manual/en/splfileobject.getmaxlinelen.php
*/
public function getMaxLineLen(): int
{
return $this->maxLength;
}
/**
* Tells whether the end of file has been reached.
*
* @see https://www.php.net/manual/en/splfileobject.eof.php
*/
public function eof(): bool
{
return feof($this->stream);
}
/**
* Retrieves the current line as a CSV Record.
*/
private function getCurrentRecord(): array|false
{
$isEmptyLine = SplFileObject::SKIP_EMPTY === ($this->flags & SplFileObject::SKIP_EMPTY);
do {
$ret = fgetcsv($this->stream, 0, $this->delimiter, $this->enclosure, $this->escape);
} while ($isEmptyLine && is_array($ret) && null === $ret[0]);
return $ret;
}
/**
* Retrieves the current line.
*/
private function getCurrentLine(): string|false
{
$isEmptyLine = SplFileObject::SKIP_EMPTY === ($this->flags & SplFileObject::SKIP_EMPTY);
$dropNewLine = SplFileObject::DROP_NEW_LINE === ($this->flags & SplFileObject::DROP_NEW_LINE);
$shouldBeIgnored = fn (string|false $line): bool => ($isEmptyLine || $dropNewLine)
&& (false !== $line && '' === rtrim($line, "\r\n"));
$arguments = [$this->stream];
if (0 < $this->maxLength) {
$arguments[] = $this->maxLength;
}
do {
$line = fgets(...$arguments);
} while ($shouldBeIgnored($line));
if ($dropNewLine && false !== $line) {
return rtrim($line, "\r\n");
}
return $line;
}
/**
* Seeks to specified line.
*
* @see https://www.php.net/manual/en/splfileobject.seek.php
*
* @throws Exception if the position is negative
*/
public function seek(int $offset): void
{
if ($offset < 0) {
throw InvalidArgument::dueToInvalidSeekingPosition($offset, __METHOD__);
}
$this->rewind();
while ($this->key() !== $offset && $this->valid()) {
$this->current();
$this->next();
}
if (0 !== $offset) {
$this->offset--;
}
$this->current();
}
/**
* Outputs all remaining data on a file pointer.
*
* @see https://www.php.net/manual/en/splfileobject.fpassthru.php
*/
public function fpassthru(): int|false
{
return fpassthru($this->stream);
}
/**
* Reads from file.
*
* @see https://www.php.net/manual/en/splfileobject.fread.php
*
* @param int<0, max> $length The number of bytes to read
*/
public function fread(int $length): string|false
{
return fread($this->stream, $length);
}
/**
* Seeks to a position.
*
* @see https://www.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
{
return match (true) {
!$this->is_seekable => throw UnavailableFeature::dueToMissingStreamSeekability(),
default => fseek($this->stream, $offset, $whence),
};
}
/**
* Write to stream.
*
* @see http://php.net/manual/en/SplFileObject.fwrite.php
*/
public function fwrite(string $str, ?int $length = null): int|false
{
$args = [$this->stream, $str];
if (null !== $length) {
$args[] = $length;
}
return fwrite(...$args);
}
/**
* Flushes the output to a file.
*
* @see https://www.php.net/manual/en/splfileobject.fflush.php
*/
public function fflush(): bool
{
return fflush($this->stream);
}
}

95
vendor/league/csv/src/SwapDelimiter.php vendored Normal file
View File

@@ -0,0 +1,95 @@
<?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 php_user_filter;
use function in_array;
use function str_replace;
use function stream_bucket_append;
use function stream_bucket_make_writeable;
use function stream_filter_register;
use function stream_get_filters;
use const PSFS_PASS_ON;
final class SwapDelimiter extends php_user_filter
{
private const FILTER_NAME = 'string.league.csv.delimiter';
public const MODE_READ = 'read';
public const MODE_WRITE = 'write';
private string $search = '';
private string $replace = '';
public static function getFiltername(): string
{
return self::FILTER_NAME;
}
/**
* Static method to register the class as a stream filter.
*/
public static function register(): void
{
if (!in_array(self::FILTER_NAME, stream_get_filters(), true)) {
stream_filter_register(self::FILTER_NAME, self::class);
}
}
/**
* Static method to attach the stream filter to a CSV Reader or Writer instance.
*/
public static function addTo(AbstractCsv $csv, string $inputDelimiter): void
{
self::register();
$csv->addStreamFilter(self::getFiltername(), [
'mb_separator' => $inputDelimiter,
'separator' => $csv->getDelimiter(),
'mode' => $csv instanceof Writer ? self::MODE_WRITE : self::MODE_READ,
]);
}
public function onCreate(): bool
{
if (self::FILTER_NAME !== $this->filtername) {
return false;
}
if (!is_array($this->params)) {
return false;
}
$mode = $this->params['mode'] ?? '';
[$this->search, $this->replace] = match ($mode) {
self::MODE_READ => [trim($this->params['mb_separator'] ?? ''), trim($this->params['separator'] ?? '')],
self::MODE_WRITE => [trim($this->params['separator'] ?? ''), trim($this->params['mb_separator'] ?? '')],
default => ['', ''],
};
return !in_array('', [$this->replace, $this->search], true);
}
public function filter($in, $out, &$consumed, bool $closing): int
{
while (null !== ($bucket = stream_bucket_make_writeable($in))) {
$content = $bucket->data;
$bucket->data = str_replace($this->search, $this->replace, $content);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}

64
vendor/league/csv/src/SyntaxError.php vendored Normal file
View File

@@ -0,0 +1,64 @@
<?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;
use function array_count_values;
use function array_filter;
use function array_keys;
/**
* 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,150 @@
<?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 Closure;
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
* @method array first() returns the first record from the tabular data.
* @method array nth(int $nth_record) returns the nth record from the tabular data.
* @method mixed value(int|string $column = 0) returns a given value from the first element of the tabular data.
* @method Iterator nthAsObject(int $nth, string $className, array $header = []) returns the nth record from the tabular data as an instance of the defined class name.
* @method Iterator firstAsObject(string $className, array $header = []) returns the first record from the tabular data as an instance of the defined class name.
* @method bool each(Closure $callback) iterates over each record and passes it to a closure. Iteration is interrupted if the closure returns false
* @method bool exists(Closure $callback) tells whether at least one record satisfies the predicate.
* @method mixed reduce(Closure $callback, mixed $initial = null) reduces the collection to a single value, passing the result of each iteration into the subsequent iteration
* @method Iterator getObjects(string $className, array $header = []) Returns the tabular data records as an iterator object containing instance of the defined class name.
* @method Iterator getRecordsAsObject(string $className, array $header = []) Returns the tabular data records as an iterator object containing instance of the defined class name.
* @method TabularDataReader filter(Query\Predicate|Closure $predicate) returns all the elements of this collection for which your callback function returns `true`
* @method TabularDataReader slice(int $offset, int $length = null) extracts a slice of $length elements starting at position $offset from the Collection.
* @method TabularDataReader sorted(Query\Sort|Closure $orderBy) sorts the Collection according to the closure provided see Statement::orderBy method
* @method TabularDataReader select(string|int ...$columnOffsetOrName) extract a selection of the tabular data records columns.
* @method TabularDataReader matchingFirstOrFail(string $expression) extract the first found fragment identifier of the tabular data or fail
* @method TabularDataReader|null matchingFirst(string $expression) extract the first found fragment identifier of the tabular data or return null if none is found
* @method iterable<int, TabularDataReader> matching(string $expression) extract all found fragment identifiers for the tabular data
* @method iterable<TabularDataReader> chunkBy(int $recordsCount) Chunk the TabulaDataReader into smaller TabularDataReader instances of the given size or less.
* @method TabularDataReader mapHeader(array $headers) Returns a new TabulaDataReader with a new set of headers.
*
* @template TValue of array
* @template-extends IteratorAggregate<array-key, TValue>
*/
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 containing flat array.
*
* 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.
*
* @return Iterator<array-key, array<array-key, mixed>>
*/
public function getIterator(): Iterator;
/**
* Returns the header associated with the tabular data.
*
* The header must contain unique string or to be 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 mapper to use instead of the CSV document header
*
* @return Iterator<array-key, array<array-key, mixed>>
*/
public function getRecords(array $header = []): 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;
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.9.0
*
* 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 less than 0
*/
public function fetchOne(int $nth_record = 0): array;
/**
* DEPRECATION WARNING! This method 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 fetched
*
* @param string|int $index CSV column index
*
* @throws UnableToProcessCsv if the column index is invalid or not found
*
* @return Iterator<int, mixed>
*/
public function fetchColumn(string|int $index = 0): Iterator;
}

View File

@@ -0,0 +1,47 @@
<?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 Stringable;
/**
* A class to insert records into a CSV Document.
*/
interface TabularDataWriter
{
/**
* Adds multiple records to the CSV document.
*
* @see TabularDataWriter::insertOne
*
* @param iterable<array<null|int|float|string|Stringable>> $records
*
* @throws CannotInsertRecord If the record can not be inserted
* @throws Exception If the record can not be inserted
*/
public function insertAll(iterable $records): int;
/**
* Adds a single record to a CSV document.
*
* A record is an array that can contain scalar type values, NULL values
* or objects implementing the __toString method.
*
* @param array<null|int|float|string|Stringable> $record
*
* @throws CannotInsertRecord If the record can not be inserted
* @throws Exception If the record can not be inserted
*/
public function insertOne(array $record): int;
}

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.');
}
}

303
vendor/league/csv/src/Writer.php vendored Normal file
View File

@@ -0,0 +1,303 @@
<?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_map;
use function array_reduce;
use function implode;
use function restore_error_handler;
use function set_error_handler;
use function str_replace;
use const STREAM_FILTER_WRITE;
/**
* A class to insert records into a CSV Document.
*/
class Writer extends AbstractCsv implements TabularDataWriter
{
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 bool $enclose_all = false;
/** @var array{0:array<string>,1:array<string>} */
protected array $enclosure_replace = [[], []];
protected function resetProperties(): void
{
parent::resetProperties();
$this->enclosure_replace = [
[$this->enclosure, $this->escape.$this->enclosure.$this->enclosure],
[$this->enclosure.$this->enclosure, $this->escape.$this->enclosure],
];
}
/**
* Returns the current end of line sequence characters.
*/
public function getEndOfLine(): string
{
return $this->newline;
}
/**
* Returns the flush threshold.
*/
public function getFlushThreshold(): ?int
{
return $this->flush_threshold;
}
/**
* Tells whether new entries will all be enclosed on writing.
*/
public function encloseAll(): bool
{
return $this->enclose_all;
}
/**
* Adds multiple records to the CSV document.
* @see Writer::insertOne
*
* @throws CannotInsertRecord
* @throws Exception
*/
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 contain scalar type values, NULL values
* or objects implementing the __toString method.
*
* @throws CannotInsertRecord If the record can not be inserted
* @throws Exception If the record can not be inserted
*/
public function insertOne(array $record): int
{
$insert = fn (array $record): int|false => match (true) {
$this->enclose_all => $this->document->fwrite(implode(
$this->delimiter,
array_map(
fn ($content) => $this->enclosure.$content.$this->enclosure,
str_replace($this->enclosure_replace[0], $this->enclosure_replace[1], $record)
)
).$this->newline),
default => $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape, $this->newline),
};
$record = array_reduce($this->formatters, fn (array $record, callable $formatter): array => $formatter($record), $record);
$this->validateRecord($record);
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$bytes = $insert($record);
restore_error_handler();
if (false === $bytes) {
throw CannotInsertRecord::triggerOnInsertion($record);
}
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;
}
/**
* Validates 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);
}
}
}
/**
* 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 end of line sequence.
*/
public function setEndOfLine(string $endOfLine): self
{
$this->newline = $endOfLine;
return $this;
}
/**
* Sets the flush threshold.
*
* @throws InvalidArgument if the threshold is a integer less 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;
}
public function relaxEnclosure(): self
{
$this->enclose_all = false;
return $this;
}
public function forceEnclosure(): self
{
$this->enclose_all = true;
return $this;
}
/**
* 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);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 9.9.0
* @codeCoverageIgnore
*
* Adds a single record to a CSV Document using PHP algorithm.
*
* @see https://php.net/manual/en/function.fputcsv.php
*/
protected function addRecord(array $record): int|false
{
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.9.0
* @codeCoverageIgnore
*
* Applies post insertion actions.
*/
protected function consolidate(): int
{
if (null === $this->flush_threshold) {
return 0;
}
++$this->flush_counter;
if (0 === $this->flush_counter % $this->flush_threshold) {
$this->flush_counter = 0;
$this->document->fflush();
}
return 0;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Writer::getEndOfLine()
* @deprecated Since version 9.10.0
* @codeCoverageIgnore
*
* Returns the current newline sequence characters.
*/
public function getNewline(): string
{
return $this->getEndOfLine();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Writer::setEndOfLine()
* @deprecated Since version 9.10.0
* @codeCoverageIgnore
*
* Sets the newline sequence.
*/
public function setNewline(string $newline): self
{
return $this->setEndOfLine($newline);
}
}

183
vendor/league/csv/src/XMLConverter.php vendored Normal file
View File

@@ -0,0 +1,183 @@
<?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()
{
}
/**
* Converts 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;
}
/**
* Creates 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;
}
/**
* Converts 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;
}
/**
* Converts Cell to Item.
*
* Converts the CSV item into a DOMElement and adds the item offset
* as attribute to the returned DOMElement
*/
protected function fieldToElement(DOMDocument $doc, string $value, int|string $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.
*
* @throws DOMException
*/
public function rootElement(string $node_name): self
{
$clone = clone $this;
$clone->root_name = $this->filterElementName($node_name);
return $clone;
}
/**
* Filters 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.
*
* @throws DOMException
*/
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;
}
/**
* Filters 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.
*
* @throws DOMException
*/
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;
}
}

50
vendor/league/csv/src/functions.php vendored Normal file
View File

@@ -0,0 +1,50 @@
<?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 Bom::tryFromSequence()
* @codeCoverageIgnore
*
* 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 Bom::tryFromSequence($str)?->value ?? '';
}
/**
* @param array<string> $delimiters
*
* @return array<string,int>
* @deprecated since version 9.7.0
* @see Info::getDelimiterStats()
* @codeCoverageIgnore
*
* 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\delimiter_detect')) {
require __DIR__.'/functions.php';
}