InitPHP\HTTP\Message\UploadedFile implements Psr\Http\Message\UploadedFileInterface. The two consumer use-cases are:
- Inside a controller — iterate
ServerRequest::getUploadedFiles()and move each upload to its final home. - In tests — build an
UploadedFiledirectly from aStreamto simulate an incoming file without touching$_FILES.
use InitPHP\HTTP\Message\UploadedFile;
use InitPHP\HTTP\Message\Stream;
$file = new UploadedFile(
new Stream('file contents', 'php://temp'),
13, // size in bytes — may be null when unknown
UPLOAD_ERR_OK, // one of the UPLOAD_ERR_* constants
'manifesto.txt', // client-supplied filename
'text/plain' // client-supplied media type
);You can also pass a file path string or an open resource as the first argument; UploadedFile wraps it into a Stream on first getStream() call.
$file->getSize(); // int|null
$file->getError(); // int — one of the UPLOAD_ERR_* constants
$file->getClientFilename(); // string|null
$file->getClientMediaType(); // string|null
$file->getStream(); // StreamInterface (throws if moved or upload errored)$file->moveTo('/var/www/uploads/manifesto.txt');moveTo() selects the right primitive based on the SAPI:
- CLI (tests, scripts) — uses
rename()(nois_uploaded_file()requirement). - Web SAPIs — uses
move_uploaded_file()to satisfy PHP's upload safety check. - Stream-backed UploadedFile (e.g. constructed from a
Streamin tests) — copies viaStream::read()/Stream::write()in 1 MiB chunks, looping until all bytes are flushed (partial writes are retried).
After a successful move, the upload is consumed:
$file->getStream(); // throws RuntimeException — "after it has already been moved"
$file->moveTo('/elsewhere'); // throws tooPHP's $_FILES represents nested form names like file[parent][child] as parallel arrays of tmp_name, size, error, name, type. ServerRequest::normalizeFiles() walks the tree recursively and produces a matching tree of UploadedFile values:
<form enctype="multipart/form-data" method="POST">
<input type="file" name="docs[brief]">
<input type="file" name="docs[exhibits][a]">
<input type="file" name="docs[exhibits][b]">
</form>$request = ServerRequest::createFromGlobals();
$tree = $request->getUploadedFiles();
$tree['docs']['brief'] instanceof UploadedFileInterface; // true
$tree['docs']['exhibits']['a'] instanceof UploadedFileInterface; // true
$tree['docs']['exhibits']['b'] instanceof UploadedFileInterface; // trueWalk the tree with a small recursive helper:
function walk(array $files, callable $sink): void {
foreach ($files as $entry) {
if ($entry instanceof \Psr\Http\Message\UploadedFileInterface) {
$sink($entry);
} elseif (is_array($entry)) {
walk($entry, $sink);
}
}
}
walk($tree, static fn ($file) => $file->moveTo('/uploads/' . $file->getClientFilename()));getError() returns one of PHP's UPLOAD_ERR_* constants. Anything other than UPLOAD_ERR_OK means the upload failed and getStream() / moveTo() will throw RuntimeException. Build a small mapping table for user-facing messages:
$errors = [
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize.',
UPLOAD_ERR_FORM_SIZE => 'File exceeds the form-level MAX_FILE_SIZE.',
UPLOAD_ERR_PARTIAL => 'Upload was interrupted.',
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Server is missing a temporary directory.',
UPLOAD_ERR_CANT_WRITE => 'Server could not write the file to disk.',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.',
];
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new \DomainException($errors[$file->getError()] ?? 'Unknown upload error.');
}