Skip to content

Commit 203be6a

Browse files
committed
Use Vapor style approach
1 parent 14d1881 commit 203be6a

3 files changed

Lines changed: 150 additions & 101 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ node_modules/
1111
package-lock.json
1212
/.claude
1313
/.playwright-mcp
14+
.phpunit.result.cache

src/Event/Http/Psr7Bridge.php

Lines changed: 3 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Bref\Event\Http;
44

55
use Bref\Context\Context;
6+
use Bref\Support\MultipartArray;
67
use Nyholm\Psr7\ServerRequest;
78
use Nyholm\Psr7\Stream;
89
use Nyholm\Psr7\UploadedFile;
@@ -13,20 +14,6 @@
1314

1415
use function str_starts_with;
1516

16-
// Polyfill for array_is_list (PHP 8.1+) to support PHP 8.0
17-
if (! function_exists('array_is_list')) {
18-
function array_is_list(array $array): bool
19-
{
20-
$i = 0;
21-
foreach ($array as $key => $value) {
22-
if ($key !== $i++) {
23-
return false;
24-
}
25-
}
26-
return true;
27-
}
28-
}
29-
3017
/**
3118
* Bridges PSR-7 requests and responses with API Gateway or ALB event/response formats.
3219
*/
@@ -138,102 +125,17 @@ private static function parseBodyAndUploadedFiles(HttpRequestEvent $event): arra
138125
}
139126
file_put_contents($tmpPath, $part->getBody());
140127
$file = new UploadedFile($tmpPath, filesize($tmpPath), UPLOAD_ERR_OK, $part->getFileName(), $part->getMimeType());
141-
self::parseKeyAndInsertValueInArray($files, $part->getName(), $file);
128+
$files = MultipartArray::setValue($files, $part->getName(), $file);
142129
} else {
143130
if ($parsedBody === null) {
144131
$parsedBody = [];
145132
}
146-
self::parseKeyAndInsertValueInArray($parsedBody, $part->getName(), $part->getBody());
133+
$parsedBody = MultipartArray::setValue($parsedBody, $part->getName(), $part->getBody());
147134
}
148135
}
149136
return [$files, $parsedBody];
150137
}
151138

152-
/**
153-
* Parse a string key like "files[id_cards][jpg][]" and do $array['files']['id_cards']['jpg'][] = $value
154-
*/
155-
private static function parseKeyAndInsertValueInArray(array &$array, string $key, mixed $value): void
156-
{
157-
$parsed = [];
158-
// We use parse_str to parse the key in the same way PHP does natively
159-
// We use "=mock" because the value can be an object (in case of uploaded files)
160-
parse_str(urlencode($key) . '=mock', $parsed);
161-
// Replace `mock` with the actual value
162-
array_walk_recursive($parsed, fn (&$v) => $v = $value);
163-
164-
// Use a custom merge that handles both structured arrays and regular arrays
165-
$array = self::mergeRecursivePreserveNumeric($array, $parsed);
166-
}
167-
168-
private static function mergeRecursivePreserveNumeric(array $a, array $b): array
169-
{
170-
foreach ($b as $key => $bVal) {
171-
if (! array_key_exists($key, $a)) {
172-
$a[$key] = $bVal;
173-
continue;
174-
}
175-
176-
$aVal = $a[$key];
177-
178-
if (is_array($aVal) && is_array($bVal)) {
179-
$aIsList = array_is_list($aVal);
180-
$bIsList = array_is_list($bVal);
181-
182-
if ($aIsList && $bIsList) {
183-
// Determine whether list items are arrays (objects) -> merge-by-index
184-
$mergeByIndex = false;
185-
foreach ($aVal as $item) {
186-
if (is_array($item)) {
187-
$mergeByIndex = true;
188-
break;
189-
}
190-
}
191-
if (! $mergeByIndex) {
192-
foreach ($bVal as $item) {
193-
if (is_array($item)) {
194-
$mergeByIndex = true;
195-
break;
196-
}
197-
}
198-
}
199-
200-
if ($mergeByIndex) {
201-
$max = max(count($aVal), count($bVal));
202-
$merged = [];
203-
for ($i = 0; $i < $max; $i++) {
204-
$hasA = array_key_exists($i, $aVal);
205-
$hasB = array_key_exists($i, $bVal);
206-
if ($hasA && $hasB) {
207-
if (is_array($aVal[$i]) && is_array($bVal[$i])) {
208-
$merged[$i] = self::mergeRecursivePreserveNumeric($aVal[$i], $bVal[$i]);
209-
} else {
210-
// if one is scalar, b wins
211-
$merged[$i] = $bVal[$i];
212-
}
213-
} elseif ($hasA) {
214-
$merged[$i] = $aVal[$i];
215-
} else {
216-
$merged[$i] = $bVal[$i];
217-
}
218-
}
219-
$a[$key] = $merged;
220-
} else {
221-
// both lists of scalars -> append
222-
$a[$key] = array_merge($aVal, $bVal);
223-
}
224-
} else {
225-
// At least one side is associative -> merge recursively by key
226-
$a[$key] = self::mergeRecursivePreserveNumeric($aVal, $bVal);
227-
}
228-
} else {
229-
// Non-array or conflicting types -> b wins
230-
$a[$key] = $bVal;
231-
}
232-
}
233-
234-
return $a;
235-
}
236-
237139
/**
238140
* Cleanup previously uploaded files.
239141
*/

src/Support/MultipartArray.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bref\Support;
4+
5+
/**
6+
* Build nested arrays from multipart form field names with bracket notation.
7+
*
8+
* Adapted from Laravel Vapor's {@see https://github.com/laravel/vapor-core/blob/2.0/src/Arr.php Arr::setMultipartArrayValue}.
9+
*/
10+
final class MultipartArray
11+
{
12+
/**
13+
* @param array<string, mixed> $array
14+
* @return array<string, mixed>
15+
*/
16+
public static function setValue(array $array, string $name, mixed $value): array
17+
{
18+
if (! str_contains($name, '[')) {
19+
if (array_key_exists($name, $array) && ! is_array($array[$name])) {
20+
$array[$name] = [$array[$name], $value];
21+
} else {
22+
$array[$name] = $value;
23+
}
24+
25+
return $array;
26+
}
27+
28+
$existingValue = self::getValueAtPath($array, $name);
29+
if ($existingValue !== null && ! is_array($existingValue)) {
30+
return self::appendDuplicateFieldValue($array, $name, $value);
31+
}
32+
33+
return self::setMultipartArrayValue($array, $name, $value);
34+
}
35+
36+
/**
37+
* @param array<string, mixed> $array
38+
* @return array<string, mixed>
39+
*/
40+
private static function setMultipartArrayValue(array $array, string $name, mixed $value): array
41+
{
42+
$segments = explode('[', $name);
43+
44+
$pointer = &$array;
45+
46+
foreach ($segments as $key => $segment) {
47+
if ($key === 0) {
48+
$pointer = &$pointer[$segment];
49+
50+
continue;
51+
}
52+
53+
if (self::malformedMultipartSegment($segment)) {
54+
$array[$name] = $value;
55+
56+
return $array;
57+
}
58+
59+
$segment = substr($segment, 0, -1);
60+
61+
if ($segment === '') {
62+
$pointer = &$pointer[];
63+
} else {
64+
$pointer = &$pointer[$segment];
65+
}
66+
}
67+
68+
$pointer = $value;
69+
70+
return $array;
71+
}
72+
73+
private static function malformedMultipartSegment(string $segment): bool
74+
{
75+
return $segment === '' || substr($segment, -1) !== ']';
76+
}
77+
78+
/**
79+
* When the same field name is submitted more than once, append the new value
80+
* to the parent list instead of overwriting the existing entry.
81+
*
82+
* @param array<string, mixed> $array
83+
* @return array<string, mixed>
84+
*/
85+
private static function appendDuplicateFieldValue(array $array, string $name, mixed $value): array
86+
{
87+
$segments = explode('[', $name);
88+
$parent = &$array[$segments[0]];
89+
90+
for ($i = 1; $i < count($segments) - 1; $i++) {
91+
$segment = substr($segments[$i], 0, -1);
92+
$parent = &$parent[$segment];
93+
}
94+
95+
$parent[] = $value;
96+
97+
return $array;
98+
}
99+
100+
/**
101+
* @param array<string, mixed> $array
102+
*/
103+
private static function getValueAtPath(array $array, string $name): mixed
104+
{
105+
if (! str_contains($name, '[')) {
106+
return $array[$name] ?? null;
107+
}
108+
109+
$segments = explode('[', $name);
110+
$pointer = $array;
111+
112+
foreach ($segments as $key => $segment) {
113+
if ($key === 0) {
114+
if (! is_array($pointer) || ! array_key_exists($segment, $pointer)) {
115+
return null;
116+
}
117+
$pointer = $pointer[$segment];
118+
119+
continue;
120+
}
121+
122+
if (self::malformedMultipartSegment($segment)) {
123+
return null;
124+
}
125+
126+
$segment = substr($segment, 0, -1);
127+
128+
if ($segment === '') {
129+
if (! is_array($pointer)) {
130+
return null;
131+
}
132+
$pointer = end($pointer);
133+
if ($pointer === false) {
134+
return null;
135+
}
136+
} else {
137+
if (! is_array($pointer) || ! array_key_exists($segment, $pointer)) {
138+
return null;
139+
}
140+
$pointer = $pointer[$segment];
141+
}
142+
}
143+
144+
return $pointer;
145+
}
146+
}

0 commit comments

Comments
 (0)