Skip to content

Commit

Permalink
fix(php): JSON serialize empty arrays as empty object (#5986)
Browse files Browse the repository at this point in the history
Fix empty object as array JSON issue
  • Loading branch information
Swimburger authored Feb 13, 2025
1 parent f2227ad commit 64f11e4
Show file tree
Hide file tree
Showing 147 changed files with 3,669 additions and 1,260 deletions.
81 changes: 59 additions & 22 deletions generators/php/base/src/asIs/Client/RawClient.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class RawClient
*/
public function __construct(
public readonly ?array $options = null,
) {
)
{
$this->client = $this->options['client']
?? $this->createDefaultClient();
$this->headers = $this->options['headers'] ?? [];
Expand Down Expand Up @@ -67,8 +68,9 @@ private function createDefaultClient(): Client
*/
public function sendRequest(
BaseApiRequest $request,
?array $options = null,
): ResponseInterface {
?array $options = null,
): ResponseInterface
{
$opts = $options ?? [];
$httpRequest = $this->buildRequest($request, $opts);
return $this->client->send($httpRequest, $this->toGuzzleOptions($opts));
Expand All @@ -81,7 +83,8 @@ public function sendRequest(
* } $options
* @return array<string, mixed>
*/
private function toGuzzleOptions(array $options): array {
private function toGuzzleOptions(array $options): array
{
$guzzleOptions = [];
if (isset($options['maxRetries'])) {
$guzzleOptions['maxRetries'] = $options['maxRetries'];
Expand All @@ -103,8 +106,9 @@ private function toGuzzleOptions(array $options): array {
*/
private function buildRequest(
BaseApiRequest $request,
array $options
): Request {
array $options
): Request
{
$url = $this->buildUrl($request, $options);
$headers = $this->encodeHeaders($request, $options);
$body = $this->encodeRequestBody($request, $options);
Expand All @@ -125,8 +129,9 @@ private function buildRequest(
*/
private function encodeHeaders(
BaseApiRequest $request,
array $options,
): array {
array $options,
): array
{
return match (get_class($request)) {
JsonApiRequest::class => array_merge(
["Content-Type" => "application/json"],
Expand All @@ -152,8 +157,9 @@ private function encodeHeaders(
*/
private function encodeRequestBody(
BaseApiRequest $request,
array $options,
): ?StreamInterface {
array $options,
): ?StreamInterface
{
return match (get_class($request)) {
JsonApiRequest::class => $request->body === null ? null : Utils::streamFor(
json_encode(
Expand All @@ -178,17 +184,27 @@ private function encodeRequestBody(
private function buildJsonBody(
mixed $body,
array $options,
): mixed {
): mixed
{
$overrideProperties = $options['bodyProperties'] ?? [];
if (is_array($body) && (empty($body) || self::isSequential($body))) {
return array_merge($body, $overrideProperties);
}

if ($body instanceof JsonSerializable) {
$serialized = $body->jsonSerialize();
return is_array($serialized)
? array_merge($serialized, $overrideProperties)
: $serialized;
$result = $body->jsonSerialize();
} else {
$result = $body;
}
if (is_array($result)) {
$result = array_merge($result, $overrideProperties);
if (empty($result)) {
// force to be serialized as {} instead of []
return (object)($result);
}
}
return is_array($body)
? array_merge($body, $overrideProperties)
: $body;

return $result;
}

/**
Expand All @@ -200,8 +216,9 @@ private function buildJsonBody(
*/
private function buildUrl(
BaseApiRequest $request,
array $options,
): string {
array $options,
): string
{
$baseUrl = $request->baseUrl;
$trimmedBaseUrl = rtrim($baseUrl, '/');
$trimmedBasePath = ltrim($request->path, '/');
Expand All @@ -220,7 +237,8 @@ private function buildUrl(
* @param array<string, mixed> $query
* @return string
*/
private function encodeQuery(array $query): string {
private function encodeQuery(array $query): string
{
$parts = [];
foreach ($query as $key => $value) {
if (is_array($value)) {
Expand All @@ -234,7 +252,8 @@ private function encodeQuery(array $query): string {
return implode('&', $parts);
}

private function encodeQueryValue(mixed $value): string {
private function encodeQueryValue(mixed $value): string
{
if (is_string($value)) {
return urlencode($value);
}
Expand All @@ -247,4 +266,22 @@ private function encodeQueryValue(mixed $value): string {
// Unreachable, but included for a best effort.
return urlencode(strval(json_encode($value)));
}

/**
* Check if an array is sequential, not associative.
* @param mixed[] $arr
* @return bool
*/
private static function isSequential(array $arr): bool
{
if (empty($arr)) return false;
$length = count($arr);
$keys = array_keys($arr);
for ($i = 0; $i < $length; $i++) {
if ($keys[$i] !== $i) {
return false;
}
}
return true;
}
}
7 changes: 7 additions & 0 deletions generators/php/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
- version: 0.13.3
changelogEntry:
- type: fix
summary: >-
Fix issue where an empty request would be JSON serialized as an empty array instead of an empty object.
irVersion: 55

- version: 0.13.2
changelogEntry:
- type: fix
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
{
"version": "1.0.0",
"types": {},
"headers": [],
"headers": [
{
"name": {
"name": {
"originalName": "version",
"camelCase": {
"unsafeName": "version",
"safeName": "version"
},
"snakeCase": {
"unsafeName": "version",
"safeName": "version"
},
"screamingSnakeCase": {
"unsafeName": "VERSION",
"safeName": "VERSION"
},
"pascalCase": {
"unsafeName": "Version",
"safeName": "Version"
}
},
"wireValue": "X-API-Version"
},
"typeReference": {
"type": "literal",
"value": {
"type": "string",
"value": "1.0.0"
}
}
}
],
"endpoints": {
"endpoint_service.getWithBearerToken": {
"auth": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
"path": "/apiKey",
"pathParameters": {},
"queryParameters": {},
"headers": {},
"headers": {
"X-API-Version": "1.0.0"
},
"responseStatusCode": 200,
"responseBody": "string",
"responseBodyV3": {
Expand Down Expand Up @@ -76,5 +78,16 @@
"tokenName": "apiKey"
},
"snippetsConfiguration": {},
"globalHeaders": []
"globalHeaders": [
{
"key": "X-API-Version",
"type": {
"type": "literal",
"value": {
"type": "stringLiteral",
"value": "1.0.0"
}
}
}
]
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 64f11e4

Please sign in to comment.