Tracking file upload progress is crucial for providing a good user experience, especially when handling large files. PHP offers a built-in solution through its Session Upload Progress functionality, which provides reliable and efficient progress tracking without additional dependencies.

Server configuration requirements

Before implementing upload progress tracking, ensure your server is properly configured:

; php.ini configuration
session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
upload_max_filesize = 10M
post_max_size = 10M

For Nginx, disable request buffering to ensure accurate progress tracking:

location /upload {
    client_max_body_size 10M;
    client_body_buffer_size 128k;
    proxy_request_buffering off;
    # ... other configuration
}

Implementing the upload handler

Create a secure upload handler that processes files and implements proper validation:

<?php
// upload.php
declare(strict_types=1);

session_start();

class FileUploadHandler {
    private const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
    private const MAX_FILE_SIZE = 10485760; // 10MB
    private const UPLOAD_DIR = __DIR__ . '/uploads';

    public function __construct() {
        if (!is_dir(self::UPLOAD_DIR)) {
            mkdir(self::UPLOAD_DIR, 0755, true);
        }
    }

    public function handleUpload(): array {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            return ['error' => 'Invalid request method'];
        }

        if (!isset($_FILES['file'])) {
            return ['error' => 'No file uploaded'];
        }

        $file = $_FILES['file'];

        try {
            $this->validateUpload($file);
            $filename = $this->generateSafeFilename($file['name']);
            $destination = self::UPLOAD_DIR . '/' . $filename;

            if (!move_uploaded_file($file['tmp_name'], $destination)) {
                throw new RuntimeException('Failed to move uploaded file');
            }

            return [
                'success' => true,
                'filename' => $filename,
                'size' => $file['size']
            ];
        } catch (Exception $e) {
            return ['error' => $e->getMessage()];
        }
    }

    private function validateUpload(array $file): void {
        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new RuntimeException($this->getUploadErrorMessage($file['error']));
        }

        if ($file['size'] > self::MAX_FILE_SIZE) {
            throw new RuntimeException('File exceeds maximum size limit');
        }

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

        if (!in_array($mimeType, self::ALLOWED_TYPES, true)) {
            throw new RuntimeException('Invalid file type');
        }
    }

    private function generateSafeFilename(string $filename): string {
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
        return bin2hex(random_bytes(16)) . '.' . $extension;
    }

    private function getUploadErrorMessage(int $error): string {
        return match($error) {
            UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
            UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
            UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
            UPLOAD_ERR_NO_FILE => 'No file was uploaded',
            UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
            UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
            UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload',
            default => 'Unknown upload error'
        };
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $handler = new FileUploadHandler();
    header('Content-Type: application/json');
    echo json_encode($handler->handleUpload());
    exit;
}
?>

Progress tracking implementation

Create a progress tracking endpoint that safely retrieves upload progress:

<?php
// progress.php
declare(strict_types=1);

session_start();

header('Content-Type: application/json');

$key = ini_get('session.upload_progress.prefix') . $_GET['id'] ?? '';
$progress = [];

if (isset($_SESSION[$key])) {
    $current = $_SESSION[$key];
    $progress = [
        'lengthComputable' => true,
        'loaded' => $current['bytes_processed'],
        'total' => $current['content_length'],
        'percentage' => ($current['bytes_processed'] / $current['content_length']) * 100
    ];
}

echo json_encode($progress);

Client-side integration

Implement a responsive upload form with progress tracking:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>File Upload with Progress</title>
    <style>
      .progress {
        width: 100%;
        height: 20px;
        background: #f0f0f0;
        border-radius: 4px;
        overflow: hidden;
      }
      .progress-bar {
        width: 0;
        height: 100%;
        background: #4caf50;
        transition: width 0.3s ease;
      }
    </style>
  </head>
  <body>
    <form id="uploadForm" enctype="multipart/form-data">
      <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" id="progress-id" />
      <input type="file" name="file" required />
      <button type="submit">Upload</button>
      <div class="progress">
        <div class="progress-bar" id="progress-bar"></div>
      </div>
      <div id="status"></div>
    </form>

    <script>
      const form = document.getElementById('uploadForm')
      const progressBar = document.getElementById('progress-bar')
      const status = document.getElementById('status')
      const progressId = document.getElementById('progress-id')

      form.addEventListener('submit', async (e) => {
        e.preventDefault()

        // Generate unique ID for this upload
        const uploadId = Date.now().toString()
        progressId.value = uploadId

        const formData = new FormData(form)

        try {
          // Start progress tracking
          const tracker = trackProgress(uploadId)

          // Perform upload
          const response = await fetch('upload.php', {
            method: 'POST',
            body: formData,
          })

          // Stop progress tracking
          clearInterval(tracker)

          const result = await response.json()

          if (result.error) {
            throw new Error(result.error)
          }

          status.textContent = 'Upload complete!'
          progressBar.style.width = '100%'
        } catch (error) {
          status.textContent = `Error: ${error.message}`
          progressBar.style.backgroundColor = '#f44336'
        }
      })

      function trackProgress(uploadId) {
        return setInterval(async () => {
          try {
            const response = await fetch(`progress.php?id=${uploadId}`)
            const progress = await response.json()

            if (progress.lengthComputable) {
              const percentage = Math.round(progress.percentage)
              progressBar.style.width = `${percentage}%`
              status.textContent = `Uploading: ${percentage}%`
            }
          } catch (error) {
            console.error('Progress tracking error:', error)
          }
        }, 1000)
      }
    </script>
  </body>
</html>

Browser compatibility

This implementation works in all modern browsers that support:

  • FormData API
  • Fetch API
  • CSS transitions
  • ES6+ JavaScript features

For older browsers, consider adding polyfills or using a more traditional XMLHttpRequest approach.

Summary

PHP's Session Upload Progress provides a reliable way to track file uploads without external dependencies. By combining it with proper security measures and error handling, you can create a robust upload system that provides real-time feedback to users.

For more advanced file upload capabilities, including chunked uploads and resume functionality, consider exploring the tus protocol implementation for PHP.

Happy coding!