mirror of https://github.com/flightphp/core
commit
b331797ae3
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace flight\util;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use JsonException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json utility class for encoding and decoding JSON data.
|
||||||
|
*
|
||||||
|
* This class provides centralized JSON handling for the FlightPHP framework,
|
||||||
|
* with consistent error handling and default options.
|
||||||
|
*/
|
||||||
|
class Json
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Default JSON encoding options
|
||||||
|
*/
|
||||||
|
public const DEFAULT_ENCODE_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default JSON decoding options
|
||||||
|
*/
|
||||||
|
public const DEFAULT_DECODE_OPTIONS = JSON_THROW_ON_ERROR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes data to JSON string.
|
||||||
|
*
|
||||||
|
* @param mixed $data Data to encode
|
||||||
|
* @param int $options JSON encoding options (bitmask)
|
||||||
|
* @param int $depth Maximum depth
|
||||||
|
*
|
||||||
|
* @return string JSON encoded string
|
||||||
|
* @throws Exception If encoding fails
|
||||||
|
*/
|
||||||
|
public static function encode($data, int $options = 0, int $depth = 512): string
|
||||||
|
{
|
||||||
|
$options = $options | self::DEFAULT_ENCODE_OPTIONS; // Ensure default options are applied
|
||||||
|
try {
|
||||||
|
return json_encode($data, $options, $depth);
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new Exception('JSON encoding failed: ' . $e->getMessage(), $e->getCode(), $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes JSON string to PHP data.
|
||||||
|
*
|
||||||
|
* @param string $json JSON string to decode
|
||||||
|
* @param bool $associative Whether to return associative arrays instead of objects
|
||||||
|
* @param int $depth Maximum decoding depth
|
||||||
|
* @param int $options JSON decoding options (bitmask)
|
||||||
|
*
|
||||||
|
* @return mixed Decoded data
|
||||||
|
* @throws Exception If decoding fails
|
||||||
|
*/
|
||||||
|
public static function decode(string $json, bool $associative = false, int $depth = 512, int $options = 0)
|
||||||
|
{
|
||||||
|
$options = $options | self::DEFAULT_DECODE_OPTIONS; // Ensure default options are applied
|
||||||
|
try {
|
||||||
|
return json_decode($json, $associative, $depth, $options);
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new Exception('JSON decoding failed: ' . $e->getMessage(), $e->getCode(), $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is valid JSON.
|
||||||
|
*
|
||||||
|
* @param string $json String to validate
|
||||||
|
*
|
||||||
|
* @return bool True if valid JSON, false otherwise
|
||||||
|
*/
|
||||||
|
public static function isValid(string $json): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
json_decode($json, false, 512, JSON_THROW_ON_ERROR);
|
||||||
|
return true;
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last JSON error message.
|
||||||
|
*
|
||||||
|
* @return string Error message or empty string if no error
|
||||||
|
*/
|
||||||
|
public static function getLastError(): string
|
||||||
|
{
|
||||||
|
$error = json_last_error();
|
||||||
|
if ($error === JSON_ERROR_NONE) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return json_last_error_msg();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty prints JSON data.
|
||||||
|
*
|
||||||
|
* @param mixed $data Data to encode
|
||||||
|
* @param int $additionalOptions Additional options to merge with pretty print
|
||||||
|
*
|
||||||
|
* @return string Pretty formatted JSON string
|
||||||
|
* @throws Exception If encoding fails
|
||||||
|
*/
|
||||||
|
public static function prettyPrint($data, int $additionalOptions = 0): string
|
||||||
|
{
|
||||||
|
$options = self::DEFAULT_ENCODE_OPTIONS | JSON_PRETTY_PRINT | $additionalOptions;
|
||||||
|
return self::encode($data, $options);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace tests;
|
||||||
|
|
||||||
|
use flight\util\Json;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class JsonTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
// Clear any previous JSON errors
|
||||||
|
json_encode(['clear' => 'error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test basic encoding
|
||||||
|
public function testEncode(): void
|
||||||
|
{
|
||||||
|
$data = ['name' => 'John', 'age' => 30];
|
||||||
|
$result = Json::encode($data);
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertJson($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding with custom options
|
||||||
|
public function testEncodeWithOptions(): void
|
||||||
|
{
|
||||||
|
$data = ['url' => 'https://example.com/path'];
|
||||||
|
$result = Json::encode($data, JSON_UNESCAPED_SLASHES);
|
||||||
|
$this->assertStringContainsString('https://example.com/path', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding with invalid data
|
||||||
|
public function testEncodeInvalidData(): void
|
||||||
|
{
|
||||||
|
$this->expectException(Exception::class);
|
||||||
|
$this->expectExceptionMessage('JSON encoding failed');
|
||||||
|
|
||||||
|
// Create a resource that cannot be encoded
|
||||||
|
$resource = fopen('php://memory', 'r');
|
||||||
|
Json::encode($resource);
|
||||||
|
fclose($resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test basic decoding
|
||||||
|
public function testDecode(): void
|
||||||
|
{
|
||||||
|
$json = '{"name":"John","age":30}';
|
||||||
|
$result = Json::decode($json);
|
||||||
|
$this->assertIsObject($result);
|
||||||
|
$this->assertEquals('John', $result->name);
|
||||||
|
$this->assertEquals(30, $result->age);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decoding to associative array
|
||||||
|
public function testDecodeAssociative(): void
|
||||||
|
{
|
||||||
|
$json = '{"name":"John","age":30}';
|
||||||
|
$result = Json::decode($json, true);
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEquals('John', $result['name']);
|
||||||
|
$this->assertEquals(30, $result['age']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decoding with custom depth
|
||||||
|
public function testDecodeWithDepth(): void
|
||||||
|
{
|
||||||
|
$json = '{"level1":{"level2":{"level3":"value"}}}';
|
||||||
|
$result = Json::decode($json, true, 512);
|
||||||
|
$this->assertEquals('value', $result['level1']['level2']['level3']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decoding invalid JSON
|
||||||
|
public function testDecodeInvalidJson(): void
|
||||||
|
{
|
||||||
|
$this->expectException(Exception::class);
|
||||||
|
$this->expectExceptionMessage('JSON decoding failed');
|
||||||
|
|
||||||
|
Json::decode('{"invalid": json}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test JSON validation with valid JSON
|
||||||
|
public function testIsValidWithValidJson(): void
|
||||||
|
{
|
||||||
|
$validJson = '{"name":"John","age":30}';
|
||||||
|
$this->assertTrue(Json::isValid($validJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test JSON validation with invalid JSON
|
||||||
|
public function testIsValidWithInvalidJson(): void
|
||||||
|
{
|
||||||
|
$invalidJson = '{"invalid": json}';
|
||||||
|
$this->assertFalse(Json::isValid($invalidJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test JSON validation with empty string
|
||||||
|
public function testIsValidWithEmptyString(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse(Json::isValid(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test pretty print functionality
|
||||||
|
public function testPrettyPrint(): void
|
||||||
|
{
|
||||||
|
$data = ['name' => 'John', 'age' => 30];
|
||||||
|
$result = Json::prettyPrint($data);
|
||||||
|
$this->assertStringContainsString("\n", $result);
|
||||||
|
$this->assertStringContainsString(' ', $result); // Should contain indentation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test pretty print with additional options
|
||||||
|
public function testPrettyPrintWithAdditionalOptions(): void
|
||||||
|
{
|
||||||
|
$data = ['html' => '<script>alert("test")</script>'];
|
||||||
|
$result = Json::prettyPrint($data, JSON_HEX_TAG);
|
||||||
|
$this->assertStringContainsString('\u003C', $result); // Should escape < character
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test getLastError when no error
|
||||||
|
public function testGetLastErrorNoError(): void
|
||||||
|
{
|
||||||
|
// Perform a valid JSON operation first
|
||||||
|
Json::encode(['valid' => 'data']);
|
||||||
|
$this->assertEquals('', Json::getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test getLastError when there is an error
|
||||||
|
public function testGetLastErrorWithError(): void
|
||||||
|
{
|
||||||
|
// Trigger a JSON error by using json_decode directly with invalid JSON
|
||||||
|
// This bypasses our Json class exception handling to test getLastError()
|
||||||
|
json_decode('{"invalid": json}');
|
||||||
|
|
||||||
|
$errorMessage = Json::getLastError();
|
||||||
|
$this->assertNotEmpty($errorMessage);
|
||||||
|
$this->assertIsString($errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding arrays
|
||||||
|
public function testEncodeArray(): void
|
||||||
|
{
|
||||||
|
$data = [1, 2, 3, 'four'];
|
||||||
|
$result = Json::encode($data);
|
||||||
|
$this->assertEquals('[1,2,3,"four"]', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding null
|
||||||
|
public function testEncodeNull(): void
|
||||||
|
{
|
||||||
|
$result = Json::encode(null);
|
||||||
|
$this->assertEquals('null', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding boolean values
|
||||||
|
public function testEncodeBoolean(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('true', Json::encode(true));
|
||||||
|
$this->assertEquals('false', Json::encode(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding strings
|
||||||
|
public function testEncodeString(): void
|
||||||
|
{
|
||||||
|
$result = Json::encode('Hello World');
|
||||||
|
$this->assertEquals('"Hello World"', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encoding numbers
|
||||||
|
public function testEncodeNumbers(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('42', Json::encode(42));
|
||||||
|
$this->assertEquals('3.14', Json::encode(3.14));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decoding arrays
|
||||||
|
public function testDecodeArray(): void
|
||||||
|
{
|
||||||
|
$json = '[1,2,3,"four"]';
|
||||||
|
$result = Json::decode($json, true);
|
||||||
|
$this->assertEquals([1, 2, 3, 'four'], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decoding nested objects
|
||||||
|
public function testDecodeNestedObjects(): void
|
||||||
|
{
|
||||||
|
$json = '{"user":{"name":"John","profile":{"age":30}}}';
|
||||||
|
$result = Json::decode($json, true);
|
||||||
|
$this->assertEquals('John', $result['user']['name']);
|
||||||
|
$this->assertEquals(30, $result['user']['profile']['age']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default encoding options are applied
|
||||||
|
public function testDefaultEncodingOptions(): void
|
||||||
|
{
|
||||||
|
$data = ['url' => 'https://example.com/path'];
|
||||||
|
$result = Json::encode($data);
|
||||||
|
// Should not escape slashes due to JSON_UNESCAPED_SLASHES
|
||||||
|
$this->assertStringContainsString('https://example.com/path', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test round trip encoding/decoding
|
||||||
|
public function testRoundTrip(): void
|
||||||
|
{
|
||||||
|
$original = [
|
||||||
|
'string' => 'test',
|
||||||
|
'number' => 42,
|
||||||
|
'boolean' => true,
|
||||||
|
'null' => null,
|
||||||
|
'array' => [1, 2, 3],
|
||||||
|
'object' => ['nested' => 'value']
|
||||||
|
];
|
||||||
|
|
||||||
|
$encoded = Json::encode($original);
|
||||||
|
$decoded = Json::decode($encoded, true);
|
||||||
|
|
||||||
|
$this->assertEquals($original, $decoded);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue