first php projects

Private File Upload Handler

Store Outside The Public Directory

Use a structure such as:

public/upload.php
storage/uploads/
src/DocumentUpload.php

The web server should not serve storage/uploads/ directly. A later download handler can check authorization before reading a stored file.

Validate The Temporary Upload

PHP reports an upload error code and temporary filename in $_FILES. Check the error first, then size, then detected content type:

PHP example
<?php

declare(strict_types=1);

function acceptedUpload(array $file): bool
{
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return false;
    }

    if (($file['size'] ?? 0) > 2_000_000) {
        return false;
    }

    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);

    return in_array($mime, ['application/pdf', 'image/png'], true);
}

Do not trust the original extension or browser-supplied MIME type. Generate a storage name with bin2hex(random_bytes(16)), keep the original filename only as display metadata, and move the file with move_uploaded_file().

Plan For Partial Failure

Storage and database writes are separate operations. If moving the file succeeds but saving metadata fails, remove the stored file or record cleanup work. If metadata saves first but moving fails, remove the incomplete record.

Verify The Rejected Paths

Check:

  1. A valid PDF is accepted.
  2. A file above the size limit is rejected.
  3. A renamed executable is rejected by detected MIME type.
  4. A guessed storage URL cannot fetch the private file.
  5. A failed metadata write leaves no abandoned file.

Practice

Practice: Build A Private Document Upload

Implement the private upload boundary from the lesson. Add a download handler that loads metadata by record ID and checks authorization before reading storage.

Requirements

  • Limit file size and accepted content types.
  • Generate a storage name in application code.
  • Store files outside the public web root.
  • Persist original name only as display metadata.
  • Do not trust extension or browser MIME type alone.
  • Protect against traversal and executable uploads.
  • Plan cleanup when storage succeeds but database write fails.
Show solution
PHP example
<?php

$storageName = bin2hex(random_bytes(16));
$destination = dirname(__DIR__) . '/storage/uploads/' . $storageName;

if (!move_uploaded_file($file['tmp_name'], $destination)) {
    throw new RuntimeException('Upload could not be stored.');
}

Persist $storageName, original display name, MIME type, size, and owner ID. The download route should receive a record ID, load metadata, check the current user's permission, then stream the controlled storage path. Never join request text directly onto the storage directory.