File Manager
A powerful tool for uploading and managing files in PHP applications. Simplify storage, organize directories, and handle complex upload logic effortlessly within your codebase.
Basic File Upload
To upload files, create a form with enctype="multipart/form-data".
<form action="/" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<button>Upload</button>
</form>
Understanding $_FILES
The $_FILES array contains all information about the uploaded file.
$_FILES
Array
(
[file] => Array
(
[name] => test-upload.png
[full_path] => test-upload.png
[type] => image/png
[tmp_name] => C:\xampp\tmp\php1E10.tmp
[error] => 0
[size] => 107018
)
)
Handling Multiple Files
Add the multiple attribute to your input and append [] to the name.
<form action="/" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple />
<button>Upload</button>
</form>
<?php
if ($isPost) {
foreach ($_FILES['files']['name'] as $key => $name) {
if ($_FILES['files']['error'][$key] === 0) {
$tmpName = $_FILES['files']['tmp_name'][$key];
move_uploaded_file($tmpName, "uploads/$name");
}
}
}
?>
Common Error Codes
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Exceeds upload_max_filesize in php.ini. |
| 2 | Exceeds MAX_FILE_SIZE in HTML form. |
| 3 | File partially uploaded. |
| 4 | No file uploaded. |
| 6 | Missing temporary folder. |
| 7 | Failed to write to disk. |
Configuration
Setting Max File Size (HTML)
Use a hidden input before the file input.
<!-- 50 KB Limit -->
<input type="hidden" name="MAX_FILE_SIZE" value="51200" />
<input type="file" name="file" />
Moving Uploads (PHP)
Securely move files from temp storage.
<?php
move_uploaded_file(
$_FILES['file']['tmp_name'],
PUBLIC_PATH . "/uploads/" . $_FILES['file']['name']
);
?>
PHP.ini Settings
| Directive | Default | Role |
|---|---|---|
| file_uploads | On | Enables or disables file uploads. |
| upload_max_filesize | 2M | Max size of a single file. Main |
| post_max_size | 8M | Max size of entire POST data. Main |
| max_file_uploads | 20 | Max number of files per request. |
The UploadFile Class
Located at vendor/tsnc/src/FileManager/UploadFile.php, this class simplifies handling uploads.
Organization Tip
Store uploaded files outside src/app (logic only). Use /uploads in the root or src/uploads.
Key Features
- Built-in size validation
- Secure type restriction
- Filename cleanup & sanitization
- Handles multiple uploads automatically
Initialization
$upload = new UploadFile(PUBLIC_PATH . '/uploads/');
Public Methods
-
upload(bool $renameDuplicates = true): voidUploads file(s) to the destination directory. Rename logic is optional.
-
update(array $file, string $oldFilename): boolReplaces an existing file with a new upload using the old filename.
-
setMaxSize(int $bytes): voidSets the maximum file size allowed for uploads (in bytes).
-
setPermittedTypes(array $mimeTypes): voidDefines allowed MIME types (e.g.
['image/png', 'application/pdf']). -
allowAllTypes(?string $suffix = null): voidDisables type checking. Optionally appends a suffix to files for safety.
-
rename(string $oldName, string $newName): boolRenames a file in the destination directory.
-
delete(string $filename): boolDeletes a file from the destination directory.
-
getMessages(): arrayReturns an array of status messages about the operation.
-
getErrorCode(): arrayReturns an array of error codes generated during upload.
-
getSuccessfulUploads(): arrayReturns an array with 'original' and 'final' names of uploaded files.
-
static convertToBytes(string $val): intHelper to convert shorthand sizes (e.g. '2M') to integer bytes.
-
static convertFromBytes(int $bytes): stringHelper to convert integer bytes to a readable string (e.g. '2.5 MB').
Full File Manager App
A complete example including upload, listing, renaming, and deleting functionality.
<?php
use PP\Attributes\Exposed;
use PP\FileManager\UploadFile;
use PP\Rule;
use PP\Validator;
const UPLOADS = PUBLIC_PATH . '/uploads/';
// The constructor throws when the folder is missing, and a fresh app has none.
is_dir(UPLOADS) || mkdir(UPLOADS, 0775, true);
$maxSize = 50 * 1024; // 50 KB
function uploader(): UploadFile
{
$uploader = new UploadFile(UPLOADS);
$uploader->setMaxSize(50 * 1024);
return $uploader;
}
function currentFiles(): array
{
return array_values(array_diff(scandir(UPLOADS) ?: [], ['.', '..']));
}
#[Exposed]
function listFiles()
{
return ['files' => currentFiles()];
}
#[Exposed(limits: '20/min')]
function uploadFiles()
{
$uploader = uploader();
try {
$uploader->upload();
return [
'success' => current($uploader->getErrorCode()) === 0,
'messages' => $uploader->getMessages(),
'files' => currentFiles(),
];
} catch (Throwable $e) {
return ['success' => false, 'messages' => [$e->getMessage()], 'files' => currentFiles()];
}
}
#[Exposed]
function renameFile($data)
{
$newName = Validator::string($data->newName ?? '');
$result = Validator::withRules($newName, Rule::required()->min(3));
// Field-level feedback is data, not an exception.
if ($result !== true) {
return ['success' => false, 'errors' => ['newName' => $result]];
}
$uploader = uploader();
$uploader->rename(Validator::string($data->oldName ?? ''), $newName);
return [
'success' => current($uploader->getErrorCode()) === 0,
'errors' => [],
'messages' => $uploader->getMessages(),
'files' => currentFiles(),
];
}
#[Exposed]
function deleteFile($data)
{
$uploader = uploader();
$uploader->delete(Validator::string($data->name ?? ''));
return [
'success' => current($uploader->getErrorCode()) === 0,
'messages' => $uploader->getMessages(),
'files' => currentFiles(),
];
}
?>
<div class="w-full max-w-2xl mx-auto p-6 flex flex-col gap-6">
<div class="bg-card border border-border rounded-xl p-6">
<h1 class="text-2xl font-bold mb-4">Upload File</h1>
<input type="file" multiple pp-ref="picker" class="block w-full" />
<button onclick="upload()" disabled="{busy}">
{busy ? `Uploading ${progress}%` : 'Upload'}
</button>
<div hidden="{!busy}" class="mt-3 h-2 rounded-full bg-muted">
<div class="h-full rounded-full bg-primary" pp-style="width: {progress}%"></div>
</div>
</div>
<div hidden="{!messages.length}" class="space-y-2">
<template pp-for="(message, index) in messages">
<div key="{index}" class="{ok ? 'p-3 rounded-lg bg-green-500/10 text-green-500' : 'p-3 rounded-lg bg-destructive/10 text-destructive'}">
{message}
</div>
</template>
</div>
<div class="bg-card border border-border rounded-xl p-6">
<h2 class="text-xl font-bold mb-4">Files</h2>
<div class="grid gap-2 max-h-[300px] overflow-y-auto">
<template pp-for="name in files">
<div key="{name}" class="flex justify-between items-center p-3 rounded-lg bg-muted/30">
<span class="text-sm font-mono truncate">{name}</span>
<div class="flex gap-2">
<button onclick="startRename(name)">Rename</button>
<button onclick="remove(name)">Delete</button>
</div>
</div>
</template>
</div>
</div>
<div hidden="{!renaming}" class="bg-card border border-border rounded-xl p-6">
<h3 class="text-lg font-bold mb-4">Rename {renaming}</h3>
<input value="{newName}" oninput="setNewName(event.target.value)" placeholder="New filename" />
<p hidden="{!nameError}" class="text-sm text-destructive">{nameError}</p>
<div class="flex gap-3 justify-end mt-4">
<button onclick="setRenaming('')">Cancel</button>
<button onclick="confirmRename()">Rename</button>
</div>
</div>
<script>
const picker = pp.ref(null);
const [files, setFiles] = pp.state([]);
const [messages, setMessages] = pp.state([]);
const [ok, setOk] = pp.state(true);
const [busy, setBusy] = pp.state(false);
const [progress, setProgress] = pp.state(0);
const [renaming, setRenaming] = pp.state('');
const [newName, setNewName] = pp.state('');
const [nameError, setNameError] = pp.state('');
pp.effect(() => {
refresh();
}, []);
// Auto-dismiss the banner after 10s.
pp.effect(() => {
if (!messages.length) return;
const timer = setTimeout(() => setMessages([]), 10000);
return () => clearTimeout(timer);
}, [messages]);
function apply(response) {
setOk(!!response.success);
setMessages(response.messages ?? []);
if (response.files) setFiles(response.files);
}
async function refresh() {
const response = await pp.rpc('listFiles');
setFiles(response.files ?? []);
}
async function upload() {
const chosen = picker.current?.files;
if (!chosen?.length) return;
setBusy(true);
setProgress(0);
try {
// A top-level File/FileList switches pp.rpc to multipart,
// and onUploadProgress switches it to the XHR upload path.
const response = await pp.rpc('uploadFiles', { file: chosen }, {
onUploadProgress: ({ percent }) => {
if (percent != null) setProgress(Math.floor(percent));
},
});
apply(response);
} catch (e) {
setOk(false);
setMessages([e.message]);
} finally {
setBusy(false);
}
}
function startRename(name) {
setRenaming(name);
setNewName(name.split('.')[0]);
setNameError('');
}
async function confirmRename() {
const response = await pp.rpc('renameFile', { oldName: renaming, newName });
if (response.errors?.newName) return setNameError(response.errors.newName);
setRenaming('');
apply(response);
}
async function remove(name) {
apply(await pp.rpc('deleteFile', { name }));
}
</script>
</div>