Skip to content

Commit fd4c8b6

Browse files
dadajuiceclaude
andcommitted
feat(upload): add batch storeMany API with tests and docs
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0074f98 commit fd4c8b6

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

docs/uploads.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ $relativePath = $uploader->store($upload, 'avatars');
3838

3939
`store()` returns a path relative to the destination root (for example `avatars/a1b2c3...jpg`).
4040

41+
### Batch upload
42+
43+
`storeMany()` accepts a `list<FileUpload>` and returns paths in the same order.
44+
Processing stops on the first failure and throws the same `UploadException` as `store()`.
45+
46+
```php
47+
/** @var list<string> $paths */
48+
$paths = $uploader->storeMany(
49+
FileUpload::listFromPhpArray($_FILES['photos']),
50+
'photos',
51+
);
52+
```
53+
54+
An empty list returns an empty array immediately.
55+
4156
## Request normalization
4257

4358
`Request::fromGlobals()` normalizes `$_FILES` entries into `FileUpload` objects:

src/Zephyrus/Upload/Uploader.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,31 @@ public function __construct(
4646
}, $this->allowedMimeTypes), static fn (string $mimeType): bool => $mimeType !== ''));
4747
}
4848

49+
/**
50+
* Persists multiple uploads in order and returns their relative paths.
51+
*
52+
* Each file is validated and stored via `store()`, sharing the same optional
53+
* `$subDirectory`. Processing stops immediately on the first failure and the
54+
* same `UploadException` is re-thrown, leaving previously stored files in place.
55+
*
56+
* @param list<FileUpload> $files Uploads to persist.
57+
* @param string|null $subDirectory Optional sub-path applied to every file.
58+
*
59+
* @return list<string> Relative paths in the same order as `$files`.
60+
*
61+
* @throws UploadException On the first validation or move failure.
62+
*/
63+
public function storeMany(array $files, ?string $subDirectory = null): array
64+
{
65+
$paths = [];
66+
67+
foreach ($files as $file) {
68+
$paths[] = $this->store($file, $subDirectory);
69+
}
70+
71+
return $paths;
72+
}
73+
4974
/**
5075
* Persists the upload to disk and returns its path relative to `$destinationRoot`.
5176
*

tests/Unit/Upload/UploaderTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,4 +385,60 @@ public function test_store_rejects_double_dot_as_target_name(): void
385385
$this->removeDir($dest);
386386
}
387387
}
388+
389+
// ------------------------------------------------------------------
390+
// storeMany
391+
// ------------------------------------------------------------------
392+
393+
public function test_store_many_returns_empty_array_for_empty_input(): void
394+
{
395+
$dest = $this->makeTempDir();
396+
397+
$paths = (new Uploader($dest))->storeMany([]);
398+
399+
self::assertSame([], $paths);
400+
401+
$this->removeDir($dest);
402+
}
403+
404+
public function test_store_many_returns_relative_paths_in_order(): void
405+
{
406+
$dest = $this->makeTempDir();
407+
$files = [
408+
$this->makeValidFile('a.txt'),
409+
$this->makeValidFile('b.txt'),
410+
$this->makeValidFile('c.txt'),
411+
];
412+
413+
$paths = (new Uploader($dest))->storeMany($files, 'batch');
414+
415+
self::assertCount(3, $paths);
416+
foreach ($paths as $path) {
417+
self::assertStringStartsWith('batch/', $path);
418+
self::assertFileExists($dest . '/' . $path);
419+
}
420+
self::assertCount(3, array_unique($paths), 'Each file should receive a unique name.');
421+
422+
$this->removeDir($dest);
423+
}
424+
425+
public function test_store_many_stops_on_first_invalid_file(): void
426+
{
427+
$dest = $this->makeTempDir();
428+
$files = [
429+
$this->makeValidFile('first.txt'),
430+
new FileUpload('bad.txt', '', '/tmp/bad', 0, UPLOAD_ERR_NO_FILE),
431+
$this->makeValidFile('third.txt'),
432+
];
433+
434+
try {
435+
$this->expectException(UploadException::class);
436+
(new Uploader($dest))->storeMany($files);
437+
} finally {
438+
// Only the first file should have been written.
439+
$stored = glob($dest . '/*');
440+
self::assertCount(1, $stored ?: []);
441+
$this->removeDir($dest);
442+
}
443+
}
388444
}

0 commit comments

Comments
 (0)