1. php
  2. /web development
  3. /forms

PHP Form Handling

Introduction to PHP Form Handling

Forms are the primary method for collecting user input in web applications. PHP provides powerful built-in capabilities for processing form data securely and efficiently. This guide covers everything from basic form processing to advanced techniques for building robust, secure form handling systems.

Understanding proper form handling is crucial for building interactive web applications that collect and process user data safely.

Basic Form Processing

Simple Contact Form

<!-- contact.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Contact Form</title>
    <style>
        .form-group { margin-bottom: 15px; }
        .error { color: red; font-size: 0.9em; }
        .success { color: green; }
        input, textarea { width: 100%; padding: 8px; margin-top: 5px; }
        button { padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Contact Us</h1>
    
    <form action="process_contact.php" method="POST">
        <div class="form-group">
            <label for="name">Name:</label>
            <input type="text" id="name" name="name" required>
        </div>
        
        <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>
        </div>
        
        <div class="form-group">
            <label for="subject">Subject:</label>
            <input type="text" id="subject" name="subject" required>
        </div>
        
        <div class="form-group">
            <label for="message">Message:</label>
            <textarea id="message" name="message" rows="5" required></textarea>
        </div>
        
        <button type="submit">Send Message</button>
    </form>
</body>
</html>
<?php
// process_contact.php
session_start();

$errors = [];
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Get and sanitize input data
    $name = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $subject = trim($_POST['subject'] ?? '');
    $message = trim($_POST['message'] ?? '');
    
    // Validation
    if (empty($name)) {
        $errors['name'] = 'Name is required';
    } elseif (strlen($name) < 2) {
        $errors['name'] = 'Name must be at least 2 characters';
    }
    
    if (empty($email)) {
        $errors['email'] = 'Email is required';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = 'Please enter a valid email address';
    }
    
    if (empty($subject)) {
        $errors['subject'] = 'Subject is required';
    }
    
    if (empty($message)) {
        $errors['message'] = 'Message is required';
    } elseif (strlen($message) < 10) {
        $errors['message'] = 'Message must be at least 10 characters';
    }
    
    // If no errors, process the form
    if (empty($errors)) {
        // Here you would typically:
        // 1. Save to database
        // 2. Send email
        // 3. Log the contact request
        
        // For this example, we'll just simulate success
        $success = true;
        
        // In a real application, you might send an email:
        /*
        $to = '[email protected]';
        $email_subject = 'Contact Form: ' . $subject;
        $email_body = "Name: $name\nEmail: $email\n\nMessage:\n$message";
        $headers = "From: $email\r\nReply-To: $email\r\n";
        
        mail($to, $email_subject, $email_body, $headers);
        */
        
        // Clear form data after successful submission
        $_POST = [];
    }
}

function getValue($field) {
    return htmlspecialchars($_POST[$field] ?? '');
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>Contact Form</title>
    <style>
        .form-group { margin-bottom: 15px; }
        .error { color: red; font-size: 0.9em; }
        .success { color: green; padding: 10px; background: #e8f5e8; margin-bottom: 20px; }
        input, textarea { width: 100%; padding: 8px; margin-top: 5px; }
        button { padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer; }
        .has-error input, .has-error textarea { border: 2px solid red; }
    </style>
</head>
<body>
    <h1>Contact Us</h1>
    
    <?php if ($success): ?>
        <div class="success">
            Thank you for your message! We'll get back to you soon.
        </div>
    <?php endif; ?>
    
    <form method="POST">
        <div class="form-group <?php echo isset($errors['name']) ? 'has-error' : ''; ?>">
            <label for="name">Name:</label>
            <input type="text" id="name" name="name" value="<?php echo getValue('name'); ?>" required>
            <?php if (isset($errors['name'])): ?>
                <div class="error"><?php echo $errors['name']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group <?php echo isset($errors['email']) ? 'has-error' : ''; ?>">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" value="<?php echo getValue('email'); ?>" required>
            <?php if (isset($errors['email'])): ?>
                <div class="error"><?php echo $errors['email']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group <?php echo isset($errors['subject']) ? 'has-error' : ''; ?>">
            <label for="subject">Subject:</label>
            <input type="text" id="subject" name="subject" value="<?php echo getValue('subject'); ?>" required>
            <?php if (isset($errors['subject'])): ?>
                <div class="error"><?php echo $errors['subject']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group <?php echo isset($errors['message']) ? 'has-error' : ''; ?>">
            <label for="message">Message:</label>
            <textarea id="message" name="message" rows="5" required><?php echo getValue('message'); ?></textarea>
            <?php if (isset($errors['message'])): ?>
                <div class="error"><?php echo $errors['message']; ?></div>
            <?php endif; ?>
        </div>
        
        <button type="submit">Send Message</button>
    </form>
</body>
</html>

Advanced Form Validation Class

<?php
class FormValidator {
    private $data;
    private $errors = [];
    private $rules = [];
    
    public function __construct($data) {
        $this->data = $data;
    }
    
    public function addRule($field, $rule, $parameters = null, $message = null) {
        if (!isset($this->rules[$field])) {
            $this->rules[$field] = [];
        }
        
        $this->rules[$field][] = [
            'rule' => $rule,
            'parameters' => $parameters,
            'message' => $message
        ];
        
        return $this;
    }
    
    public function validate() {
        $this->errors = [];
        
        foreach ($this->rules as $field => $fieldRules) {
            $value = $this->data[$field] ?? null;
            
            foreach ($fieldRules as $ruleData) {
                if (!$this->applyRule($field, $value, $ruleData)) {
                    break; // Stop on first error for this field
                }
            }
        }
        
        return empty($this->errors);
    }
    
    private function applyRule($field, $value, $ruleData) {
        $rule = $ruleData['rule'];
        $parameters = $ruleData['parameters'];
        $customMessage = $ruleData['message'];
        
        switch ($rule) {
            case 'required':
                if (empty($value)) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . ' is required';
                    return false;
                }
                break;
                
            case 'email':
                if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
                    $this->errors[$field] = $customMessage ?: 'Please enter a valid email address';
                    return false;
                }
                break;
                
            case 'min_length':
                if (!empty($value) && strlen($value) < $parameters) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . " must be at least {$parameters} characters";
                    return false;
                }
                break;
                
            case 'max_length':
                if (!empty($value) && strlen($value) > $parameters) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . " must not exceed {$parameters} characters";
                    return false;
                }
                break;
                
            case 'numeric':
                if (!empty($value) && !is_numeric($value)) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . ' must be a number';
                    return false;
                }
                break;
                
            case 'integer':
                if (!empty($value) && !filter_var($value, FILTER_VALIDATE_INT)) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . ' must be an integer';
                    return false;
                }
                break;
                
            case 'min_value':
                if (!empty($value) && is_numeric($value) && $value < $parameters) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . " must be at least {$parameters}";
                    return false;
                }
                break;
                
            case 'max_value':
                if (!empty($value) && is_numeric($value) && $value > $parameters) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . " must not exceed {$parameters}";
                    return false;
                }
                break;
                
            case 'url':
                if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
                    $this->errors[$field] = $customMessage ?: 'Please enter a valid URL';
                    return false;
                }
                break;
                
            case 'phone':
                if (!empty($value) && !preg_match('/^[\+]?[1-9][\d]{0,15}$/', $value)) {
                    $this->errors[$field] = $customMessage ?: 'Please enter a valid phone number';
                    return false;
                }
                break;
                
            case 'regex':
                if (!empty($value) && !preg_match($parameters, $value)) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . ' format is invalid';
                    return false;
                }
                break;
                
            case 'in':
                if (!empty($value) && !in_array($value, $parameters)) {
                    $this->errors[$field] = $customMessage ?: 'Please select a valid option';
                    return false;
                }
                break;
                
            case 'confirmed':
                $confirmField = $field . '_confirmation';
                if ($value !== ($this->data[$confirmField] ?? null)) {
                    $this->errors[$field] = $customMessage ?: ucfirst($field) . ' confirmation does not match';
                    return false;
                }
                break;
        }
        
        return true;
    }
    
    public function getErrors() {
        return $this->errors;
    }
    
    public function getError($field) {
        return $this->errors[$field] ?? null;
    }
    
    public function hasError($field) {
        return isset($this->errors[$field]);
    }
}

// Usage example
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $validator = new FormValidator($_POST);
    
    $validator
        ->addRule('name', 'required')
        ->addRule('name', 'min_length', 2)
        ->addRule('name', 'max_length', 50)
        ->addRule('email', 'required')
        ->addRule('email', 'email')
        ->addRule('age', 'required')
        ->addRule('age', 'integer')
        ->addRule('age', 'min_value', 18, 'You must be at least 18 years old')
        ->addRule('website', 'url')
        ->addRule('phone', 'phone')
        ->addRule('country', 'required')
        ->addRule('country', 'in', ['US', 'CA', 'UK', 'AU'])
        ->addRule('password', 'required')
        ->addRule('password', 'min_length', 8)
        ->addRule('password', 'confirmed');
    
    if ($validator->validate()) {
        echo "Form is valid!";
        // Process the form...
    } else {
        $errors = $validator->getErrors();
        // Display errors...
    }
}
?>

Multi-Step Form Handling

<?php
session_start();

class MultiStepForm {
    private $steps = ['personal', 'contact', 'preferences', 'review'];
    private $currentStep;
    
    public function __construct() {
        $this->currentStep = $_SESSION['form_step'] ?? 'personal';
        
        if (!isset($_SESSION['form_data'])) {
            $_SESSION['form_data'] = [];
        }
    }
    
    public function getCurrentStep() {
        return $this->currentStep;
    }
    
    public function getStepNumber() {
        return array_search($this->currentStep, $this->steps) + 1;
    }
    
    public function getTotalSteps() {
        return count($this->steps);
    }
    
    public function isLastStep() {
        return $this->currentStep === end($this->steps);
    }
    
    public function processStep($postData) {
        $errors = [];
        
        switch ($this->currentStep) {
            case 'personal':
                $errors = $this->validatePersonalInfo($postData);
                break;
            case 'contact':
                $errors = $this->validateContactInfo($postData);
                break;
            case 'preferences':
                $errors = $this->validatePreferences($postData);
                break;
            case 'review':
                // Final submission
                return $this->submitForm();
        }
        
        if (empty($errors)) {
            // Save step data
            $_SESSION['form_data'][$this->currentStep] = $postData;
            
            // Move to next step
            $this->nextStep();
            
            return ['success' => true];
        }
        
        return ['success' => false, 'errors' => $errors];
    }
    
    private function validatePersonalInfo($data) {
        $validator = new FormValidator($data);
        
        $validator
            ->addRule('first_name', 'required')
            ->addRule('first_name', 'min_length', 2)
            ->addRule('last_name', 'required')
            ->addRule('last_name', 'min_length', 2)
            ->addRule('date_of_birth', 'required')
            ->addRule('gender', 'required')
            ->addRule('gender', 'in', ['male', 'female', 'other']);
        
        $validator->validate();
        return $validator->getErrors();
    }
    
    private function validateContactInfo($data) {
        $validator = new FormValidator($data);
        
        $validator
            ->addRule('email', 'required')
            ->addRule('email', 'email')
            ->addRule('phone', 'required')
            ->addRule('phone', 'phone')
            ->addRule('address', 'required')
            ->addRule('city', 'required')
            ->addRule('postal_code', 'required');
        
        $validator->validate();
        return $validator->getErrors();
    }
    
    private function validatePreferences($data) {
        $validator = new FormValidator($data);
        
        $validator
            ->addRule('newsletter', 'in', ['yes', 'no'])
            ->addRule('notifications', 'in', ['email', 'sms', 'both', 'none']);
        
        $validator->validate();
        return $validator->getErrors();
    }
    
    public function nextStep() {
        $currentIndex = array_search($this->currentStep, $this->steps);
        if ($currentIndex < count($this->steps) - 1) {
            $this->currentStep = $this->steps[$currentIndex + 1];
            $_SESSION['form_step'] = $this->currentStep;
        }
    }
    
    public function previousStep() {
        $currentIndex = array_search($this->currentStep, $this->steps);
        if ($currentIndex > 0) {
            $this->currentStep = $this->steps[$currentIndex - 1];
            $_SESSION['form_step'] = $this->currentStep;
        }
    }
    
    public function getStepData($step = null) {
        $step = $step ?: $this->currentStep;
        return $_SESSION['form_data'][$step] ?? [];
    }
    
    public function getAllData() {
        return $_SESSION['form_data'] ?? [];
    }
    
    private function submitForm() {
        $allData = $this->getAllData();
        
        // Here you would save to database, send emails, etc.
        // For this example, we'll just return success
        
        // Clear session data
        unset($_SESSION['form_data']);
        unset($_SESSION['form_step']);
        
        return ['success' => true, 'message' => 'Registration completed successfully!'];
    }
    
    public function reset() {
        unset($_SESSION['form_data']);
        unset($_SESSION['form_step']);
        $this->currentStep = 'personal';
    }
}

// Process form submission
$form = new MultiStepForm();
$errors = [];
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['previous'])) {
        $form->previousStep();
    } else {
        $result = $form->processStep($_POST);
        
        if ($result['success']) {
            if (isset($result['message'])) {
                $success = $result['message'];
            }
        } else {
            $errors = $result['errors'] ?? [];
        }
    }
}

$currentStep = $form->getCurrentStep();
$stepData = $form->getStepData();
?>

<!DOCTYPE html>
<html>
<head>
    <title>Multi-Step Registration Form</title>
    <style>
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .progress-bar { background: #f0f0f0; height: 20px; margin-bottom: 30px; }
        .progress { background: #007cba; height: 100%; transition: width 0.3s; }
        .form-group { margin-bottom: 15px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
        .form-group input, .form-group select, .form-group textarea { 
            width: 100%; padding: 8px; border: 1px solid #ddd; 
        }
        .error { color: red; font-size: 0.9em; }
        .success { color: green; padding: 15px; background: #e8f5e8; margin-bottom: 20px; }
        .buttons { text-align: center; margin-top: 20px; }
        .btn { padding: 10px 20px; margin: 0 5px; border: none; cursor: pointer; }
        .btn-primary { background: #007cba; color: white; }
        .btn-secondary { background: #6c757d; color: white; }
        .step-info { text-align: center; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Registration Form</h1>
        
        <?php if ($success): ?>
            <div class="success"><?php echo $success; ?></div>
            <p><a href="?reset=1">Start New Registration</a></p>
        <?php else: ?>
            
        <div class="step-info">
            Step <?php echo $form->getStepNumber(); ?> of <?php echo $form->getTotalSteps(); ?>
        </div>
        
        <div class="progress-bar">
            <div class="progress" style="width: <?php echo ($form->getStepNumber() / $form->getTotalSteps()) * 100; ?>%"></div>
        </div>
        
        <form method="POST">
            <?php if ($currentStep === 'personal'): ?>
                <h2>Personal Information</h2>
                
                <div class="form-group">
                    <label for="first_name">First Name:</label>
                    <input type="text" id="first_name" name="first_name" 
                           value="<?php echo htmlspecialchars($stepData['first_name'] ?? ''); ?>" required>
                    <?php if (isset($errors['first_name'])): ?>
                        <div class="error"><?php echo $errors['first_name']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="last_name">Last Name:</label>
                    <input type="text" id="last_name" name="last_name" 
                           value="<?php echo htmlspecialchars($stepData['last_name'] ?? ''); ?>" required>
                    <?php if (isset($errors['last_name'])): ?>
                        <div class="error"><?php echo $errors['last_name']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="date_of_birth">Date of Birth:</label>
                    <input type="date" id="date_of_birth" name="date_of_birth" 
                           value="<?php echo htmlspecialchars($stepData['date_of_birth'] ?? ''); ?>" required>
                    <?php if (isset($errors['date_of_birth'])): ?>
                        <div class="error"><?php echo $errors['date_of_birth']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="gender">Gender:</label>
                    <select id="gender" name="gender" required>
                        <option value="">Select Gender</option>
                        <option value="male" <?php echo ($stepData['gender'] ?? '') === 'male' ? 'selected' : ''; ?>>Male</option>
                        <option value="female" <?php echo ($stepData['gender'] ?? '') === 'female' ? 'selected' : ''; ?>>Female</option>
                        <option value="other" <?php echo ($stepData['gender'] ?? '') === 'other' ? 'selected' : ''; ?>>Other</option>
                    </select>
                    <?php if (isset($errors['gender'])): ?>
                        <div class="error"><?php echo $errors['gender']; ?></div>
                    <?php endif; ?>
                </div>
                
            <?php elseif ($currentStep === 'contact'): ?>
                <h2>Contact Information</h2>
                
                <div class="form-group">
                    <label for="email">Email:</label>
                    <input type="email" id="email" name="email" 
                           value="<?php echo htmlspecialchars($stepData['email'] ?? ''); ?>" required>
                    <?php if (isset($errors['email'])): ?>
                        <div class="error"><?php echo $errors['email']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="phone">Phone:</label>
                    <input type="tel" id="phone" name="phone" 
                           value="<?php echo htmlspecialchars($stepData['phone'] ?? ''); ?>" required>
                    <?php if (isset($errors['phone'])): ?>
                        <div class="error"><?php echo $errors['phone']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="address">Address:</label>
                    <textarea id="address" name="address" rows="3" required><?php echo htmlspecialchars($stepData['address'] ?? ''); ?></textarea>
                    <?php if (isset($errors['address'])): ?>
                        <div class="error"><?php echo $errors['address']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="city">City:</label>
                    <input type="text" id="city" name="city" 
                           value="<?php echo htmlspecialchars($stepData['city'] ?? ''); ?>" required>
                    <?php if (isset($errors['city'])): ?>
                        <div class="error"><?php echo $errors['city']; ?></div>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="postal_code">Postal Code:</label>
                    <input type="text" id="postal_code" name="postal_code" 
                           value="<?php echo htmlspecialchars($stepData['postal_code'] ?? ''); ?>" required>
                    <?php if (isset($errors['postal_code'])): ?>
                        <div class="error"><?php echo $errors['postal_code']; ?></div>
                    <?php endif; ?>
                </div>
                
            <?php elseif ($currentStep === 'preferences'): ?>
                <h2>Preferences</h2>
                
                <div class="form-group">
                    <label for="newsletter">Subscribe to Newsletter:</label>
                    <select id="newsletter" name="newsletter">
                        <option value="yes" <?php echo ($stepData['newsletter'] ?? '') === 'yes' ? 'selected' : ''; ?>>Yes</option>
                        <option value="no" <?php echo ($stepData['newsletter'] ?? '') === 'no' ? 'selected' : ''; ?>>No</option>
                    </select>
                </div>
                
                <div class="form-group">
                    <label for="notifications">Notification Preferences:</label>
                    <select id="notifications" name="notifications">
                        <option value="email" <?php echo ($stepData['notifications'] ?? '') === 'email' ? 'selected' : ''; ?>>Email Only</option>
                        <option value="sms" <?php echo ($stepData['notifications'] ?? '') === 'sms' ? 'selected' : ''; ?>>SMS Only</option>
                        <option value="both" <?php echo ($stepData['notifications'] ?? '') === 'both' ? 'selected' : ''; ?>>Both</option>
                        <option value="none" <?php echo ($stepData['notifications'] ?? '') === 'none' ? 'selected' : ''; ?>>None</option>
                    </select>
                </div>
                
            <?php elseif ($currentStep === 'review'): ?>
                <h2>Review Your Information</h2>
                
                <?php $allData = $form->getAllData(); ?>
                
                <h3>Personal Information</h3>
                <p><strong>Name:</strong> <?php echo htmlspecialchars($allData['personal']['first_name'] . ' ' . $allData['personal']['last_name']); ?></p>
                <p><strong>Date of Birth:</strong> <?php echo htmlspecialchars($allData['personal']['date_of_birth']); ?></p>
                <p><strong>Gender:</strong> <?php echo htmlspecialchars($allData['personal']['gender']); ?></p>
                
                <h3>Contact Information</h3>
                <p><strong>Email:</strong> <?php echo htmlspecialchars($allData['contact']['email']); ?></p>
                <p><strong>Phone:</strong> <?php echo htmlspecialchars($allData['contact']['phone']); ?></p>
                <p><strong>Address:</strong> <?php echo htmlspecialchars($allData['contact']['address']); ?></p>
                <p><strong>City:</strong> <?php echo htmlspecialchars($allData['contact']['city']); ?></p>
                <p><strong>Postal Code:</strong> <?php echo htmlspecialchars($allData['contact']['postal_code']); ?></p>
                
                <h3>Preferences</h3>
                <p><strong>Newsletter:</strong> <?php echo htmlspecialchars($allData['preferences']['newsletter']); ?></p>
                <p><strong>Notifications:</strong> <?php echo htmlspecialchars($allData['preferences']['notifications']); ?></p>
                
            <?php endif; ?>
            
            <div class="buttons">
                <?php if ($form->getStepNumber() > 1): ?>
                    <button type="submit" name="previous" class="btn btn-secondary">Previous</button>
                <?php endif; ?>
                
                <button type="submit" class="btn btn-primary">
                    <?php echo $form->isLastStep() ? 'Submit' : 'Next'; ?>
                </button>
            </div>
        </form>
        
        <?php endif; ?>
    </div>
</body>
</html>

<?php
// Handle reset
if (isset($_GET['reset'])) {
    $form->reset();
    header('Location: ' . $_SERVER['PHP_SELF']);
    exit;
}
?>

File Upload Form

<?php
class FileUploadHandler {
    private $uploadDir;
    private $allowedTypes;
    private $maxSize;
    private $errors = [];
    
    public function __construct($uploadDir = 'uploads/', $maxSize = 5242880) { // 5MB default
        $this->uploadDir = rtrim($uploadDir, '/') . '/';
        $this->maxSize = $maxSize;
        $this->allowedTypes = [
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'image/gif' => 'gif',
            'application/pdf' => 'pdf',
            'text/plain' => 'txt',
            'application/msword' => 'doc',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx'
        ];
        
        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
    }
    
    public function handleUpload($fileField) {
        $this->errors = [];
        
        if (!isset($_FILES[$fileField])) {
            $this->errors[] = 'No file uploaded';
            return false;
        }
        
        $file = $_FILES[$fileField];
        
        // Handle multiple files
        if (is_array($file['name'])) {
            return $this->handleMultipleUploads($file);
        }
        
        return $this->processSingleFile($file);
    }
    
    private function processSingleFile($file) {
        // Check for upload errors
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = $this->getUploadErrorMessage($file['error']);
            return false;
        }
        
        // Validate file size
        if ($file['size'] > $this->maxSize) {
            $this->errors[] = 'File size exceeds maximum allowed size (' . $this->formatBytes($this->maxSize) . ')';
            return false;
        }
        
        // Get real MIME type
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        // Validate file type
        if (!array_key_exists($mimeType, $this->allowedTypes)) {
            $this->errors[] = 'File type not allowed. Allowed types: ' . implode(', ', array_unique(array_values($this->allowedTypes)));
            return false;
        }
        
        // Generate unique filename
        $extension = $this->allowedTypes[$mimeType];
        $filename = uniqid() . '_' . time() . '.' . $extension;
        $destination = $this->uploadDir . $filename;
        
        // Additional validation for images
        if (strpos($mimeType, 'image/') === 0) {
            if (!$this->validateImage($file['tmp_name'])) {
                $this->errors[] = 'Invalid image file';
                return false;
            }
        }
        
        // Move uploaded file
        if (move_uploaded_file($file['tmp_name'], $destination)) {
            return [
                'filename' => $filename,
                'original_name' => $file['name'],
                'path' => $destination,
                'size' => $file['size'],
                'type' => $mimeType
            ];
        } else {
            $this->errors[] = 'Failed to move uploaded file';
            return false;
        }
    }
    
    private function handleMultipleUploads($files) {
        $results = [];
        $fileCount = count($files['name']);
        
        for ($i = 0; $i < $fileCount; $i++) {
            $file = [
                'name' => $files['name'][$i],
                'type' => $files['type'][$i],
                'tmp_name' => $files['tmp_name'][$i],
                'error' => $files['error'][$i],
                'size' => $files['size'][$i]
            ];
            
            $result = $this->processSingleFile($file);
            if ($result) {
                $results[] = $result;
            }
        }
        
        return $results;
    }
    
    private function validateImage($tempPath) {
        $imageInfo = getimagesize($tempPath);
        return $imageInfo !== false;
    }
    
    private function getUploadErrorMessage($errorCode) {
        switch ($errorCode) {
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                return 'File is too large';
            case UPLOAD_ERR_PARTIAL:
                return 'File upload was interrupted';
            case UPLOAD_ERR_NO_FILE:
                return 'No file was uploaded';
            case UPLOAD_ERR_NO_TMP_DIR:
                return 'Temporary directory is missing';
            case UPLOAD_ERR_CANT_WRITE:
                return 'Failed to write file to disk';
            case UPLOAD_ERR_EXTENSION:
                return 'File upload stopped by extension';
            default:
                return 'Unknown upload error';
        }
    }
    
    private function formatBytes($size) {
        $units = ['B', 'KB', 'MB', 'GB'];
        $unitIndex = 0;
        
        while ($size >= 1024 && $unitIndex < count($units) - 1) {
            $size /= 1024;
            $unitIndex++;
        }
        
        return round($size, 2) . ' ' . $units[$unitIndex];
    }
    
    public function getErrors() {
        return $this->errors;
    }
}

// Process file upload
$uploadHandler = new FileUploadHandler();
$uploadResult = null;
$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['files'])) {
    $uploadResult = $uploadHandler->handleUpload('files');
    
    if (!$uploadResult) {
        $errors = $uploadHandler->getErrors();
    }
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>File Upload Form</title>
    <style>
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        .error { color: red; margin-top: 10px; }
        .success { color: green; background: #e8f5e8; padding: 10px; margin-bottom: 20px; }
        .file-info { background: #f8f9fa; padding: 10px; margin: 5px 0; border-left: 4px solid #007cba; }
        .btn { padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer; }
        .upload-area { border: 2px dashed #ccc; padding: 20px; text-align: center; margin-bottom: 15px; }
        .upload-area.dragover { border-color: #007cba; background: #f0f8ff; }
    </style>
</head>
<body>
    <div class="container">
        <h1>File Upload</h1>
        
        <?php if ($uploadResult): ?>
            <div class="success">
                <h3>Upload Successful!</h3>
                <?php if (is_array($uploadResult) && isset($uploadResult[0])): ?>
                    <!-- Multiple files -->
                    <?php foreach ($uploadResult as $file): ?>
                        <div class="file-info">
                            <strong>File:</strong> <?php echo htmlspecialchars($file['original_name']); ?><br>
                            <strong>Size:</strong> <?php echo number_format($file['size']); ?> bytes<br>
                            <strong>Type:</strong> <?php echo htmlspecialchars($file['type']); ?><br>
                            <strong>Saved as:</strong> <?php echo htmlspecialchars($file['filename']); ?>
                        </div>
                    <?php endforeach; ?>
                <?php else: ?>
                    <!-- Single file -->
                    <div class="file-info">
                        <strong>File:</strong> <?php echo htmlspecialchars($uploadResult['original_name']); ?><br>
                        <strong>Size:</strong> <?php echo number_format($uploadResult['size']); ?> bytes<br>
                        <strong>Type:</strong> <?php echo htmlspecialchars($uploadResult['type']); ?><br>
                        <strong>Saved as:</strong> <?php echo htmlspecialchars($uploadResult['filename']); ?>
                    </div>
                <?php endif; ?>
            </div>
        <?php endif; ?>
        
        <?php if (!empty($errors)): ?>
            <div class="error">
                <h3>Upload Errors:</h3>
                <ul>
                    <?php foreach ($errors as $error): ?>
                        <li><?php echo htmlspecialchars($error); ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
        <?php endif; ?>
        
        <form method="POST" enctype="multipart/form-data">
            <div class="upload-area" id="uploadArea">
                <p>Drag and drop files here, or click to select files</p>
                <p><small>Max file size: 5MB. Allowed types: JPG, PNG, GIF, PDF, TXT, DOC, DOCX</small></p>
            </div>
            
            <div class="form-group">
                <label for="files">Select Files:</label>
                <input type="file" id="files" name="files[]" multiple 
                       accept=".jpg,.jpeg,.png,.gif,.pdf,.txt,.doc,.docx">
            </div>
            
            <button type="submit" class="btn">Upload Files</button>
        </form>
    </div>
    
    <script>
        // Drag and drop functionality
        const uploadArea = document.getElementById('uploadArea');
        const fileInput = document.getElementById('files');
        
        uploadArea.addEventListener('click', () => fileInput.click());
        
        uploadArea.addEventListener('dragover', (e) => {
            e.preventDefault();
            uploadArea.classList.add('dragover');
        });
        
        uploadArea.addEventListener('dragleave', () => {
            uploadArea.classList.remove('dragover');
        });
        
        uploadArea.addEventListener('drop', (e) => {
            e.preventDefault();
            uploadArea.classList.remove('dragover');
            
            const files = e.dataTransfer.files;
            fileInput.files = files;
            
            // Show selected files
            if (files.length > 0) {
                const fileNames = Array.from(files).map(f => f.name).join(', ');
                uploadArea.innerHTML = `<p>Selected files: ${fileNames}</p>`;
            }
        });
        
        fileInput.addEventListener('change', (e) => {
            const files = e.target.files;
            if (files.length > 0) {
                const fileNames = Array.from(files).map(f => f.name).join(', ');
                uploadArea.innerHTML = `<p>Selected files: ${fileNames}</p>`;
            }
        });
    </script>
</body>
</html>

For more PHP web development concepts:

Summary

Effective PHP form handling requires:

  • Proper Validation: Both client-side and server-side validation
  • Security Measures: Protection against XSS, CSRF, and injection attacks
  • User Experience: Clear error messages and preserved form data
  • File Handling: Secure upload processing with type and size validation
  • Multi-Step Forms: Session management for complex workflows

Key principles:

  • Always validate on the server side
  • Sanitize and escape output data
  • Use CSRF tokens for state-changing operations
  • Implement proper error handling and user feedback
  • Follow security best practices for file uploads

Modern form handling combines robust validation, security measures, and good user experience to create professional web applications that safely collect and process user data.