Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions flight/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -522,11 +523,11 @@ public function _start(): void
// 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');
Expand Down Expand Up @@ -665,7 +666,7 @@ public function _error(Throwable $e): void
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre>%s</pre>
HTML,
HTML, // phpcs:ignore
$e->getMessage(),
$e->getCode(),
$e->getTraceAsString()
Expand Down Expand Up @@ -906,9 +907,7 @@ public function _json(
?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)
Expand Down Expand Up @@ -966,7 +965,7 @@ public function _jsonp(
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()
Expand Down
2 changes: 1 addition & 1 deletion flight/commands/AiGenerateInstructionsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function execute()
$detailsText
Current instructions:
$context
EOT;
EOT; // phpcs:ignore

// Read LLM creds
$creds = json_decode(file_get_contents($runwayCredsFile), true);
Expand Down
114 changes: 114 additions & 0 deletions flight/util/Json.php
Original file line number Diff line number Diff line change
@@ -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);
Copy link

Copilot AI Jul 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prettyPrint method combines options using bitwise OR and then passes them to encode(), which will apply bitwise OR again with DEFAULT_ENCODE_OPTIONS. This could lead to unexpected behavior or option conflicts. Consider calling json_encode directly with the final options.

Suggested change
return self::encode($data, $options);
try {
return json_encode($data, $options, 512);
} catch (JsonException $e) {
throw new Exception('JSON pretty print encoding failed: ' . $e->getMessage(), $e->getCode(), $e);
}

Copilot uses AI. Check for mistakes.
}
}
5 changes: 3 additions & 2 deletions tests/EngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,9 @@ public function testJson(): void
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());
Expand All @@ -397,7 +398,7 @@ public function testJsonWithDuplicateDefaultFlags()
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"]);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/FlightTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ public function testKeepThePreviousStateOfOneViewComponentByDefault(): void
<div>Hi</div>
<input type="number" />
<input type="number" />
html;
html; // phpcs:ignore

$html = str_replace(["\n", "\r"], '', $html);

Expand Down
Loading