File System

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".

HTML Form
<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.

Array Structure
$_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.

HTML
<form action="/" method="post" enctype="multipart/form-data">
    <input type="file" name="files[]" multiple />
    <button>Upload</button>
</form>
PHP Handler
<?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.

form.php
<!-- 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.

logic.php
<?php
move_uploaded_file(
    $_FILES['file']['tmp_name'], 
    DOCUMENT_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 src/Lib/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(DOCUMENT_PATH . '/uploads/');

Public Methods

  • upload(bool $renameDuplicates = true): void

    Uploads file(s) to the destination directory. Rename logic is optional.

  • update(array $file, string $oldFilename): bool

    Replaces an existing file with a new upload using the old filename.

  • setMaxSize(int $bytes): void

    Sets the maximum file size allowed for uploads (in bytes).

  • setPermittedTypes(array $mimeTypes): void

    Defines allowed MIME types (e.g. ['image/png', 'application/pdf']).

  • allowAllTypes(?string $suffix = null): void

    Disables type checking. Optionally appends a suffix to files for safety.

  • rename(string $oldName, string $newName): bool

    Renames a file in the destination directory.

  • delete(string $filename): bool

    Deletes a file from the destination directory.

  • getMessages(): array

    Returns an array of status messages about the operation.

  • getErrorCode(): array

    Returns an array of error codes generated during upload.

  • getSuccessfulUploads(): array

    Returns an array with 'original' and 'final' names of uploaded files.

  • static convertToBytes(string $val): int

    Helper to convert shorthand sizes (e.g. '2M') to integer bytes.

  • static convertFromBytes(int $bytes): string

    Helper 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.

src/app/file-manager/index.php
<?php

use Lib\Validator;
use Lib\StateManager;
use Lib\FileManager\UploadFile;

const UPLOADS = DOCUMENT_PATH . '/uploads/';

$maxSize = 50 * 1024; // 50KB
$uploadFile = new UploadFile(UPLOADS);
$uploadedFiles = scandir(UPLOADS);
$uploadMessage = StateManager::getState('uploadMessage', ['error' => [], 'message' => []]);
$serverMaxFiles = StateManager::getState('serverMaxFiles', ini_get('max_file_uploads'));
$serverPostMax = StateManager::getState('serverPostMax', UploadFile::convertToBytes(ini_get('post_max_size')));
$serverDisplayMax = StateManager::getState('serverDisplayMax', UploadFile::convertFromBytes($serverPostMax));

function uploadFile()
{
    global $state, $uploadFile, $maxSize;

    try {
        $uploadFile->setMaxSize($maxSize);
        $uploadFile->upload();
        StateManager::setState('uploadMessage', ['error' => $uploadFile->getErrorCode(), 'message' => $uploadFile->getMessages()]);
    } catch (Throwable $th) {
        StateManager::setState('uploadMessage', ['error' => [(current($uploadFile->getErrorCode()) !== 0)], 'message' => [$th->getMessage()]]);
    }
}

function deleteFile($data)
{
    global $uploadFile;

    try {
        $uploadFile->delete($data->name);
        StateManager::setState('uploadMessage', ['error' => [(current($uploadFile->getErrorCode()) !== 0)], 'message' => [current($uploadFile->getMessages())]]);
    } catch (Throwable $th) {
        return ['error' => true, 'message' => $th->getMessage()];
    }
}

function renameFile($data)
{
    global $uploadFile;

    try {
        if (($validatedValue = Validator::withRules($data->newName, 'required|min:3')) !== true) {
            return ['error' => true, 'message' => $validatedValue];
        }

        $uploadFile->rename($data->oldFileName, $data->newName);
        StateManager::setState('uploadMessage', ['error' => [(current($uploadFile->getErrorCode()) !== 0)], 'message' => [current($uploadFile->getMessages())]]);
    } catch (Throwable $th) {
        return ['error' => true, 'message' => $th->getMessage()];
    }
}
?>

<div class="w-full max-w-2xl mx-auto p-6 flex flex-col gap-6">
    
    <!-- Upload Form -->
    <div class="bg-card border border-border rounded-xl p-6 shadow-sm">
        <h1 class="text-2xl font-bold mb-4">Upload File</h1>
        <ul class="list-disc ml-5 mb-4 text-sm text-muted-foreground">
            <li>Simultaneous files: <?= $serverMaxFiles ?></li>
            <li>Max file size: <?= UploadFile::convertFromBytes($maxSize) ?></li>
            <li>Total limit: <?= $serverDisplayMax ?></li>
        </ul>

        <form class="flex flex-col gap-4" onsubmit="uploadFile" enctype="multipart/form-data" pp-suspense="{'disabled': true}">
            <input type="hidden" name="MAX_FILE_SIZE" value="<?= $maxSize ?>" />
            <input type="file" name="file[]" multiple class="block w-full text-sm border border-input rounded-lg cursor-pointer bg-background p-2" />
            <button class="bg-primary text-primary-foreground py-2 rounded-lg hover:bg-primary/90 transition-all" pp-suspense="Uploading...">
                Upload
            </button>
        </form>
    </div>

    <!-- Messages -->
    <?php if ($uploadMessage['message']) : ?>
        <div class="space-y-2" pp-display="10s">
            <?php foreach ($uploadMessage['message'] as $key => $message) : ?>
                <div class="p-3 rounded-lg text-sm <?= $uploadMessage['error'][$key] ? 'bg-destructive/10 text-destructive' : 'bg-green-500/10 text-green-500' ?>">
                    <?= $message ?>
                </div>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>

    <!-- File List -->
    <div class="bg-card border border-border rounded-xl p-6 shadow-sm">
        <h2 class="text-xl font-bold mb-4">Files</h2>
        <div class="grid gap-2 max-h-[300px] overflow-y-auto">
            <?php foreach ($uploadedFiles as $file) : ?>
                <?php if ($file === '.' || $file === '..') continue; ?>
                <div class="flex justify-between items-center p-3 bg-muted/30 rounded-lg border border-border/50">
                    <span class="text-sm font-mono truncate"><?= $file ?></span>
                    <div class="flex gap-2">
                        <button onclick="renameClickHandler({'name': '<?= $file ?>'})" class="text-muted-foreground hover:text-chart-3">Rename</button>
                        <button onclick="deleteClickHandler({'name': '<?= $file ?>'})" class="text-muted-foreground hover:text-destructive">Delete</button>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
    </div>

</div>

<!-- Dialogs (Delete & Rename) -->
<dialog id="deleteModal" class="p-6 bg-card border border-border rounded-xl shadow-2xl backdrop:bg-black/50">
    <h3 class="text-lg font-bold mb-4">Confirm Deletion</h3>
    <form onsubmit="deleteFile" class="flex gap-3">
        <input type="hidden" id="deleteFileName" name="name" />
        <button class="bg-destructive text-destructive-foreground px-4 py-2 rounded-lg">Delete</button>
        <button type="button" onclick="this.closest('dialog').close()" class="bg-muted px-4 py-2 rounded-lg">Cancel</button>
    </form>
</dialog>

<dialog id="renameModal" class="p-6 bg-card border border-border rounded-xl shadow-2xl backdrop:bg-black/50 w-full max-w-sm">
    <h3 class="text-lg font-bold mb-4">Rename File</h3>
    <form id="renameForm" onsubmit="renameFile" pp-after-request="renameFileValidation" class="flex flex-col gap-4">
        <input type="hidden" id="oldFileName" name="oldFileName" />
        <input id="newName" name="newName" class="border border-input bg-background rounded-lg p-2" placeholder="New filename" required />
        <div class="flex gap-3 justify-end">
            <button type="button" onclick="this.closest('dialog').close()" class="bg-muted px-4 py-2 rounded-lg">Cancel</button>
            <button class="bg-primary text-primary-foreground px-4 py-2 rounded-lg">Rename</button>
        </div>
        <span id="renameMessage" class="text-destructive text-sm text-center"></span>
    </form>
</dialog>

<script>
    function deleteClickHandler(data) {
        document.getElementById('deleteFileName').value = data.name;
        document.getElementById('deleteModal').showModal();
    }

    function renameClickHandler(data) {
        document.getElementById('oldFileName').value = data.name;
        const fileName = data.name.split('.');
        document.getElementById('newName').value = fileName[0];
        document.getElementById('renameModal').showModal();
    }

    function renameFileValidation(data) {
        if (data.response?.error) {
            document.getElementById('renameMessage').innerText = data.response.message;
        } else {
            document.getElementById('renameMessage').innerText = '';
            document.getElementById('renameModal').close();
        }
    }
</script>