diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php
index 1745fdd86..94ecc9784 100644
--- a/src/Event/Http/Psr7Bridge.php
+++ b/src/Event/Http/Psr7Bridge.php
@@ -13,6 +13,20 @@
use function str_starts_with;
+// Polyfill for array_is_list (PHP 8.1+) to support PHP 8.0
+if (! function_exists('array_is_list')) {
+ function array_is_list(array $array): bool
+ {
+ $i = 0;
+ foreach ($array as $key => $value) {
+ if ($key !== $i++) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
/**
* Bridges PSR-7 requests and responses with API Gateway or ALB event/response formats.
*/
@@ -146,8 +160,78 @@ private static function parseKeyAndInsertValueInArray(array &$array, string $key
parse_str(urlencode($key) . '=mock', $parsed);
// Replace `mock` with the actual value
array_walk_recursive($parsed, fn (&$v) => $v = $value);
- // Merge recursively into the main array to avoid overwriting existing values
- $array = array_merge_recursive($array, $parsed);
+
+ // Use a custom merge that handles both structured arrays and regular arrays
+ $array = self::mergeRecursivePreserveNumeric($array, $parsed);
+ }
+
+ private static function mergeRecursivePreserveNumeric(array $a, array $b): array
+ {
+ foreach ($b as $key => $bVal) {
+ if (! array_key_exists($key, $a)) {
+ $a[$key] = $bVal;
+ continue;
+ }
+
+ $aVal = $a[$key];
+
+ if (is_array($aVal) && is_array($bVal)) {
+ $aIsList = array_is_list($aVal);
+ $bIsList = array_is_list($bVal);
+
+ if ($aIsList && $bIsList) {
+ // Determine whether list items are arrays (objects) -> merge-by-index
+ $mergeByIndex = false;
+ foreach ($aVal as $item) {
+ if (is_array($item)) {
+ $mergeByIndex = true;
+ break;
+ }
+ }
+ if (! $mergeByIndex) {
+ foreach ($bVal as $item) {
+ if (is_array($item)) {
+ $mergeByIndex = true;
+ break;
+ }
+ }
+ }
+
+ if ($mergeByIndex) {
+ $max = max(count($aVal), count($bVal));
+ $merged = [];
+ for ($i = 0; $i < $max; $i++) {
+ $hasA = array_key_exists($i, $aVal);
+ $hasB = array_key_exists($i, $bVal);
+ if ($hasA && $hasB) {
+ if (is_array($aVal[$i]) && is_array($bVal[$i])) {
+ $merged[$i] = self::mergeRecursivePreserveNumeric($aVal[$i], $bVal[$i]);
+ } else {
+ // if one is scalar, b wins
+ $merged[$i] = $bVal[$i];
+ }
+ } elseif ($hasA) {
+ $merged[$i] = $aVal[$i];
+ } else {
+ $merged[$i] = $bVal[$i];
+ }
+ }
+ $a[$key] = $merged;
+ } else {
+ // both lists of scalars -> append
+ $a[$key] = array_merge($aVal, $bVal);
+ }
+ } else {
+ // At least one side is associative -> merge recursively by key
+ $a[$key] = self::mergeRecursivePreserveNumeric($aVal, $bVal);
+ }
+ } else {
+ // Non-array or conflicting types -> b wins
+ $a[$key] = $bVal;
+ }
+ }
+
+ return $a;
}
/**
diff --git a/tests/Event/Http/CommonHttpTest.php b/tests/Event/Http/CommonHttpTest.php
index 1c4805901..bb9f393d9 100644
--- a/tests/Event/Http/CommonHttpTest.php
+++ b/tests/Event/Http/CommonHttpTest.php
@@ -420,6 +420,41 @@ public function test POST request with multipart file uploads(int $version
);
}
+ /**
+ * @dataProvider provide API Gateway versions
+ */
+ public function test POST request with multipart form data containing structured arrays(int $version)
+ {
+ var_dump($version);
+ $this->fromFixture(__DIR__ . "/Fixture/ag-v$version-body-form-multipart-structured-arrays.json");
+
+ $this->assertContentType('multipart/form-data; boundary=testBoundary');
+ $this->assertMethod('POST');
+ $this->assertParsedBody([
+ 'content' => '
Test content
',
+ 'some_id' => '3034',
+ 'references' => [
+ [
+ 'other_id' => '4390954279',
+ 'url' => '',
+ ],
+ [
+ 'other_id' => '4313323164',
+ 'url' => '',
+ ],
+ [
+ 'other_id' => '',
+ 'url' => 'https://someurl.com/node/745911',
+ ],
+ ],
+ 'tags' => [
+ 'public health',
+ 'public finance',
+ ],
+ '_method' => 'PATCH',
+ ]);
+ }
+
/**
* @dataProvider provide API Gateway versions
*/
diff --git a/tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json b/tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json
new file mode 100644
index 000000000..60158768e
--- /dev/null
+++ b/tests/Event/Http/Fixture/ag-v1-body-form-multipart-structured-arrays.json
@@ -0,0 +1,53 @@
+{
+ "version": "1.0",
+ "resource": "/path",
+ "path": "/path",
+ "httpMethod": "POST",
+ "headers": {
+ "Accept": "*/*",
+ "Accept-Encoding": "gzip, deflate",
+ "Cache-Control": "no-cache",
+ "Content-Type": "multipart/form-data; boundary=testBoundary",
+ "Host": "example.org",
+ "User-Agent": "PostmanRuntime/7.20.1",
+ "X-Amzn-Trace-Id": "Root=1-ffffffff-ffffffffffffffffffffffff",
+ "X-Forwarded-For": "1.1.1.1",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "queryStringParameters": null,
+ "pathParameters": null,
+ "stageVariables": null,
+ "requestContext": {
+ "resourceId": "xxxxxx",
+ "resourcePath": "/path",
+ "httpMethod": "POST",
+ "extendedRequestId": "XXXXXX-xxxxxxxx=",
+ "requestTime": "24/Nov/2019:18:55:08 +0000",
+ "path": "/path",
+ "accountId": "123400000000",
+ "protocol": "HTTP/1.1",
+ "stage": "dev",
+ "domainPrefix": "dev",
+ "requestTimeEpoch": 1574621708700,
+ "requestId": "ffffffff-ffff-4fff-ffff-ffffffffffff",
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "sourceIp": "1.1.1.1",
+ "principalOrgId": null,
+ "accessKey": null,
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "PostmanRuntime/7.20.1",
+ "user": null
+ },
+ "domainName": "example.org",
+ "apiId": "xxxxxxxxxx"
+ },
+ "body": "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nTest content
\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n",
+ "isBase64Encoded": false
+}
diff --git a/tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json b/tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json
new file mode 100644
index 000000000..6adf2e606
--- /dev/null
+++ b/tests/Event/Http/Fixture/ag-v2-body-form-multipart-structured-arrays.json
@@ -0,0 +1,41 @@
+{
+ "version": "2.0",
+ "routeKey": "ANY /path",
+ "rawPath": "/path",
+ "rawQueryString": "",
+ "headers": {
+ "Accept": "*/*",
+ "Accept-Encoding": "gzip, deflate",
+ "Cache-Control": "no-cache",
+ "Content-Type": "multipart/form-data; boundary=testBoundary",
+ "Host": "example.org",
+ "User-Agent": "PostmanRuntime/7.20.1",
+ "X-Amzn-Trace-Id": "Root=1-ffffffff-ffffffffffffffffffffffff",
+ "X-Forwarded-For": "1.1.1.1",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "queryStringParameters": null,
+ "stageVariables": null,
+ "requestContext": {
+ "accountId": "123400000000",
+ "apiId": "xxxxxxxxxx",
+ "domainName": "example.org",
+ "domainPrefix": "0000000000",
+ "http": {
+ "method": "POST",
+ "path": "/path",
+ "protocol": "HTTP/1.1",
+ "sourceIp": "1.1.1.1",
+ "userAgent": "PostmanRuntime/7.20.1"
+ },
+ "requestId": "JTHoQgr2oAMEPMg=",
+ "routeId": "47matwk",
+ "routeKey": "ANY /path",
+ "stage": "$default",
+ "time": "24/Nov/2019:18:55:08 +0000",
+ "timeEpoch": 1574621708700
+ },
+ "body": "--testBoundary\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nTest content
\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"some_id\"\r\n\r\n3034\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][other_id]\"\r\n\r\n4390954279\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[0][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][other_id]\"\r\n\r\n4313323164\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[1][url]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][other_id]\"\r\n\r\n\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"references[2][url]\"\r\n\r\nhttps://someurl.com/node/745911\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[0]\"\r\n\r\npublic health\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"tags[1]\"\r\n\r\npublic finance\r\n--testBoundary\r\nContent-Disposition: form-data; name=\"_method\"\r\n\r\nPATCH\r\n--testBoundary--\r\n",
+ "isBase64Encoded": false
+}
diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php
index a6d0f921c..df8e37db3 100644
--- a/tests/Event/Http/Psr7BridgeTest.php
+++ b/tests/Event/Http/Psr7BridgeTest.php
@@ -32,6 +32,220 @@ public function test I can create a response from a PSR7 response()
], $response->toApiGatewayFormat());
}
+ public function test I can convert a request from an event with complex multipart form data structures()
+ {
+ $body = "--complexBoundary\r\nContent-Disposition: form-data; name=\"simple_string\"\r\n\r\nHello World\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_string\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_string\"\r\n\r\n12345\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"boolean_string\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[0]\"\r\n\r\nfirst_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[1]\"\r\n\r\nsecond_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"indexed_array[2]\"\r\n\r\nthird_item\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[name]\"\r\n\r\nJohn Doe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"associative_array[email]\"\r\n\r\njohn@example.com\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][first_name]\"\r\n\r\nJohn\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][last_name]\"\r\n\r\nDoe\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][profile][age]\"\r\n\r\n30\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][theme]\"\r\n\r\ndark\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[user][settings][notifications]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][name]\"\r\n\r\nAcme Corp\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"nested_objects[company][employees]\"\r\n\r\n150\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][id]\"\r\n\r\n1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][name]\"\r\n\r\nItem One\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][0]\"\r\n\r\ntag1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[0][tags][1]\"\r\n\r\ntag2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][id]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][name]\"\r\n\r\nItem Two\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][0]\"\r\n\r\ntag3\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"mixed_arrays[1][tags][1]\"\r\n\r\ntag4\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[0]\"\r\n\r\nfirst\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[2]\"\r\n\r\nthird\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"sparse_array[5]\"\r\n\r\nsixth\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[first_key]\"\r\n\r\nfirst_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"string_keys[second_key]\"\r\n\r\nsecond_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[0]\"\r\n\r\nzero_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[1]\"\r\n\r\none_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"numeric_keys[10]\"\r\n\r\nten_value\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[empty_string]\"\r\n\r\n\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[zero_string]\"\r\n\r\n0\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[false_string]\"\r\n\r\nfalse\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"empty_values[null_string]\"\r\n\r\nnull\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][name]\"\r\n\r\nDeep Item 1\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][0][value]\"\r\n\r\n100\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][name]\"\r\n\r\nDeep Item 2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][items][1][value]\"\r\n\r\n200\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][count]\"\r\n\r\n2\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"complex_nesting[level1][level2][level3][metadata][enabled]\"\r\n\r\ntrue\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nfirst_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nsecond_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"duplicate_keys[0]\"\r\n\r\nthird_duplicate\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with spaces]\"\r\n\r\nvalue with spaces\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with-dashes]\"\r\n\r\nvalue-with-dashes\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with_underscores]\"\r\n\r\nvalue_with_underscores\r\n--complexBoundary\r\nContent-Disposition: form-data; name=\"special_chars[with.dots]\"\r\n\r\nvalue.with.dots\r\n--complexBoundary--\r\n";
+
+ $datav1 = [
+ 'version' => '1.0',
+ 'resource' => '/path',
+ 'path' => '/path',
+ 'httpMethod' => 'POST',
+ 'headers' => [
+ 'Accept' => '*/*',
+ 'Accept-Encoding' => 'gzip, deflate',
+ 'Cache-Control' => 'no-cache',
+ 'Content-Type' => 'multipart/form-data; boundary=complexBoundary',
+ 'Host' => 'example.org',
+ 'User-Agent' => 'PostmanRuntime/7.20.1',
+ 'X-Amzn-Trace-Id' => 'Root=1-ffffffff-ffffffffffffffffffffffff',
+ 'X-Forwarded-For' => '1.1.1.1',
+ 'X-Forwarded-Port' => '443',
+ 'X-Forwarded-Proto' => 'https',
+ ],
+ 'queryStringParameters' => null,
+ 'pathParameters' => null,
+ 'stageVariables' => null,
+ 'requestContext' => [
+ 'resourceId' => 'xxxxxx',
+ 'resourcePath' => '/path',
+ 'httpMethod' => 'POST',
+ 'extendedRequestId' => 'XXXXXX-xxxxxxxx=',
+ 'requestTime' => '24/Nov/2019:18:55:08 +0000',
+ 'path' => '/path',
+ 'accountId' => '123400000000',
+ 'protocol' => 'HTTP/1.1',
+ 'stage' => 'dev',
+ 'domainPrefix' => 'dev',
+ 'requestTimeEpoch' => 1574621708700,
+ 'requestId' => 'ffffffff-ffff-4fff-ffff-ffffffffffff',
+ 'identity' => [
+ 'cognitoIdentityPoolId' => null,
+ 'accountId' => null,
+ 'cognitoIdentityId' => null,
+ 'caller' => null,
+ 'sourceIp' => '1.1.1.1',
+ 'principalOrgId' => null,
+ 'accessKey' => null,
+ 'cognitoAuthenticationType' => null,
+ 'cognitoAuthenticationProvider' => null,
+ 'userArn' => null,
+ 'userAgent' => 'PostmanRuntime/7.20.1',
+ 'user' => null,
+ ],
+ 'domainName' => 'example.org',
+ 'apiId' => 'xxxxxxxxxx',
+ ],
+ 'body' => $body,
+ 'isBase64Encoded' => false,
+ ];
+
+ $datav2 = [
+ 'version' => '2.0',
+ 'routeKey' => 'ANY /path',
+ 'rawPath' => '/path',
+ 'rawQueryString' => '',
+ 'headers' => [
+ 'Accept' => '*/*',
+ 'Accept-Encoding' => 'gzip, deflate',
+ 'Cache-Control' => 'no-cache',
+ 'Content-Type' => 'multipart/form-data; boundary=complexBoundary',
+ 'Host' => 'example.org',
+ 'User-Agent' => 'PostmanRuntime/7.20.1',
+ 'X-Amzn-Trace-Id' => 'Root=1-ffffffff-ffffffffffffffffffffffff',
+ 'X-Forwarded-For' => '1.1.1.1',
+ 'X-Forwarded-Port' => '443',
+ 'X-Forwarded-Proto' => 'https',
+ ],
+ 'queryStringParameters' => null,
+ 'stageVariables' => null,
+ 'requestContext' => [
+ 'accountId' => '123400000000',
+ 'apiId' => 'xxxxxxxxxx',
+ 'domainName' => 'example.org',
+ 'domainPrefix' => '0000000000',
+ 'http' => [
+ 'method' => 'POST',
+ 'path' => '/path',
+ 'protocol' => 'HTTP/1.1',
+ 'sourceIp' => '1.1.1.1',
+ 'userAgent' => 'PostmanRuntime/7.20.1',
+ ],
+ 'requestId' => 'JTHoQgr2oAMEPMg=',
+ 'routeId' => '47matwk',
+ 'routeKey' => 'ANY /path',
+ 'stage' => '$default',
+ 'time' => '24/Nov/2019:18:55:08 +0000',
+ 'timeEpoch' => 1574621708700,
+ ],
+ 'body' => $body,
+ 'isBase64Encoded' => false,
+ ];
+
+ $expectedBody = [
+ 'simple_string' => 'Hello World',
+ 'empty_string' => '',
+ 'numeric_string' => '12345',
+ 'boolean_string' => 'true',
+ 'indexed_array' => [
+ 'first_item',
+ 'second_item',
+ 'third_item',
+ ],
+ 'associative_array' => [
+ 'name' => 'John Doe',
+ 'age' => '30',
+ 'email' => 'john@example.com',
+ ],
+ 'nested_objects' => [
+ 'user' => [
+ 'profile' => [
+ 'first_name' => 'John',
+ 'last_name' => 'Doe',
+ 'age' => '30',
+ ],
+ 'settings' => [
+ 'theme' => 'dark',
+ 'notifications' => 'true',
+ ],
+ ],
+ 'company' => [
+ 'name' => 'Acme Corp',
+ 'employees' => '150',
+ ],
+ ],
+ 'mixed_arrays' => [
+ [
+ 'id' => '1',
+ 'name' => 'Item One',
+ 'tags' => [
+ 'tag1',
+ 'tag2',
+ ],
+ ],
+ [
+ 'id' => '2',
+ 'name' => 'Item Two',
+ 'tags' => [
+ 'tag3',
+ 'tag4',
+ ],
+ ],
+ ],
+ 'sparse_array' => [
+ 0 => 'first',
+ 2 => 'third',
+ 5 => 'sixth',
+ ],
+ 'string_keys' => [
+ 'first_key' => 'first_value',
+ 'second_key' => 'second_value',
+ ],
+ 'numeric_keys' => [
+ 0 => 'zero_value',
+ 1 => 'one_value',
+ 10 => 'ten_value',
+ ],
+ 'empty_values' => [
+ 'empty_string' => '',
+ 'zero_string' => '0',
+ 'false_string' => 'false',
+ 'null_string' => 'null',
+ ],
+ 'complex_nesting' => [
+ 'level1' => [
+ 'level2' => [
+ 'level3' => [
+ 'items' => [
+ [
+ 'name' => 'Deep Item 1',
+ 'value' => '100',
+ ],
+ [
+ 'name' => 'Deep Item 2',
+ 'value' => '200',
+ ],
+ ],
+ 'metadata' => [
+ 'count' => '2',
+ 'enabled' => 'true',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'duplicate_keys' => [
+ 'first_duplicate',
+ 'second_duplicate',
+ 'third_duplicate',
+ ],
+ 'special_chars' => [
+ 'with spaces' => 'value with spaces',
+ 'with-dashes' => 'value-with-dashes',
+ 'with_underscores' => 'value_with_underscores',
+ 'with.dots' => 'value.with.dots',
+ ],
+ ];
+
+ $eventv1 = new HttpRequestEvent($datav1);
+ $requestv1 = Psr7Bridge::convertRequest($eventv1, Context::fake());
+ $this->assertEquals($expectedBody, $requestv1->getParsedBody());
+
+ $eventv2 = new HttpRequestEvent($datav2);
+ $requestv2 = Psr7Bridge::convertRequest($eventv2, Context::fake());
+ $this->assertEquals($expectedBody, $requestv2->getParsedBody());
+ }
+
protected function fromFixture(string $file): void
{
$event = new HttpRequestEvent(json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR));