diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1c1171..b4c8a75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All Notable changes to `Csv` will be documented in this file - `Cast*` methods accept more input type to improve Denormalization usage when `Reader::addFormatter` is used or when the collection contains data other than string and `null`. - `Stream::getSize` is added to the internal `Stream` class +- `Stream::getContents` is added to the internal `Stream` class - `MapIterator::toIterator` is added to the internal class `MapIterator` class to convert any `iterable` into an `Iterator`. - Casting a CSV to an `array` it now will be a collection of array instead of a simple `array`. diff --git a/docs/9.0/converter/json.md b/docs/9.0/converter/json.md index ea10c399..d0b626de 100644 --- a/docs/9.0/converter/json.md +++ b/docs/9.0/converter/json.md @@ -15,17 +15,38 @@ the converter.
-### JsonConverter::addFlags and JsonConverter::removeFlags +### JSON encode flags ```php public JsonConverter::addFlags(int ...$flag): self public JsonConverter::removeFlags(int ...$flag): self ``` -This method sets the JSON flags to be used during conversion. The method handles all the +These methods set the JSON flags to be used during conversion. The method handles all the flags supported by PHP `json_encode` function. -### JsonConverter::depth +If you prefer a more expressive way for setting the flags you can use `with*` and `without*` methods +whose name are derived from PHP JSON constants. + +```php +$converter = JsonConverter::create() + ->addFlags(JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT) + ->removeFlags(JSON_HEX_QUOT); + +//is equivalent to + +$converter = JsonConverter::create() + ->withPrettyPrint() + ->withUnescapedSlashes() + ->withForceObject() + ->withoutHexQuot(); +``` + + + +### Json encode depth ```php public JsonConverter::depth(int $depth): self @@ -34,7 +55,7 @@ public JsonConverter::depth(int $depth): self This method sets the JSON depth value during conversion. The method is a proxy to using the `json_encode` depth parameter. -### JsonConverter::indentSize +### Json encode indentation ```php public JsonConverter::indentSize(int $indentSize): self @@ -44,7 +65,7 @@ This method sets the JSON indentation size value if you use the `JSON_PRETTY_PRI all other situation this value stored via this method is never used. By default, the identation size is the same as in PHP (ie : 4 characters long). -### JsonConverter::formatter +### Json encode formatter ```php public JsonConverter::formatter(?callback $formatter): mixed @@ -52,7 +73,7 @@ public JsonConverter::formatter(?callback $formatter): mixed This method allow to apply a callback prior to `json_encode` your collection individual item. Since the encoder does not rely on PHP's `JsonSerializable` interface but on PHP's `iterable` -structure. The expected conversion may differ to what you expect. This callback allows you to +structure. The resulting conversion may differ to what you expect. This callback allows you to specify how each item will be converted. The formatter should return a type that can be handled by PHP `json_encode` function. @@ -81,7 +102,8 @@ $document->setHeaderOffset(0); CharsetConverter::addTo($document, 'iso-8859-15', 'utf-8'); $converter = JsonConverter::create() - ->addFlags(JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES) + ->withPrettyPrint() + ->withUnescapedSlashes() ->depth(2) ->indentSize(2) ->formatter(function (array $row) { diff --git a/docs/9.0/reader/index.md b/docs/9.0/reader/index.md index 47371930..cc7de0e9 100644 --- a/docs/9.0/reader/index.md +++ b/docs/9.0/reader/index.md @@ -324,6 +324,10 @@ found records are returned as a [ResultSet](/9.0/reader/resultset) object. ### Json serialization + + The `Reader` class implements the `JsonSerializable` interface. As such you can use the `json_encode` function directly on the instantiated object. The interface is implemented using PHP's `iterator_array` on the `Reader::getRecords` method. As such, the returned `JSON` diff --git a/docs/9.0/reader/resultset.md b/docs/9.0/reader/resultset.md index a2a6bf74..bcf0974b 100644 --- a/docs/9.0/reader/resultset.md +++ b/docs/9.0/reader/resultset.md @@ -21,6 +21,10 @@ found records are returned as a [ResultSet](/9.0/reader/resultset) object. ### Json serialization + + The `ResultSet` class implements the `JsonSerializable` interface. As such you can use the `json_encode` function directly on the instantiated object. The interface is implemented using PHP's `iterator_array` on the `ResultSet::getRecords` method. As such, the returned `JSON` string data is affected by the diff --git a/src/JsonConverter.php b/src/JsonConverter.php index b0d8adef..eac698ba 100644 --- a/src/JsonConverter.php +++ b/src/JsonConverter.php @@ -13,6 +13,7 @@ namespace League\Csv; +use BadMethodCallException; use Closure; use Exception; use InvalidArgumentException; @@ -22,6 +23,26 @@ use SplFileInfo; use SplFileObject; +use function array_filter; +use function array_reduce; +use function get_defined_constants; +use function in_array; +use function is_resource; +use function is_string; +use function json_encode; +use function json_last_error; +use function lcfirst; +use function restore_error_handler; +use function set_error_handler; +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_FORCE_OBJECT; use const JSON_PRETTY_PRINT; @@ -30,41 +51,65 @@ /** * 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 JsonConverter withHexAmp() adds the JSON_HEX_AMP flag + * @method JsonConverter withoutHexAmp() removes the JSON_HEX_AMP flag + * @method JsonConverter withHexApos() adds the JSON_HEX_APOS flag + * @method JsonConverter withoutHexApos() removes the JSON_HEX_APOS flag + * @method JsonConverter withHexQuot() adds the JSON_HEX_QUOT flag + * @method JsonConverter withoutHexQuot() removes the JSON_HEX_QUOT flag + * @method JsonConverter withForceObject() adds the JSON_FORCE_OBJECT flag + * @method JsonConverter withoutForceObject() removes the JSON_FORCE_OBJECT flag + * @method JsonConverter withNumericCheck() adds the JSON_NUMERIC_CHECK flag + * @method JsonConverter withoutNumericCheck() removes the JSON_NUMERIC_CHECK flag + * @method JsonConverter withUnescapedSlashes() adds the JSON_UNESCAPED_SLASHES flag + * @method JsonConverter withoutUnescapedSlashes() removes the JSON_UNESCAPED_SLASHES flag + * @method JsonConverter withPrettyPrint() adds the JSON_PRETTY_PRINT flag + * @method JsonConverter withoutPrettyPrint() removes the JSON_PRETTY_PRINT flag + * @method JsonConverter withUnescapedUnicode() adds the JSON_UNESCAPED_UNICODE flag + * @method JsonConverter withoutUnescapedUnicode() removes the JSON_UNESCAPED_UNICODE flag + * @method JsonConverter withPartialOutputOnError() adds the JSON_PARTIAL_OUTPUT_ON_ERROR flag + * @method JsonConverter withoutPartialOutputOnError() removes the JSON_PARTIAL_OUTPUT_ON_ERROR flag + * @method JsonConverter withPreserveZeroFraction() adds the JSON_PRESERVE_ZERO_FRACTION flag + * @method JsonConverter withoutPreserveZeroFraction() removes the JSON_PRESERVE_ZERO_FRACTION flag + * @method JsonConverter withUnescapedLineTerminators() adds the JSON_UNESCAPED_LINE_TERMINATORS flag + * @method JsonConverter withoutUnescapedLineTerminators() removes the JSON_UNESCAPED_LINE_TERMINATORS flag + * @method JsonConverter withInvalidUtf8Ignore() adds the JSON_INVALID_UTF8_IGNORE flag + * @method JsonConverter withoutInvalidUtf8Ignore() removes the JSON_INVALID_UTF8_IGNORE flag + * @method JsonConverter withInvalidUtf8Substitute() adds the JSON_INVALID_UTF8_SUBSTITUTE flag + * @method JsonConverter withoutInvalidUtf8Substitute() removes the JSON_INVALID_UTF8_SUBSTITUTE flag + * @method JsonConverter withThrowOnError() adds the JSON_THROW_ON_ERROR flag + * @method JsonConverter withoutThrowOnError() removes the JSON_THROW_ON_ERROR flag */ final class JsonConverter { public readonly int $flags; /** @var int<1, max> */ public readonly int $depth; - /** @var non-empty-string */ - public readonly string $indentation; + /** @var int<1, max> */ + public readonly int $indentSize; /** @var Closure(T, array-key): mixed */ public readonly Closure $formatter; - public readonly bool $isPrettyPrint; - public readonly bool $isForceObject; + private readonly bool $isPrettyPrint; + private readonly bool $isForceObject; + /** @var non-empty-string */ + private readonly string $indentation; /** @var Closure(string, array-key): string */ private readonly Closure $internalFormatter; public static function create(): self { - return new self( - flags: 0, - depth: 512, - indentSize: 4, - formatter: null, - ); + return new self(flags: 0, depth: 512, indentSize: 4, formatter: null); } /** * @param int<1, max> $depth * @param int<1, max> $indentSize */ - private function __construct( - int $flags, - int $depth, - int $indentSize, - ?Closure $formatter - ) { + private function __construct(int $flags, int $depth, int $indentSize, ?Closure $formatter) + { json_encode([], $flags & ~JSON_THROW_ON_ERROR, $depth); JSON_ERROR_NONE === json_last_error() || throw new InvalidArgumentException('The flags or the depth given are not valid JSON encoding parameters in PHP; '.json_last_error_msg()); @@ -72,6 +117,7 @@ private function __construct( $this->flags = $flags; $this->depth = $depth; + $this->indentSize = $indentSize; $this->indentation = str_repeat(' ', $indentSize); $this->formatter = $formatter ?? fn (mixed $value) => $value; $this->isPrettyPrint = ($this->flags & JSON_PRETTY_PRINT) === JSON_PRETTY_PRINT; @@ -80,29 +126,89 @@ private function __construct( } /** - * Adds a list of JSON flags. + * @return Closure(string, array-key): string + */ + private function setInternalFormatter(): Closure + { + $callback = match ($this->isForceObject) { + false => fn (string $json, int|string $offset): string => $json, + default => fn (string $json, int|string $offset): string => '"'.json_encode($offset).'":'.$json, + }; + + return match ($this->isPrettyPrint) { + false => $callback, + default => fn (string $json, int|string $offset): string => $this->prettyPrint($callback($json, $offset)), + }; + } + + /** + * @throws BadMethodCallException + */ + public function __call(string $name, array $arguments): self + { + return match (true) { + str_starts_with($name, 'without') => $this->removeFlags(self::methodToFlag()[lcfirst(substr($name, 7))] ?? throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.')), + str_starts_with($name, 'with') => $this->addFlags(self::methodToFlag()[lcfirst(substr($name, 4))] ?? throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.')), + default => throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.'), + }; + } + + /** + * Returns the PHP json flag associated to its method suffix to ease method lookup. + * + * @return array