[增添]添加了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

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