From 63c9cc9bb6e7571988397dbe83fb946b05601bce Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Sun, 6 Sep 2026 12:57:58 +0200 Subject: [PATCH 1/9] feat: Introduce JSON utility class with encoding, decoding, and validation methods; deprecate JsonDecoder --- phpstan-baseline.neon | 30 ++++++++++++++++++++++++ src/JSON.php | 52 +++++++++++++++++++++++++++++++++++++++++ src/JsonDecoder.php | 3 +++ tests/Unit/JSONTest.php | 31 ++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/JSON.php create mode 100644 tests/Unit/JSONTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8cca3a8..1bf9043 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,6 +72,36 @@ parameters: count: 1 path: tests/Unit/FullTextTest.php + - + message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 2 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' identifier: method.internalClass diff --git a/src/JSON.php b/src/JSON.php new file mode 100644 index 0000000..858eb2c --- /dev/null +++ b/src/JSON.php @@ -0,0 +1,52 @@ + $depth + * + * @throws JsonException + */ + public static function decode(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + return json_decode($json, $associative, $depth, JSON_THROW_ON_ERROR | $flags); + } + + /** + * @param int<1, 2147483647> $depth + */ + public static function decodeSilently(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + try { + return self::decode($json, $associative, $depth, $flags); + } catch (JsonException) { + return null; + } + } + + /** + * @param int<1, 2147483647> $depth + * @param int-mask-of<0|1048576> $flags + */ + public static function validate(#[Language('JSON')] string $json, int $depth = 512, int $flags = 0): bool + { + return json_validate($json, $depth, $flags); + } +} diff --git a/src/JsonDecoder.php b/src/JsonDecoder.php index 70e8267..b266cfc 100644 --- a/src/JsonDecoder.php +++ b/src/JsonDecoder.php @@ -7,6 +7,9 @@ use Artemeon\Support\Exception\InvalidJsonFormatException; use JsonException; +/** + * @deprecated Use {@see JSON} instead. + */ final class JsonDecoder { public static function decode(string $json, bool $assoc = true, int $flags = 0): mixed diff --git a/tests/Unit/JSONTest.php b/tests/Unit/JSONTest.php new file mode 100644 index 0000000..82bddae --- /dev/null +++ b/tests/Unit/JSONTest.php @@ -0,0 +1,31 @@ + 'bar'])) + ->toBe('{"foo":"bar"}'); +}); + +it('should decode data', function (): void { + expect(JSON::decode('{"foo": "bar"}', true)) + ->toBe(['foo' => 'bar']); +}); + +it('should throw with invalid data', function (): void { + JSON::decode('lol'); +})->throws(JsonException::class); + +it('should return null and not throw with invalid data', function (): void { + expect(JSON::decodeSilently('lol')) + ->toBeNull(); +}); + +it('should validate data', function (): void { + expect(JSON::validate('{"foo": "bar"}')) + ->toBeTrue() + ->and(JSON::validate('lol')) + ->toBeFalse(); +}); From 48662b57ab22d5d0806929f7c84f538325ef445c Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Sun, 6 Sep 2026 12:57:58 +0200 Subject: [PATCH 2/9] feat: Introduce JSON utility class with encoding, decoding, and validation methods; deprecate JsonDecoder --- phpstan-baseline.neon | 30 ++++++++++++++++++++++++ src/JSON.php | 52 +++++++++++++++++++++++++++++++++++++++++ src/JsonDecoder.php | 3 +++ tests/Unit/JSONTest.php | 31 ++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/JSON.php create mode 100644 tests/Unit/JSONTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8cca3a8..1bf9043 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,6 +72,36 @@ parameters: count: 1 path: tests/Unit/FullTextTest.php + - + message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 2 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' identifier: method.internalClass diff --git a/src/JSON.php b/src/JSON.php new file mode 100644 index 0000000..858eb2c --- /dev/null +++ b/src/JSON.php @@ -0,0 +1,52 @@ + $depth + * + * @throws JsonException + */ + public static function decode(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + return json_decode($json, $associative, $depth, JSON_THROW_ON_ERROR | $flags); + } + + /** + * @param int<1, 2147483647> $depth + */ + public static function decodeSilently(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + try { + return self::decode($json, $associative, $depth, $flags); + } catch (JsonException) { + return null; + } + } + + /** + * @param int<1, 2147483647> $depth + * @param int-mask-of<0|1048576> $flags + */ + public static function validate(#[Language('JSON')] string $json, int $depth = 512, int $flags = 0): bool + { + return json_validate($json, $depth, $flags); + } +} diff --git a/src/JsonDecoder.php b/src/JsonDecoder.php index 70e8267..b266cfc 100644 --- a/src/JsonDecoder.php +++ b/src/JsonDecoder.php @@ -7,6 +7,9 @@ use Artemeon\Support\Exception\InvalidJsonFormatException; use JsonException; +/** + * @deprecated Use {@see JSON} instead. + */ final class JsonDecoder { public static function decode(string $json, bool $assoc = true, int $flags = 0): mixed diff --git a/tests/Unit/JSONTest.php b/tests/Unit/JSONTest.php new file mode 100644 index 0000000..82bddae --- /dev/null +++ b/tests/Unit/JSONTest.php @@ -0,0 +1,31 @@ + 'bar'])) + ->toBe('{"foo":"bar"}'); +}); + +it('should decode data', function (): void { + expect(JSON::decode('{"foo": "bar"}', true)) + ->toBe(['foo' => 'bar']); +}); + +it('should throw with invalid data', function (): void { + JSON::decode('lol'); +})->throws(JsonException::class); + +it('should return null and not throw with invalid data', function (): void { + expect(JSON::decodeSilently('lol')) + ->toBeNull(); +}); + +it('should validate data', function (): void { + expect(JSON::validate('{"foo": "bar"}')) + ->toBeTrue() + ->and(JSON::validate('lol')) + ->toBeFalse(); +}); From 39b57b69b2b5e2d974256483f7d968a1fd1388a4 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Sun, 6 Sep 2026 13:27:43 +0200 Subject: [PATCH 3/9] chore: Update PHPStan baseline messages and increment error counts --- phpstan-baseline.neon | 62 +++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1bf9043..e0cbca5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,181 +1,181 @@ parameters: ignoreErrors: - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 11 path: tests/Unit/ArrayIteratorTest.php - - message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeTrue() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/ArrayIteratorTest.php - - message: '#^Call to method with\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method with() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 5 path: tests/Unit/ArrayIteratorTest.php - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass - count: 25 + count: 26 path: tests/Unit/ArraySectionIteratorTest.php - - message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeFalse() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/ArraySectionIteratorTest.php - - message: '#^Call to method toBeJson\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeJson() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 2 path: tests/Unit/ArraySectionIteratorTest.php - - message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeTrue() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/ArraySectionIteratorTest.php - - message: '#^Call to method toHaveKeys\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toHaveKeys() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/ArraySectionIteratorTest.php - - message: '#^Call to method with\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method with() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 11 path: tests/Unit/ArraySectionIteratorTest.php - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/FullTextTest.php - - message: '#^Call to method toBeGreaterThan\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeGreaterThan() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/FullTextTest.php - - message: '#^Call to method with\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method with() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/FullTextTest.php - - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method throws() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 2 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeFalse() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeNull() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeTrue() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method throws() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method toBeArray\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeArray() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeFalse() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeNull() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method toBeObject\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeObject() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeTrue() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method with\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method with() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 6 path: tests/Unit/JsonDecoderTest.php - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 8 path: tests/Unit/StringableTest.php - - message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeTrue() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 3 path: tests/Unit/StringableTest.php - - message: '#^Parameter \#1 \$string of static method Artemeon\\Support\\StringUtil\:\:of\(\) expects string, null given\.$#' + rawMessage: 'Parameter #1 $string of static method Artemeon\Support\StringUtil::of() expects string, null given.' identifier: argument.type count: 1 path: tests/Unit/StringableTest.php - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/TimerTest.php - - message: '#^Call to method toBeLessThan\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeLessThan() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 2 path: tests/Unit/TimerTest.php - - message: '#^Call to method with\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method with() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/TimerTest.php From f91c52f933c1d5641ba971adba6e96b4aee5ed63 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Sun, 6 Sep 2026 12:57:58 +0200 Subject: [PATCH 4/9] feat: Introduce JSON utility class with encoding, decoding, and validation methods; deprecate JsonDecoder --- phpstan-baseline.neon | 30 ++++++++++++++++++++++++ src/JSON.php | 52 +++++++++++++++++++++++++++++++++++++++++ src/JsonDecoder.php | 3 +++ tests/Unit/JSONTest.php | 31 ++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/JSON.php create mode 100644 tests/Unit/JSONTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1d1b842..6b17c04 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -76,6 +76,36 @@ parameters: rawMessage: 'Call to method throws() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 2 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 path: tests/Unit/JsonDecoderTest.php - diff --git a/src/JSON.php b/src/JSON.php new file mode 100644 index 0000000..858eb2c --- /dev/null +++ b/src/JSON.php @@ -0,0 +1,52 @@ + $depth + * + * @throws JsonException + */ + public static function decode(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + return json_decode($json, $associative, $depth, JSON_THROW_ON_ERROR | $flags); + } + + /** + * @param int<1, 2147483647> $depth + */ + public static function decodeSilently(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + try { + return self::decode($json, $associative, $depth, $flags); + } catch (JsonException) { + return null; + } + } + + /** + * @param int<1, 2147483647> $depth + * @param int-mask-of<0|1048576> $flags + */ + public static function validate(#[Language('JSON')] string $json, int $depth = 512, int $flags = 0): bool + { + return json_validate($json, $depth, $flags); + } +} diff --git a/src/JsonDecoder.php b/src/JsonDecoder.php index 70e8267..b266cfc 100644 --- a/src/JsonDecoder.php +++ b/src/JsonDecoder.php @@ -7,6 +7,9 @@ use Artemeon\Support\Exception\InvalidJsonFormatException; use JsonException; +/** + * @deprecated Use {@see JSON} instead. + */ final class JsonDecoder { public static function decode(string $json, bool $assoc = true, int $flags = 0): mixed diff --git a/tests/Unit/JSONTest.php b/tests/Unit/JSONTest.php new file mode 100644 index 0000000..82bddae --- /dev/null +++ b/tests/Unit/JSONTest.php @@ -0,0 +1,31 @@ + 'bar'])) + ->toBe('{"foo":"bar"}'); +}); + +it('should decode data', function (): void { + expect(JSON::decode('{"foo": "bar"}', true)) + ->toBe(['foo' => 'bar']); +}); + +it('should throw with invalid data', function (): void { + JSON::decode('lol'); +})->throws(JsonException::class); + +it('should return null and not throw with invalid data', function (): void { + expect(JSON::decodeSilently('lol')) + ->toBeNull(); +}); + +it('should validate data', function (): void { + expect(JSON::validate('{"foo": "bar"}')) + ->toBeTrue() + ->and(JSON::validate('lol')) + ->toBeFalse(); +}); From 4987fa714711ceb84c6c7a957144afe71117fb8d Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Sun, 6 Sep 2026 12:57:58 +0200 Subject: [PATCH 5/9] feat: Introduce JSON utility class with encoding, decoding, and validation methods; deprecate JsonDecoder --- phpstan-baseline.neon | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6b17c04..e514502 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -102,6 +102,36 @@ parameters: count: 1 path: tests/Unit/JSONTest.php + - + message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 2 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + + - + message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + identifier: method.internalClass + count: 1 + path: tests/Unit/JSONTest.php + - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' identifier: method.internalClass From 3911f6f961f7001456268d09403d8e39b6dd9358 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Sun, 6 Sep 2026 13:27:43 +0200 Subject: [PATCH 6/9] chore: Update PHPStan baseline messages and increment error counts --- phpstan-baseline.neon | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e514502..e751d27 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -103,37 +103,37 @@ parameters: path: tests/Unit/JSONTest.php - - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method throws() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBe\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBe() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 2 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBeFalse\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeFalse() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBeNull\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeNull() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method toBeTrue\(\) of internal class Pest\\Mixins\\Expectation\ from outside its root namespace Pest\.$#' + rawMessage: 'Call to method toBeTrue() of internal class Pest\Mixins\Expectation from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JSONTest.php - - message: '#^Call to method throws\(\) of internal class Pest\\PendingCalls\\TestCall from outside its root namespace Pest\.$#' + rawMessage: 'Call to method throws() of internal class Pest\PendingCalls\TestCall from outside its root namespace Pest.' identifier: method.internalClass count: 1 path: tests/Unit/JsonDecoderTest.php From 041a7fe2795889ef5b27797a9951962fd8f7fc7b Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 7 Sep 2026 19:38:08 +0200 Subject: [PATCH 7/9] feat: enhance JSON handling with new decoding helpers and expanded tests --- src/JSON.php | 26 ++++++++++++ tests/Unit/JSONTest.php | 90 +++++++++++++++++++++++++++++++++-------- 2 files changed, 99 insertions(+), 17 deletions(-) diff --git a/src/JSON.php b/src/JSON.php index 858eb2c..99cf165 100644 --- a/src/JSON.php +++ b/src/JSON.php @@ -29,6 +29,32 @@ public static function decode(#[Language('JSON')] string $json, ?bool $associati return json_decode($json, $associative, $depth, JSON_THROW_ON_ERROR | $flags); } + /** + * @param int<1, 2147483647> $depth + * + * @return array + * + * @throws JsonException + */ + public static function decodeAsArray(#[Language('JSON')] string $json, int $depth = 512, int $flags = 0): array + { + return is_array($decoded = self::decode($json, false, $depth, $flags)) + ? $decoded + : throw new JsonException(sprintf('Input JSON %s is expected to be an array. Got %s instead.', $json, gettype($decoded))); + } + + /** + * @param int<1, 2147483647> $depth + * + * @throws JsonException + */ + public static function decodeAsObject(#[Language('JSON')] string $json, int $depth = 512, int $flags = 0): object + { + return is_object($decoded = self::decode($json, false, $depth, $flags)) + ? $decoded + : throw new JsonException(sprintf('Input JSON %s is expected to be an object. Got %s instead.', $json, gettype($decoded))); + } + /** * @param int<1, 2147483647> $depth */ diff --git a/tests/Unit/JSONTest.php b/tests/Unit/JSONTest.php index 82bddae..f7be673 100644 --- a/tests/Unit/JSONTest.php +++ b/tests/Unit/JSONTest.php @@ -4,28 +4,84 @@ use Artemeon\Support\JSON; -it('should encode data', function (): void { - expect(JSON::encode(['foo' => 'bar'])) - ->toBe('{"foo":"bar"}'); +describe('JSON Encoding', function (): void { + it('should encode data', function (mixed $input, string $expected): void { + expect(JSON::encode($input)) + ->toBe($expected); + }) + ->with([ + [['foo' => 'bar'], '{"foo":"bar"}'], + [['foo' => 'bar', 'baz' => 'qux'], '{"foo":"bar","baz":"qux"}'], + [true, 'true'], + [false, 'false'], + [null, 'null'], + [123, '123'], + [123.456, '123.456'], + ['foo', '"foo"'], + ['foo"bar', '"foo\\"bar"'], + ]); }); -it('should decode data', function (): void { - expect(JSON::decode('{"foo": "bar"}', true)) - ->toBe(['foo' => 'bar']); +describe('Simple JSON Decoding', function (): void { + it('should decode data', function (): void { + expect(JSON::decode('{"foo": "bar"}', true)) + ->toBe(['foo' => 'bar']); + }); + + it('should throw with invalid data', function (): void { + JSON::decode('lol'); + })->throws(JsonException::class); + + it('should return null and not throw with invalid data', function (): void { + expect(JSON::decodeSilently('lol')) + ->toBeNull(); + }); +}); + +describe('JSON Validation', function (): void { + it('should validate data', function (): void { + expect(JSON::validate('{"foo": "bar"}')) + ->toBeTrue() + ->and(JSON::validate('lol')) + ->toBeFalse(); + }); }); -it('should throw with invalid data', function (): void { - JSON::decode('lol'); -})->throws(JsonException::class); +$nonArrayAndObjectJsonData = [ + ['true', 'boolean'], + ['false', 'boolean'], + ['"foo"', 'string'], + ['123', 'integer'], + ['123.456', 'double'], + ['null', 'NULL'], +]; + +describe('JSON Array Decoding', function () use ($nonArrayAndObjectJsonData): void { + it('should decode as an array', function (): void { + expect(JSON::decodeAsArray('["foo", "bar"]')) + ->toBe(['foo', 'bar']); + }); -it('should return null and not throw with invalid data', function (): void { - expect(JSON::decodeSilently('lol')) - ->toBeNull(); + it('should throw with invalid array', function (string $json, string $type): void { + expect(fn (): array => JSON::decodeAsArray($json)) + ->toThrow(JsonException::class, sprintf('Input JSON %s is expected to be an array. Got %s instead.', $json, $type)); + })->with([ + ['{"foo": "bar"}', 'object'], + ...$nonArrayAndObjectJsonData, + ]); }); -it('should validate data', function (): void { - expect(JSON::validate('{"foo": "bar"}')) - ->toBeTrue() - ->and(JSON::validate('lol')) - ->toBeFalse(); +describe('JSON Object Decoding', function () use ($nonArrayAndObjectJsonData): void { + it('should decode as an object', function (): void { + expect(JSON::decodeAsObject('{"foo": "bar"}')) + ->toEqual((object) ['foo' => 'bar']); + }); + + it('should throw with invalid object', function (string $json, string $type): void { + expect(fn (): object => JSON::decodeAsObject($json)) + ->toThrow(JsonException::class, sprintf('Input JSON %s is expected to be an object. Got %s instead.', $json, $type)); + })->with([ + ['["foo", "bar"]', 'array'], + ...$nonArrayAndObjectJsonData, + ]); }); From d9d3aa14224d8e8cfe0561063e77c8d83ab69cd6 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 7 Sep 2026 19:44:56 +0200 Subject: [PATCH 8/9] refactor: reorder `decodeSilently` method position in JSON.php file --- src/JSON.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/JSON.php b/src/JSON.php index 99cf165..6e0355b 100644 --- a/src/JSON.php +++ b/src/JSON.php @@ -29,6 +29,18 @@ public static function decode(#[Language('JSON')] string $json, ?bool $associati return json_decode($json, $associative, $depth, JSON_THROW_ON_ERROR | $flags); } + /** + * @param int<1, 2147483647> $depth + */ + public static function decodeSilently(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed + { + try { + return self::decode($json, $associative, $depth, $flags); + } catch (JsonException) { + return null; + } + } + /** * @param int<1, 2147483647> $depth * @@ -55,18 +67,6 @@ public static function decodeAsObject(#[Language('JSON')] string $json, int $dep : throw new JsonException(sprintf('Input JSON %s is expected to be an object. Got %s instead.', $json, gettype($decoded))); } - /** - * @param int<1, 2147483647> $depth - */ - public static function decodeSilently(#[Language('JSON')] string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed - { - try { - return self::decode($json, $associative, $depth, $flags); - } catch (JsonException) { - return null; - } - } - /** * @param int<1, 2147483647> $depth * @param int-mask-of<0|1048576> $flags From ebe7bfc6197e40c3eaf3f314f26e222665981d48 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Mon, 7 Sep 2026 19:46:08 +0200 Subject: [PATCH 9/9] style: fix docblock order in `decodeAsArray` method --- src/JSON.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JSON.php b/src/JSON.php index 6e0355b..70cde82 100644 --- a/src/JSON.php +++ b/src/JSON.php @@ -44,9 +44,9 @@ public static function decodeSilently(#[Language('JSON')] string $json, ?bool $a /** * @param int<1, 2147483647> $depth * - * @return array - * * @throws JsonException + * + * @return array */ public static function decodeAsArray(#[Language('JSON')] string $json, int $depth = 512, int $flags = 0): array {