File: /home/posscale/public_html/printmanager/vendor/league/csv/src/JsonConverter.php
<?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 BadMethodCallException;
use Closure;
use Deprecated;
use Exception;
use Generator;
use InvalidArgumentException;
use Iterator;
use JsonException;
use RuntimeException;
use SplFileInfo;
use SplFileObject;
use TypeError;
use function array_filter;
use function array_map;
use function array_reduce;
use function array_values;
use function get_defined_constants;
use function implode;
use function in_array;
use function is_bool;
use function json_encode;
use function json_last_error;
use function preg_match;
use function str_repeat;
use function str_replace;
use function str_starts_with;
use function strlen;
use function strtolower;
use function substr;
use function ucwords;
use const ARRAY_FILTER_USE_KEY;
use const JSON_ERROR_NONE;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
/**
* Converts and store tabular data into a JSON string.
* @template T
*
* @method JsonConverter withHexTag() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexTag() removes the JSON_HEX_TAG flag
* @method bool useHexTag() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withHexAmp() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexAmp() removes the JSON_HEX_TAG flag
* @method bool useHexAmp() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withHexApos() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexApos() removes the JSON_HEX_TAG flag
* @method bool useHexApos() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withHexQuot() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutHexQuot() removes the JSON_HEX_TAG flag
* @method bool useHexQuot() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withForceObject() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutForceObject() removes the JSON_HEX_TAG flag
* @method bool useForceObject() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withNumericCheck() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutNumericCheck() removes the JSON_HEX_TAG flag
* @method bool useNumericCheck() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withUnescapedSlashes() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutUnescapedSlashes() removes the JSON_HEX_TAG flag
* @method bool useUnescapedSlashes() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withoutPrettyPrint() removes the JSON_PRETTY_PRINT flag
* @method bool usePrettyPrint() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withUnescapedUnicode() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutUnescapedUnicode() removes the JSON_HEX_TAG flag
* @method bool useUnescapedUnicode() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withPartialOutputOnError() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutPartialOutputOnError() removes the JSON_HEX_TAG flag
* @method bool usePartialOutputOnError() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withPreserveZeroFraction() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutPreserveZeroFraction() removes the JSON_HEX_TAG flag
* @method bool usePreserveZeroFraction() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withUnescapedLineTerminators() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutUnescapedLineTerminators() removes the JSON_HEX_TAG flag
* @method bool useUnescapedLineTerminators() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withInvalidUtf8Ignore() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutInvalidUtf8Ignore() removes the JSON_HEX_TAG flag
* @method bool useInvalidUtf8Ignore() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withInvalidUtf8Substitute() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutInvalidUtf8Substitute() removes the JSON_HEX_TAG flag
* @method bool useInvalidUtf8Substitute() tells whether the JSON_HEX_TAG flag is used
* @method JsonConverter withThrowOnError() adds the JSON_HEX_TAG flag
* @method JsonConverter withoutThrowOnError() removes the JSON_HEX_TAG flag
* @method bool useThrowOnError() tells whether the JSON_HEX_TAG flag is used
*/
final class JsonConverter
{
public readonly int $flags;
/** @var int<1, max> */
public readonly int $depth;
/** @var int<1, max> */
public readonly int $indentSize;
/** @var ?Closure(T, array-key): mixed */
public readonly ?Closure $formatter;
/** @var int<1, max> */
public readonly int $chunkSize;
public readonly JsonFormat $format;
private readonly string $start;
private readonly string $end;
private readonly string $separator;
private readonly string $emptyIterable;
/** @var non-empty-string */
private readonly string $indentation;
/** @var Closure(array<int, T>): string */
private readonly Closure $jsonEncodeChunk;
/** @var array<string> */
private array $indentationLevels = [];
/**
* @param int<1, max> $depth
* @param int<1, max> $indentSize
* @param ?callable(T, array-key): mixed $formatter
* @param int<1, max> $chunkSize
*
* @throws InvalidArgumentException
*/
public function __construct(
int $flags = 0,
int $depth = 512,
int $indentSize = 4,
?callable $formatter = null,
int $chunkSize = 500,
JsonFormat $jsonFormat = JsonFormat::Standard,
) {
json_encode([], $flags & ~JSON_THROW_ON_ERROR, $depth);
JSON_ERROR_NONE === ($errorCode = json_last_error()) || throw new InvalidArgumentException('The flags or the depth given are not valid JSON encoding parameters in PHP; '.json_last_error_msg(), $errorCode);
1 <= $indentSize || throw new InvalidArgumentException('The indentation space must be greater or equal to 1.');
1 <= $chunkSize || throw new InvalidArgumentException('The chunk size must be greater or equal to 1.');
$this->flags = $flags;
$this->depth = $depth;
$this->indentSize = $indentSize;
$this->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);
$this->format = $jsonFormat;
// Initialize settings and closure to use for conversion.
// To speed up the process, we pre-calculate them
$this->indentation = str_repeat(' ', $this->indentSize);
$start = '[';
$end = ']';
$separator = ',';
$chunkFormatter = fn (array $value): array => $value;
if (JsonFormat::Standard === $this->format) {
$chunkFormatter = array_values(...);
}
$prettyPrintFormatter = fn (string $json): string => $json;
if ($this->useForceObject()) {
$start = '{';
$end = '}';
$chunkFormatter = fn (array $value): array => $value;
}
if (JsonFormat::Standard !== $this->format) {
$start = '';
$end = "\n";
$separator = "\n";
}
$this->emptyIterable = JsonFormat::Standard !== $this->format ? '' : $start.$end;
if ($this->usePrettyPrint()) {
$start .= "\n";
$end = "\n".$end;
$separator .= "\n";
if (JsonFormat::Standard !== $this->format) {
$start = '';
$end = "\n";
$separator = "\n";
}
$prettyPrintFormatter = $this->prettyPrint(...);
}
$flags = ($this->flags & ~JSON_PRETTY_PRINT) | JSON_THROW_ON_ERROR;
$this->chunkSize = $chunkSize;
$this->start = $start;
$this->end = $end;
$this->separator = $separator;
$this->jsonEncodeChunk = match ($this->format) {
JsonFormat::Standard => fn (array $chunk): string => ($prettyPrintFormatter)(substr(json_encode(($chunkFormatter)($chunk), $flags, $this->depth), /* @phpstan-ignore-line */ 1, -1)),
default => fn (array $chunk): string => implode($this->separator, array_map(fn ($value) => json_encode(($chunkFormatter)($value), $flags, $this->depth), $chunk)),
};
}
/**
* Pretty Print the JSON string without using JSON_PRETTY_PRINT
* The method also allow using an arbitrary length for the indentation.
*/
private function prettyPrint(string $json): string
{
$level = 1;
$inQuotes = false;
$escape = false;
$length = strlen($json);
$str = [$this->indentation];
for ($i = 0; $i < $length; $i++) {
$char = $json[$i];
if ('"' === $char && !$escape) {
$inQuotes = !$inQuotes;
}
$escape = '\\' === $char && !$escape;
$str[] = $inQuotes ? $char : match ($char) {
'{', '[' => $char.($this->indentationLevels[++$level] ??= "\n".str_repeat($this->indentation, $level)),
'}', ']' => ($this->indentationLevels[--$level] ??= "\n".str_repeat($this->indentation, $level)).$char,
',' => $char.($this->indentationLevels[$level] ??= "\n".str_repeat($this->indentation, $level)),
':' => $char.' ',
default => $char,
};
}
return implode('', $str);
}
/**
* @throws BadMethodCallException
*/
public function __call(string $name, array $arguments): self|bool
{
return match (true) {
str_starts_with($name, 'without') => $this->removeFlags(self::methodToFlag($name, 7)),
str_starts_with($name, 'with') => $this->addFlags(self::methodToFlag($name, 4)),
str_starts_with($name, 'use') => $this->useFlags(self::methodToFlag($name, 3)),
default => throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.'),
};
}
/**
* @param int<1, max>|null $indentSize
*/
public function withPrettyPrint(?int $indentSize = null): self
{
$flags = $this->flags | JSON_PRETTY_PRINT;
$indentSize = $indentSize ?? $this->indentSize;
return match (true) {
$flags === $this->flags && $indentSize === $this->indentSize => $this,
default => new self($flags, $this->depth, $indentSize, $this->formatter, $this->chunkSize, $this->format),
};
}
/**
* Returns the PHP json flag associated to its method suffix to ease method lookup.
*/
private static function methodToFlag(string $method, int $prefixSize): int
{
static $suffix2Flag;
if (null === $suffix2Flag) {
$suffix2Flag = [];
/** @var array<string, int> $jsonFlags */
$jsonFlags = get_defined_constants(true)['json'];
$jsonEncodeFlags = array_filter(
$jsonFlags,
fn (string $key) => 1 !== preg_match('/^(JSON_BIGINT_AS_STRING|JSON_OBJECT_AS_ARRAY|JSON_ERROR_)(.*)?$/', $key),
ARRAY_FILTER_USE_KEY
);
foreach ($jsonEncodeFlags as $name => $value) {
$suffix2Flag[str_replace('_', '', ucwords(strtolower(substr($name, 5)), '_'))] = $value;
}
}
return $suffix2Flag[substr($method, $prefixSize)]
?? throw new BadMethodCallException('The method "'.self::class.'::'.$method.'" does not exist.');
}
/**
* Adds a list of JSON flags.
*/
public function addFlags(int ...$flags): self
{
return $this->setFlags(
array_reduce($flags, fn (int $carry, int $flag): int => $carry | $flag, $this->flags)
);
}
/**
* Removes a list of JSON flags.
*/
public function removeFlags(int ...$flags): self
{
return $this->setFlags(
array_reduce($flags, fn (int $carry, int $flag): int => $carry & ~$flag, $this->flags)
);
}
/**
* Tells whether the flag is being used by the current JsonConverter.
*/
public function useFlags(int ...$flags): bool
{
foreach ($flags as $flag) {
// the JSON_THROW_ON_ERROR flag is always used even if it is not set by the user
if (JSON_THROW_ON_ERROR !== $flag && ($this->flags & $flag) !== $flag) {
return false;
}
}
return [] !== $flags;
}
public function format(JsonFormat $format): self
{
return match ($format) {
$this->format => $this,
default => new self($this->flags, $this->depth, $this->indentSize, $this->formatter, $this->chunkSize, $format),
};
}
/**
* Sets the encoding flags.
*/
private function setFlags(int $flags): self
{
return match ($flags) {
$this->flags => $this,
default => new self($flags, $this->depth, $this->indentSize, $this->formatter, $this->chunkSize, $this->format),
};
}
/**
* Set the depth of Json encoding.
*
* @param int<1, max> $depth
*/
public function depth(int $depth): self
{
return match ($depth) {
$this->depth => $this,
default => new self($this->flags, $depth, $this->indentSize, $this->formatter, $this->chunkSize, $this->format),
};
}
/**
* Set the indentation size.
*
* @param int<1, max> $chunkSize
*/
public function chunkSize(int $chunkSize): self
{
return match ($chunkSize) {
$this->chunkSize => $this,
default => new self($this->flags, $this->depth, $this->indentSize, $this->formatter, $chunkSize, $this->format),
};
}
/**
* Set a callback to format each item before json encode.
*/
public function formatter(?callable $formatter): self
{
return new self($this->flags, $this->depth, $this->indentSize, $formatter, $this->chunkSize, $this->format);
}
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable($this): bool)|bool $condition
* @param callable($this): (self|null) $onSuccess
* @param ?callable($this): (self|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
/**
* Sends and makes the JSON structure downloadable via HTTP.
*.
* Returns the number of characters read from the handle and passed through to the output.
*
* @param TabularDataProvider|TabularData|iterable<T> $records
* @param array<string> $header
*
* @throws Exception
* @throws JsonException
*/
public function download(TabularDataProvider|TabularData|iterable $records, ?string $filename = null, array $header = []): int
{
if (null !== $filename) {
$mimetype = JsonFormat::Standard === $this->format ? 'application/json' : 'application/x-ndjson';
HttpHeaders::forFileDownload($filename, $mimetype.'; charset=utf-8');
}
return $this->save(
records: $records,
destination: new SplFileObject('php://output', 'wb'),
header: $header,
);
}
/**
* Returns the JSON representation of a tabular data collection.
*
* @param TabularDataProvider|TabularData|iterable<T> $records
* @param array<string> $header
*
* @throws Exception
* @throws JsonException
*/
public function encode(TabularDataProvider|TabularData|iterable $records, array $header = []): string
{
$stream = Stream::fromString();
$this->save(records: $records, destination: $stream, header: $header);
$stream->rewind();
return (string) $stream->getContents();
}
/**
* Store the generated JSON in the destination filepath.
*
* if a Path or a SplFileInfo object is given,
* the file will be emptying before adding the JSON
* content to it. For all the other types you are
* required to provide a file with the correct open
* mode.
*
* @param TabularDataProvider|TabularData|iterable<T> $records
* @param SplFileInfo|SplFileObject|Stream|resource|string $destination
* @param resource|null $context
* @param array<string> $header
*
* @throws JsonException
* @throws RuntimeException
* @throws TypeError
* @throws UnavailableStream
*/
public function save(TabularDataProvider|TabularData|iterable $records, mixed $destination, $context = null, array $header = []): int
{
$stream = match (true) {
$destination instanceof Stream,
$destination instanceof SplFileObject => $destination,
$destination instanceof SplFileInfo => $destination->openFile(mode:'wb', context: $context),
default => Stream::from($destination, 'wb', $context),
};
$bytes = 0;
$writtenBytes = 0;
foreach ($this->convert($records, $header) as $line) {
/** @var int|false $writtenBytes */
$writtenBytes = Warning::cloak($stream->fwrite(...), $line);
if (false === $writtenBytes) {
break;
}
$bytes += $writtenBytes;
}
false !== $writtenBytes || throw new RuntimeException('Unable to write '.(isset($line) ? '`'.$line.'`' : '').' to the destination path `'.$stream->getPathname().'`.');
return $bytes;
}
/**
* Returns an Iterator that you can iterate to generate the actual JSON string representation.
*
* @param TabularDataProvider|TabularData|iterable<T> $records
* @param array<string> $header
*
* @throws JsonException
* @throws Exception
*
* @return Iterator<string>
*/
public function convert(TabularDataProvider|TabularData|iterable $records, array $header = []): Iterator
{
if ($records instanceof TabularDataProvider) {
$records = $records->getTabularData();
}
if ($records instanceof TabularData) {
$records = $records->getRecords();
}
$iterator = match ($this->formatter) {
null => MapIterator::toIterator($records),
default => MapIterator::fromIterable($records, $this->formatter)
};
if (in_array($this->format, [JsonFormat::NdJsonHeader, JsonFormat::NdJsonHeaderLess], true)) {
$iterator = self::getList($iterator, $header, $this->format)();
}
$iterator->rewind();
if (!$iterator->valid()) {
yield $this->emptyIterable;
return;
}
$chunk = [];
$chunkOffset = 0;
$offset = 0;
$current = $iterator->current();
$iterator->next();
if (JsonFormat::Standard === $this->format) {
yield $this->start;
}
while ($iterator->valid()) {
if ($chunkOffset === $this->chunkSize) {
yield ($this->jsonEncodeChunk)($chunk).$this->separator;
$chunkOffset = 0;
$chunk = [];
}
$chunk[$offset] = $current;
++$chunkOffset;
++$offset;
$current = $iterator->current();
$iterator->next();
}
if ([] !== $chunk) {
yield ($this->jsonEncodeChunk)($chunk).$this->separator;
}
yield ($this->jsonEncodeChunk)([$offset => $current]).$this->end;
}
/**
* @param array<string> $header
*
* @throws InvalidArgument
*
* @return Closure(): Generator
*/
private static function getList(Iterator $data, array $header, JsonFormat $format): Closure
{
if (JsonFormat::NdJsonHeaderLess === $format) {
return fn () => yield from new MapIterator($data, fn (array $record): array => array_values($record));
}
[] !== $header || throw new InvalidArgument('A non empty header must be provided when using `JsonFormat::NdJsonHeader`.');
return function () use ($header, $data) {
yield $header;
yield from new MapIterator($data, fn (array $record): array => array_values($record));
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see JsonConverter::withPrettyPrint()
* @deprecated Since version 9.19.0
* @codeCoverageIgnore
*
* Set the indentation size.
*
* @param int<1, max> $indentSize
*/
#[Deprecated(message:'use League\Csv\JsonConverter::withPrettyPrint() instead', since:'league/csv:9.19.0')]
public function indentSize(int $indentSize): self
{
return match ($indentSize) {
$this->indentSize => $this,
default => new self($this->flags, $this->depth, $indentSize, $this->formatter, $this->chunkSize, $this->format),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see JsonConverter::__construct()
* @deprecated Since version 9.22.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Csv\JsonConverter::__construct() instead', since:'league/csv:9.22.0')]
public static function create(): self
{
return new self(
flags: 0,
depth: 512,
indentSize: 4,
formatter: null,
chunkSize: 500,
jsonFormat: JsonFormat::Standard
);
}
}