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..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 @@ -76,37 +75,60 @@ public function getFile(string $key) * Handle files * @param array $files * @return array> - * @throws BaseException - * @throws ReflectionException */ public function handleFiles(array $files): array { - if (!count($files)) { - return []; - } + $formatted = []; - $key = key($files); + foreach ($files as $key => $file) { + if (!is_array($file) || !isset($file['name'])) { + continue; + } - if (!$key) { - return []; - } + if (!is_array($file['name'])) { + $formatted[$key] = new UploadedFile($file); + continue; + } + + if (!$this->isMultiFilePayload($file)) { + continue; + } - if (!is_array($files[$key]['name'])) { - return [$key => new UploadedFile($files[$key])]; - } else { - $formatted = []; + $types = $file['type']; + $tmpNames = $file['tmp_name']; + $errors = $file['error']; + $sizes = $file['size']; + $multiFiles = []; - foreach ($files[$key]['name'] as $index => $name) { - $formatted[$key][$index] = new UploadedFile([ + foreach ($file['name'] as $index => $name) { + if (!isset($types[$index], $tmpNames[$index], $errors[$index], $sizes[$index])) { + continue; + } + + $multiFiles[$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' => $types[$index], + 'tmp_name' => $tmpNames[$index], + 'error' => $errors[$index], + 'size' => $sizes[$index], ]); } - return $formatted; + $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']); } } 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')); + } }