Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .php-cs-fixer.cache
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"php":"8.4.11","version":"3.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\/Event\/Http\/Psr7Bridge.php":"52cfdf8941ac1795e80ef89a9934ea28"}}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"phpstan/phpstan": "^1.10.26",
"phpunit/phpunit": "^9.6.10",
"symfony/console": "^4.4|^5.0|^6.0|^7.0",
"symfony/yaml": "^4.4|^5.0|^6.0|^7.0"
"symfony/yaml": "^4.4|^5.0|^6.0|^7.0",
"friendsofphp/php-cs-fixer": "^3.89"
Comment thread
mnapoli marked this conversation as resolved.
Outdated
},
"scripts": {
"test": [
Expand Down
75 changes: 73 additions & 2 deletions src/Event/Http/Psr7Bridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use RuntimeException;

use function str_starts_with;
use function array_is_list;

Check failure on line 15 in src/Event/Http/Psr7Bridge.php

View workflow job for this annotation

GitHub Actions / PHP CodeSniffer

Use statements should be sorted alphabetically. The first wrong one is array_is_list.

/**
* Bridges PSR-7 requests and responses with API Gateway or ALB event/response formats.
Expand Down Expand Up @@ -146,8 +147,78 @@
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;
}

/**
Expand Down
35 changes: 35 additions & 0 deletions tests/Event/Http/CommonHttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '<h1>Test content</h1>',
'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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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\n<h1>Test content</h1>\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
}
Original file line number Diff line number Diff line change
@@ -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\n<h1>Test content</h1>\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
}
Loading