Skip to content
Open
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
39 changes: 34 additions & 5 deletions generators/php/base/src/asIs/Client/RawClient.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,17 +252,46 @@ private function buildUrl(
*/
private function encodeQuery(array $query): string
{
$flatParams = $this->flattenQueryParams($query);
$parts = [];
foreach ($query as $key => $value) {
if (is_array($value)) {
foreach ($flatParams as [$key, $value]) {
$parts[] = urlencode($key) . '=' . $this->encodeQueryValue($value);
}
return implode('&', $parts);
}

/**
* Recursively flattens nested arrays into query parameter format with bracket notation.
* For example: ['filter' => ['name' => 'john']] becomes [['filter[name]', 'john']]
*
* @param array<string, mixed> $data
* @param string|null $keyPrefix
* @return array<array{string, mixed}>
*/
private function flattenQueryParams(array $data, ?string $keyPrefix = null): array
{
$result = [];
foreach ($data as $key => $value) {
$fullKey = $keyPrefix !== null ? "{$keyPrefix}[{$key}]" : (string)$key;

if (is_array($value) && !empty($value) && !self::isSequential($value)) {
// Associative array (object-like) - recurse with bracket notation
$result = array_merge($result, $this->flattenQueryParams($value, $fullKey));
} elseif (is_array($value)) {
// Sequential array - each item gets the same key (exploded)
foreach ($value as $item) {
$parts[] = urlencode($key) . '=' . $this->encodeQueryValue($item);
if (is_array($item) && !empty($item) && !self::isSequential($item)) {
// Array of objects
$result = array_merge($result, $this->flattenQueryParams($item, $fullKey));
} else {
$result[] = [$fullKey, $item];
}
}
} else {
$parts[] = urlencode($key) . '=' . $this->encodeQueryValue($value);
$result[] = [$fullKey, $value];
}
}
return implode('&', $parts);
return $result;
}

private function encodeQueryValue(mixed $value): string
Expand Down
75 changes: 75 additions & 0 deletions generators/php/base/src/asIs/Client/RawClientTest.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -667,4 +667,79 @@ public function testMaxDelayCapIsApplied(): void
$this->assertGreaterThanOrEqual(60000, $delay);
$this->assertLessThanOrEqual(72000, $delay);
}

/**
* @throws ClientExceptionInterface
*/
public function testDeepObjectQueryParameters(): void
{
$this->mockHandler->append(new Response(200));

$request = new JsonApiRequest(
$this->baseUrl,
'/test',
HttpMethod::GET,
[],
['filter' => ['name' => 'john', 'age' => 30]]
);

$this->rawClient->sendRequest($request);

$lastRequest = $this->mockHandler->getLastRequest();
assert($lastRequest instanceof RequestInterface);
$this->assertEquals(
'https://api.example.com/test?filter%5Bname%5D=john&filter%5Bage%5D=30',
(string)$lastRequest->getUri()
);
}

/**
* @throws ClientExceptionInterface
*/
public function testDeeplyNestedObjectQueryParameters(): void
{
$this->mockHandler->append(new Response(200));

$request = new JsonApiRequest(
$this->baseUrl,
'/test',
HttpMethod::GET,
[],
['filter' => ['user' => ['name' => 'john', 'role' => 'admin'], 'status' => 'active']]
);

$this->rawClient->sendRequest($request);

$lastRequest = $this->mockHandler->getLastRequest();
assert($lastRequest instanceof RequestInterface);
$this->assertEquals(
'https://api.example.com/test?filter%5Buser%5D%5Bname%5D=john&filter%5Buser%5D%5Brole%5D=admin&filter%5Bstatus%5D=active',
(string)$lastRequest->getUri()
);
}

/**
* @throws ClientExceptionInterface
*/
public function testArrayOfObjectsQueryParameters(): void
{
$this->mockHandler->append(new Response(200));

$request = new JsonApiRequest(
$this->baseUrl,
'/test',
HttpMethod::GET,
[],
['objects' => [['key' => 'hello', 'value' => 'world'], ['key' => 'foo', 'value' => 'bar']]]
);

$this->rawClient->sendRequest($request);

$lastRequest = $this->mockHandler->getLastRequest();
assert($lastRequest instanceof RequestInterface);
$this->assertEquals(
'https://api.example.com/test?objects%5Bkey%5D=hello&objects%5Bvalue%5D=world&objects%5Bkey%5D=foo&objects%5Bvalue%5D=bar',
(string)$lastRequest->getUri()
);
}
}
8 changes: 8 additions & 0 deletions generators/php/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json

- version: 1.26.0
changelogEntry:
- summary: |
Add support for deep object query parameters. Nested objects in query parameters are now encoded using bracket notation (e.g., `filter[user][name]=john&filter[user][age]=30`). This matches the OpenAPI 3.0 deepObject serialization style and is consistent with Python, Java, and C# generators.
type: feat
createdAt: "2026-01-26"
irVersion: 62

- version: 1.25.4
changelogEntry:
- summary: |
Expand Down
Loading