From d7062fc880c29a322bbfb696d954367d501835b5 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Sun, 17 May 2026 20:25:49 +0400 Subject: [PATCH 1/2] [#526] Fix Request uploaded-file parsing for top-level multipart fields --- CHANGELOG.md | 2 ++ src/Http/Traits/Request/File.php | 30 ++++++++----------- .../Traits/Request/HttpRequestFileTest.php | 30 +++++++++++++++++++ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25efb03c..826fd47e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Upgrade guide: https://github.com/softberg/quantum-php-docs/blob/master/v3.0/upg - Updated CI pipeline to test against PHP 7.4, 8.0, and 8.1 - CI now fails on PHP warnings and deprecations for stricter quality control - Added `declare(strict_types=1)` to all Exception classes for improved type safety +- Static analysis baseline is now PHPStan level 7 - **BREAKING:** Refactored routing system internals: - Routes are now represented as first-class objects (`Route`, `RouteCollection`, `MatchedRoute`) @@ -84,6 +85,7 @@ Upgrade guide: https://github.com/softberg/quantum-php-docs/blob/master/v3.0/upg - Standardized `defineValidationRules(Request $request): void` across DemoWeb and DemoApi middleware templates - Fixed OpenAPI installer route generation to return `Response` objects via `response()->...` helpers and avoid undefined response-variable usage (#520) - Standardized `defineValidationRules(Request $request): void` in Toolkit middleware templates +- Fixed request uploaded-file parsing to preserve multiple top-level multipart file fields in both real and internal request flows (#526) ### Added - `AppContext` class representing the runtime identity of a single application execution diff --git a/src/Http/Traits/Request/File.php b/src/Http/Traits/Request/File.php index 74902205..f54277f6 100644 --- a/src/Http/Traits/Request/File.php +++ b/src/Http/Traits/Request/File.php @@ -76,8 +76,6 @@ public function getFile(string $key) * Handle files * @param array $files * @return array> - * @throws BaseException - * @throws ReflectionException */ public function handleFiles(array $files): array { @@ -85,28 +83,26 @@ public function handleFiles(array $files): array return []; } - $key = key($files); + $formatted = []; - if (!$key) { - return []; - } + foreach ($files as $key => $file) { + if (!is_array($file['name'])) { + $formatted[$key] = new UploadedFile($file); - if (!is_array($files[$key]['name'])) { - return [$key => new UploadedFile($files[$key])]; - } else { - $formatted = []; + continue; + } - foreach ($files[$key]['name'] as $index => $name) { + foreach ($file['name'] as $index => $name) { $formatted[$key][$index] = new UploadedFile([ 'name' => $name, - 'type' => $files[$key]['type'][$index], - 'tmp_name' => $files[$key]['tmp_name'][$index], - 'error' => $files[$key]['error'][$index], - 'size' => $files[$key]['size'][$index], + 'type' => $file['type'][$index], + 'tmp_name' => $file['tmp_name'][$index], + 'error' => $file['error'][$index], + 'size' => $file['size'][$index], ]); } - - return $formatted; } + + return $formatted; } } diff --git a/tests/Unit/Http/Traits/Request/HttpRequestFileTest.php b/tests/Unit/Http/Traits/Request/HttpRequestFileTest.php index b284c033..0799ef69 100644 --- a/tests/Unit/Http/Traits/Request/HttpRequestFileTest.php +++ b/tests/Unit/Http/Traits/Request/HttpRequestFileTest.php @@ -79,4 +79,34 @@ public function testGetMultipleFiles(): void $this->assertEquals('bar.png', $image[1]->getNameWithExtension()); } + + public function testCreateWithMultipleTopLevelFileFields(): void + { + $request = request(); + + $files = [ + 'avatar' => [ + 'size' => 500, + 'name' => 'avatar.jpg', + 'tmp_name' => '/tmp/php8fe2.tmp', + 'type' => 'image/jpg', + 'error' => 0, + ], + 'resume' => [ + 'size' => 300, + 'name' => 'resume.pdf', + 'tmp_name' => '/tmp/php8fe3.tmp', + 'type' => 'application/pdf', + 'error' => 0, + ], + ]; + + $request->create('POST', '/upload', [], [], $files); + + $this->assertTrue($request->hasFile('avatar')); + $this->assertTrue($request->hasFile('resume')); + + $this->assertInstanceOf(UploadedFile::class, $request->getFile('avatar')); + $this->assertInstanceOf(UploadedFile::class, $request->getFile('resume')); + } } From c45453370a4abd9ee3f0ba3c3c77627c94bc7bd4 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Sun, 17 May 2026 20:50:57 +0400 Subject: [PATCH 2/2] [#526] Simplify Request file normalization flow --- src/Http/Traits/Request/File.php | 46 +++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/Http/Traits/Request/File.php b/src/Http/Traits/Request/File.php index f54277f6..96085583 100644 --- a/src/Http/Traits/Request/File.php +++ b/src/Http/Traits/Request/File.php @@ -19,7 +19,6 @@ use Quantum\Storage\Exceptions\FileUploadException; use Quantum\App\Exceptions\BaseException; use Quantum\Storage\UploadedFile; -use ReflectionException; /** * Trait File @@ -79,30 +78,57 @@ public function getFile(string $key) */ public function handleFiles(array $files): array { - if (!count($files)) { - return []; - } - $formatted = []; foreach ($files as $key => $file) { + if (!is_array($file) || !isset($file['name'])) { + continue; + } + if (!is_array($file['name'])) { $formatted[$key] = new UploadedFile($file); + continue; + } + if (!$this->isMultiFilePayload($file)) { continue; } + $types = $file['type']; + $tmpNames = $file['tmp_name']; + $errors = $file['error']; + $sizes = $file['size']; + $multiFiles = []; + foreach ($file['name'] as $index => $name) { - $formatted[$key][$index] = new UploadedFile([ + if (!isset($types[$index], $tmpNames[$index], $errors[$index], $sizes[$index])) { + continue; + } + + $multiFiles[$index] = new UploadedFile([ 'name' => $name, - 'type' => $file['type'][$index], - 'tmp_name' => $file['tmp_name'][$index], - 'error' => $file['error'][$index], - 'size' => $file['size'][$index], + 'type' => $types[$index], + 'tmp_name' => $tmpNames[$index], + 'error' => $errors[$index], + 'size' => $sizes[$index], ]); } + + $formatted[$key] = $multiFiles; } return $formatted; } + + /** + * @param array $file + */ + private function isMultiFilePayload(array $file): bool + { + return isset($file['type'], $file['tmp_name'], $file['error'], $file['size']) + && is_array($file['type']) + && is_array($file['tmp_name']) + && is_array($file['error']) + && is_array($file['size']); + } }