Lab: Implementing File Uploads
Resources
Section titled “Resources”Overview
Section titled “Overview”In this lab, you’ll implement secure file upload functionality for your Slim 4 application. You’ll build a reusable FileUploadHelper class, create an upload form, handle file validation, and display uploaded files.
Learning Objectives
Section titled “Learning Objectives”- Create a reusable FileUploadHelper class for file uploads
- Build an UploadController extending BaseController
- Handle uploaded files using Slim’s PSR-7 Request object
- Implement file validation (type, size, media type)
- Apply the Result pattern for success/failure communication
- Securely store files with unique filenames
- Apply security best practices for file uploads
Prerequisites
Section titled “Prerequisites”- Working Slim 4 application with BaseController pattern
- Flash Messages lab completed
- Result Pattern assignment completed
- SessionMiddleware registered
- Bootstrap CSS included in views
public/directory available
Step 1: Create the Uploads Directory
Section titled “Step 1: Create the Uploads Directory”Objective: Set up directory structure for storing uploaded files.
Instructions:
- Navigate to
public/directory - Create folder:
uploads/ - Inside
uploads/, create:images/ - Verify structure:
public/└── uploads/└── images/
Permissions:
- Windows: No action needed
- Linux/Mac:
chmod 755 public/uploads/
Step 2: Create the FileUploadHelper Class
Section titled “Step 2: Create the FileUploadHelper Class”Step 2.1: Class Structure
Section titled “Step 2.1: Class Structure”Create app/Helpers/FileUploadHelper.php:
<?php
namespace App\Helpers;
use Psr\Http\Message\UploadedFileInterface;
class FileUploadHelper{ /** * Upload a file with validation and return a Result. * * @param UploadedFileInterface $uploadedFile The uploaded file from the request * @param array $config Configuration options: * - 'directory' (string): Upload directory path (required) * - 'allowedTypes' (array): Array of allowed media types (required) * - 'maxSize' (int): Maximum file size in bytes (required) * - 'filenamePrefix' (string): Prefix for generated filenames (default: 'upload_') * @return Result Success with filename, or failure with error message */ public static function upload(UploadedFileInterface $uploadedFile, array $config): Result { // You'll implement the method body in the following steps }}Step 2.2: Extract and Validate Configuration
Section titled “Step 2.2: Extract and Validate Configuration”Inside the upload() method, add:
-
Extract configuration values using null coalescing operator:
$directoryfrom$config['directory'](default:null)$allowedTypesfrom$config['allowedTypes'](default:[])$maxSizefrom$config['maxSize'](default:0)$filenamePrefixfrom$config['filenamePrefix'](default:'upload_')
-
Validate required configuration:
- If
$directoryis empty, returnResult::failure('Upload directory not specified in configuration') - If
$allowedTypesis empty, returnResult::failure('Allowed file types not specified in configuration') - If
$maxSize <= 0, returnResult::failure('Maximum file size not specified in configuration')
- If
Hints:
- Use
$variable = $config['key'] ?? defaultValue; - Use
!$directoryto check if null/empty - Use
empty($allowedTypes)for array check
Step 2.3: Validate the Uploaded File
Section titled “Step 2.3: Validate the Uploaded File”Continue in upload() method:
-
Check upload errors:
- Use
$uploadedFile->getError() - Compare to
UPLOAD_ERR_OK(equals 0) - If not equal, return
Result::failure('Error uploading file')
- Use
-
Validate file size:
- Use
$uploadedFile->getSize() - Check if greater than
$maxSize - Calculate size in MB:
round($maxSize / (1024 * 1024), 1) - Return
Result::failure("File too large (max {$maxSizeMB}MB)")
- Use
-
Validate media type:
- Use
$uploadedFile->getClientMediaType() - Use
in_array()to check against$allowedTypes - If not allowed, return
Result::failure('Invalid file type. Only ' . implode(', ', $allowedTypes) . ' allowed.')
- Use
Hints:
getError()returns 0 for successgetSize()returns bytesgetClientMediaType()returns ‘image/jpeg’, ‘image/png’, etc.
Step 2.4: Generate a Safe Filename
Section titled “Step 2.4: Generate a Safe Filename”Why? User filenames could be malicious: ../../etc/passwd, script.php, or overwrite existing files.
Add code to generate safe filename:
-
Extract file extension:
- Use
pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION) - Store in
$extension
- Use
-
Generate unique filename:
- Use
uniqid($filenamePrefix) . '.' . $extension - Store in
$filename
- Use
Hints:
uniqid('prefix')generates unique ID based on current time- Example output:
upload_67a3f2b4c5e1d.jpg
Step 2.5: Create Directory and Save File
Section titled “Step 2.5: Create Directory and Save File”Add code to save the file:
-
Check/create directory:
- Use
is_dir($directory)to check existence - If doesn’t exist, use
mkdir($directory, 0755, true) - If mkdir fails, return
Result::failure('Failed to create upload directory')
- Use
-
Build destination path:
- Combine:
$directory . DIRECTORY_SEPARATOR . $filename - Store in
$destination
- Combine:
-
Move file (use try-catch):
- Try:
$uploadedFile->moveTo($destination) - Catch
\Exception: returnResult::failure('Failed to save uploaded file: ' . $e->getMessage())
- Try:
-
Return success:
- Return
Result::success('File uploaded successfully', ['filename' => $filename])
- Return
Hints:
DIRECTORY_SEPARATORis/on Unix,\on Windowsmkdir($path, $permissions, $recursive)creates directories- Always use try-catch when moving files
Step 3: Create the Upload Form
Section titled “Step 3: Create the Upload Form”Create app/Views/upload/uploadView.php:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Upload Demo</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"></head><body> <div class="container mt-5"> <h1 class="mb-4">File Upload Demo</h1>
<div class="mb-4"> <?= App\Helpers\FlashMessage::render() ?> </div>
<div class="card mb-4"> <div class="card-header"> <h5>Upload an Image</h5> </div> <div class="card-body"> <form method="POST" action="upload" enctype="multipart/form-data"> <div class="mb-3"> <label for="myfile" class="form-label">Choose a file:</label> <input type="file" class="form-control" id="myfile" name="myfile" accept="image/*" required> <div class="form-text"> Select an image file to upload (JPEG, PNG, GIF). </div> </div> <button type="submit" class="btn btn-primary">Upload File</button> </form> </div> </div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script></body></html>Key Points:
method="POST"required for file uploadsenctype="multipart/form-data"CRITICAL - without this, files won’t uploadname="myfile"is the key to access file in PHP
Step 4: Create the UploadController
Section titled “Step 4: Create the UploadController”Create app/Controllers/UploadController.php:
<?php
namespace App\Controllers;
use App\Helpers\FileUploadHelper;use App\Helpers\FlashMessage;use DI\Container;use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;
class UploadController extends BaseController{ //TODO: Add a constructor that calls the parent constructor.
/** * Display the upload form. */ public function index(Request $request, Response $response, array $args): Response { // TODO: Create a $data array with 'title' => 'File Upload Demo'
// TODO: Render 'upload/uploadView.php' view and pass $data }
/** * Process file upload. */ public function upload(Request $request, Response $response, array $args): Response { // TODO: Get uploaded files using getUploadedFiles() // TODO: Extract 'myfile' from the array
// TODO: Create $config array with: // - 'directory' => APP_BASE_DIR_PATH . '/public/uploads/images' // - 'allowedTypes' => ['image/jpeg', 'image/png', 'image/gif'] // - 'maxSize' => 2 * 1024 * 1024 (2MB in bytes) // - 'filenamePrefix' => 'upload_'
// TODO: Call FileUploadHelper::upload() with uploaded file and config
// TODO: Check if result is successful using isSuccess() // If successful: // - TODO: Get filename from result data: getData()['filename'] // - TODO: Use SessionManager to check/initialize 'uploaded_files' array // - TODO: Add new filename to 'uploaded_files' array and save // - TODO: Display success message using FlashMessage::success() // If not successful: // - TODO: Display error message using FlashMessage::error()
// TODO: Redirect back to 'upload.index' }}Step 5: Register Routes
Section titled “Step 5: Register Routes”Open app/Routes/web-routes.php and add:
use App\Controllers\UploadController;
// TODO: Create GET route for '/upload' → UploadController::class 'index'// Route name: 'upload.index'
// TODO: Create POST route for '/upload' → UploadController::class 'upload'// Route name: 'upload.process'Step 6: Display Uploaded Files (Optional)
Section titled “Step 6: Display Uploaded Files (Optional)”Add to app/Views/upload/uploadView.php after the upload form:
<!-- Uploaded Files Display --><?php if (!empty($_SESSION['uploaded_files'])): ?><div class="card mt-4"> <div class="card-header"> <h5>Uploaded Files</h5> </div> <div class="card-body"> <div class="row"> <?php foreach (array_reverse($_SESSION['uploaded_files']) as $filename): ?> <div class="col-md-4 mb-3"> <div class="card"> <img src="<?= APP_BASE_URL ?>/public/uploads/images/<?= htmlspecialchars($filename) ?>" class="card-img-top" alt="Uploaded image" style="height: 200px; object-fit: cover;"> <div class="card-body"> <p class="card-text small text-muted"> <?= htmlspecialchars($filename) ?> </p> </div> </div> </div> <?php endforeach; ?> </div> </div></div><?php endif; ?>Testing Your Implementation
Section titled “Testing Your Implementation”Visit http://localhost/[your-app]/upload and test:
Test Case 1: Valid Upload
Section titled “Test Case 1: Valid Upload”- Select valid image (JPEG, PNG, GIF)
- Click “Upload File”
- Expected: Green success message, image appears below
Test Case 2: Invalid File Type
Section titled “Test Case 2: Invalid File Type”- Upload .txt or .pdf file
- Expected: Red error message about invalid file type
Test Case 3: Large File
Section titled “Test Case 3: Large File”- Upload image larger than 2MB
- Expected: “File too large (max 2MB)“
Test Case 4: Multiple Uploads
Section titled “Test Case 4: Multiple Uploads”- Upload several images
- Expected: All appear in gallery with unique filenames like
upload_673abc123def.jpg
Troubleshooting:
- Check
public/uploads/images/exists - Verify write permissions (0755)
- Check
upload_max_filesizein php.ini (≥ 2MB) - Check
post_max_sizein php.ini (> upload_max_filesize) - Ensure form has
enctype="multipart/form-data" - Verify SessionMiddleware is registered
Common Issues
Section titled “Common Issues”Issue 1: “No file was uploaded”
Section titled “Issue 1: “No file was uploaded””Solution: Ensure form has enctype="multipart/form-data"
Issue 2: File Size Limit Exceeded
Section titled “Issue 2: File Size Limit Exceeded”Solution: Edit php.ini:
upload_max_filesize = 10Mpost_max_size = 12MRestart web server after changes.
Issue 3: Permission Denied
Section titled “Issue 3: Permission Denied”Windows: Right-click folder → Properties → Security → Ensure “Users” has “Modify”
Linux/Mac: chmod 755 public/uploads/
Issue 4: Files Not in Gallery
Section titled “Issue 4: Files Not in Gallery”Solution: Verify filenames stored in $_SESSION['uploaded_files'] and SessionMiddleware is registered
Issue 5: Images Not Displaying
Section titled “Issue 5: Images Not Displaying”Solution: Check image src path: APP_BASE_URL/public/uploads/images/filename.jpg
Key Concepts
Section titled “Key Concepts”FileUploadHelper Configuration
Section titled “FileUploadHelper Configuration”| Option | Type | Required | Example |
|---|---|---|---|
directory | string | Yes | APP_BASE_DIR_PATH . '/public/uploads/images' |
allowedTypes | array | Yes | ['image/jpeg', 'image/png'] |
maxSize | int | Yes | 2 * 1024 * 1024 (2MB) |
filenamePrefix | string | No | 'upload_' (default) |
Security Layers
Section titled “Security Layers”| Layer | Purpose | Implementation |
|---|---|---|
| Error Check | Ensure upload succeeded | getError() === UPLOAD_ERR_OK |
| Size Validation | Prevent large file attacks | getSize() > maxSize check |
| Media Type Check | Verify file type | getClientMediaType() whitelist |
| Unique Filenames | Prevent path traversal | uniqid('upload_') |
| Directory Isolation | Safe storage | public/uploads/images/ |
Upload Error Codes
Section titled “Upload Error Codes”| Code | Constant | Meaning |
|---|---|---|
| 0 | UPLOAD_ERR_OK | Success |
| 1 | UPLOAD_ERR_INI_SIZE | Exceeds upload_max_filesize |
| 4 | UPLOAD_ERR_NO_FILE | No file uploaded |
| 7 | UPLOAD_ERR_CANT_WRITE | Failed to write to disk |