Merge pull request #657 from flightphp/json-util

added JSON util to use elsewhere
master v3.17.0
n0nag0n 1 day ago committed by GitHub
commit b331797ae3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -14,6 +14,7 @@ use flight\net\Request;
use flight\net\Response;
use flight\net\Router;
use flight\template\View;
use flight\util\Json;
use Throwable;
use flight\net\Route;
use Psr\Container\ContainerInterface;
@ -522,11 +523,11 @@ class Engine
// not doing much here, just setting the requestHandled flag to true
$this->requestHandled = true;
// Allow filters to run
// This prevents multiple after events from being registered
$this->after('start', function () use ($self) {
$self->stop();
});
// Allow filters to run
// This prevents multiple after events from being registered
$this->after('start', function () use ($self) {
$self->stop();
});
} else {
// deregister the request and response objects and re-register them with new instances
$this->unregister('request');
@ -665,7 +666,7 @@ class Engine
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre>%s</pre>
HTML,
HTML, // phpcs:ignore
$e->getMessage(),
$e->getCode(),
$e->getTraceAsString()
@ -906,9 +907,7 @@ class Engine
?string $charset = 'utf-8',
int $option = 0
): void {
// add some default flags
$option |= JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR;
$json = $encode ? json_encode($data, $option) : $data;
$json = $encode ? Json::encode($data, $option) : $data;
$this->response()
->status($code)
@ -966,7 +965,7 @@ class Engine
string $charset = 'utf-8',
int $option = 0
): void {
$json = $encode ? json_encode($data, $option) : $data;
$json = $encode ? Json::encode($data, $option) : $data;
$callback = $this->request()->query[$param];
$this->response()

@ -85,7 +85,7 @@ class AiGenerateInstructionsCommand extends Command
$detailsText
Current instructions:
$context
EOT;
EOT; // phpcs:ignore
// Read LLM creds
$creds = json_decode(file_get_contents($runwayCredsFile), true);

@ -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);
}
}

@ -387,8 +387,9 @@ class EngineTest extends TestCase
public function testJsonWithDuplicateDefaultFlags()
{
$engine = new Engine();
$flags = JSON_HEX_TAG | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
// utf8 emoji
$engine->json(['key1' => 'value1', 'key2' => 'value2', 'utf8_emoji' => '😀'], 201, true, '', JSON_HEX_TAG | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$engine->json(['key1' => 'value1', 'key2' => 'value2', 'utf8_emoji' => '😀'], 201, true, '', $flags);
$this->assertEquals('application/json', $engine->response()->headers()['Content-Type']);
$this->assertEquals(201, $engine->response()->status());
$this->assertEquals('{"key1":"value1","key2":"value2","utf8_emoji":"😀"}', $engine->response()->getBody());
@ -397,7 +398,7 @@ class EngineTest extends TestCase
public function testJsonThrowOnErrorByDefault()
{
$engine = new Engine();
$this->expectException(JsonException::class);
$this->expectException(Exception::class);
$this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded');
$engine->json(['key1' => 'value1', 'key2' => 'value2', 'utf8_emoji' => "\xB1\x31"]);
}

@ -399,7 +399,7 @@ class FlightTest extends TestCase
<div>Hi</div>
<input type="number" />
<input type="number" />
html;
html; // phpcs:ignore
$html = str_replace(["\n", "\r"], '', $html);

@ -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);
}
}

@ -180,7 +180,7 @@ class ViewTest extends TestCase
<div>Hi</div>
<input type="number" />
<input type="number" />
html;
html; // phpcs:ignore
// if windows replace \n with \r\n
$html = str_replace(["\n", "\r"], '', $html);
@ -202,7 +202,7 @@ class ViewTest extends TestCase
$html = <<<'html'
<div>qux</div>
<div>bar</div>
html;
html; // phpcs:ignore
$html = str_replace(["\n", "\r"], '', $html);
@ -217,12 +217,12 @@ class ViewTest extends TestCase
$html1 = <<<'html'
<div>Hi</div>
<div></div>
html;
html; // phpcs:ignore
$html2 = <<<'html'
<input type="number" />
<input type="text" />
html;
html; // phpcs:ignore
$html1 = str_replace(["\n", "\r"], '', $html1);
$html2 = str_replace(["\n", "\r"], '', $html2);

@ -115,7 +115,7 @@ PHP;
| /put | PUT | | No | - |
| /patch | PATCH | | No | Bad Middleware |
+---------+-----------+-------+----------+----------------+
output;
output; // phpcs:ignore
$this->assertStringContainsString(
$expected,
@ -138,7 +138,7 @@ PHP;
+---------+---------+-------+----------+------------+
| /post | POST | | No | Closure |
+---------+---------+-------+----------+------------+
output;
output; // phpcs:ignore
$this->assertStringContainsString(
$expected,

@ -190,7 +190,7 @@ Flight::map('error', function (Throwable $e) {
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre style="border: 2px solid red; padding: 21px; background: lightgray; font-weight: bold;">%s</pre>
HTML,
HTML, // phpcs:ignore
$e->getMessage(),
$e->getCode(),
str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString())

Loading…
Cancel
Save