Skip to content

Lab: Implementing File Uploads

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.


  • 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

  • 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

Objective: Set up directory structure for storing uploaded files.

Instructions:

  1. Navigate to public/ directory
  2. Create folder: uploads/
  3. Inside uploads/, create: images/
  4. Verify structure:
    public/
    └── uploads/
    └── images/

Permissions:

  • Windows: No action needed
  • Linux/Mac: chmod 755 public/uploads/

Create app/Helpers/FileUploadHelper.php:

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:

  1. Extract configuration values using null coalescing operator:

    • $directory from $config['directory'] (default: null)
    • $allowedTypes from $config['allowedTypes'] (default: [])
    • $maxSize from $config['maxSize'] (default: 0)
    • $filenamePrefix from $config['filenamePrefix'] (default: 'upload_')
  2. Validate required configuration:

    • If $directory is empty, return Result::failure('Upload directory not specified in configuration')
    • If $allowedTypes is empty, return Result::failure('Allowed file types not specified in configuration')
    • If $maxSize <= 0, return Result::failure('Maximum file size not specified in configuration')

Hints:

  • Use $variable = $config['key'] ?? defaultValue;
  • Use !$directory to check if null/empty
  • Use empty($allowedTypes) for array check

Continue in upload() method:

  1. Check upload errors:

    • Use $uploadedFile->getError()
    • Compare to UPLOAD_ERR_OK (equals 0)
    • If not equal, return Result::failure('Error uploading file')
  2. 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)")
  3. 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.')

Hints:

  • getError() returns 0 for success
  • getSize() returns bytes
  • getClientMediaType() returns ‘image/jpeg’, ‘image/png’, etc.

Why? User filenames could be malicious: ../../etc/passwd, script.php, or overwrite existing files.

Add code to generate safe filename:

  1. Extract file extension:

    • Use pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION)
    • Store in $extension
  2. Generate unique filename:

    • Use uniqid($filenamePrefix) . '.' . $extension
    • Store in $filename

Hints:

  • uniqid('prefix') generates unique ID based on current time
  • Example output: upload_67a3f2b4c5e1d.jpg

Add code to save the file:

  1. 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')
  2. Build destination path:

    • Combine: $directory . DIRECTORY_SEPARATOR . $filename
    • Store in $destination
  3. Move file (use try-catch):

    • Try: $uploadedFile->moveTo($destination)
    • Catch \Exception: return Result::failure('Failed to save uploaded file: ' . $e->getMessage())
  4. Return success:

    • Return Result::success('File uploaded successfully', ['filename' => $filename])

Hints:

  • DIRECTORY_SEPARATOR is / on Unix, \ on Windows
  • mkdir($path, $permissions, $recursive) creates directories
  • Always use try-catch when moving files

Create app/Views/upload/uploadView.php:

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 uploads
  • enctype="multipart/form-data" CRITICAL - without this, files won’t upload
  • name="myfile" is the key to access file in PHP

Create app/Controllers/UploadController.php:

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'
}
}

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'

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; ?>

Visit http://localhost/[your-app]/upload and test:

  • Select valid image (JPEG, PNG, GIF)
  • Click “Upload File”
  • Expected: Green success message, image appears below
  • Upload .txt or .pdf file
  • Expected: Red error message about invalid file type
  • Upload image larger than 2MB
  • Expected: “File too large (max 2MB)“
  • 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_filesize in php.ini (≥ 2MB)
  • Check post_max_size in php.ini (> upload_max_filesize)
  • Ensure form has enctype="multipart/form-data"
  • Verify SessionMiddleware is registered

Solution: Ensure form has enctype="multipart/form-data"

Solution: Edit php.ini:

upload_max_filesize = 10M
post_max_size = 12M

Restart web server after changes.

Windows: Right-click folder → Properties → Security → Ensure “Users” has “Modify” Linux/Mac: chmod 755 public/uploads/

Solution: Verify filenames stored in $_SESSION['uploaded_files'] and SessionMiddleware is registered

Solution: Check image src path: APP_BASE_URL/public/uploads/images/filename.jpg


OptionTypeRequiredExample
directorystringYesAPP_BASE_DIR_PATH . '/public/uploads/images'
allowedTypesarrayYes['image/jpeg', 'image/png']
maxSizeintYes2 * 1024 * 1024 (2MB)
filenamePrefixstringNo'upload_' (default)
LayerPurposeImplementation
Error CheckEnsure upload succeededgetError() === UPLOAD_ERR_OK
Size ValidationPrevent large file attacksgetSize() > maxSize check
Media Type CheckVerify file typegetClientMediaType() whitelist
Unique FilenamesPrevent path traversaluniqid('upload_')
Directory IsolationSafe storagepublic/uploads/images/
CodeConstantMeaning
0UPLOAD_ERR_OKSuccess
1UPLOAD_ERR_INI_SIZEExceeds upload_max_filesize
4UPLOAD_ERR_NO_FILENo file uploaded
7UPLOAD_ERR_CANT_WRITEFailed to write to disk