Lab 7: Implementing File Uploads
Resources
Section titled “Resources”Overview
Section titled “Overview”In this lab, you will implement secure file upload functionality for your admin panel. You will build a reusable FileUploadHelper class, create an upload form inside the admin layout, handle file validation, and display uploaded files.
Learning Objectives
Section titled “Learning Objectives”- Create a reusable
FileUploadHelperclass for file uploads. - Build an
UploadControllerextendingBaseController. - 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.
Prerequisites
Section titled “Prerequisites”- Working Slim 4 application with BaseController pattern.
- Flash Messages lab completed.
- Result Pattern tutorial completed.
- SessionMiddleware registered.
public/directory available.
Step 1: Add File Upload Link to Admin Sidebar
Section titled “Step 1: Add File Upload Link to Admin Sidebar”Objective: Add a navigation link for file uploads in the admin sidebar.
Instructions:
- Open
app/Views/admin/adminHeader.php - Find the navigation list (
<ul class="nav flex-column ...">) in the sidebar - Add a new nav item after the existing links:
<li class="nav-item"> <a class="nav-link" href="<?= APP_BASE_URL ?>/admin/upload"> File Upload </a></li>- Save the file
Step 2: Create the Uploads Directory
Section titled “Step 2: Create the Uploads Directory”Objective: Set up the directory structure for storing uploaded files.
Instructions:
- Navigate to your
public/directory - Create a folder named
uploads/ - Inside
uploads/, create a folder namedimages/ - Verify the structure:
public/└── uploads/└── images/
Step 3: Create the FileUploadHelper Class
Section titled “Step 3: Create the FileUploadHelper Class”Step 3.1: Class Structure
Section titled “Step 3.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 3.2: Extract and Validate Configuration
Section titled “Step 3.2: Extract and Validate Configuration”Inside the upload() method, add code to:
-
Extract configuration values from the
$configarray using the null coalescing operator (??):$directoryfrom$config['directory'](default:null)$allowedTypesfrom$config['allowedTypes'](default:[])$maxSizefrom$config['maxSize'](default:0)$filenamePrefixfrom$config['filenamePrefix'](default:'upload_')
-
Validate the required configuration values. If any are missing, return a
Result::failure()with an appropriate error message:- 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
Step 3.3: Validate the Uploaded File
Section titled “Step 3.3: Validate the Uploaded File”Continue in the upload() method:
-
Check for upload errors using
$uploadedFile->getError(). If it does not equalUPLOAD_ERR_OK, returnResult::failure('Error uploading file'). -
Validate the file size using
$uploadedFile->getSize(). If it exceeds$maxSize, calculate the limit in MB withround($maxSize / (1024 * 1024), 1)and return a failure message. -
Validate the media type using
$uploadedFile->getClientMediaType(). Usein_array()to check it against the$allowedTypesarray. If not allowed, return a failure with the list of accepted types.
Step 3.4: Generate a Safe Filename
Section titled “Step 3.4: Generate a Safe Filename”User-provided filenames can be malicious (e.g., ../../etc/passwd, script.php) or collide with existing files. Add code to generate a safe filename:
-
Extract the file extension using
pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION). -
Generate a unique filename using
uniqid($filenamePrefix) . '.' . $extension. This produces filenames likeupload_67a3f2b4c5e1d.jpg.
Step 3.5: Create Directory and Save File
Section titled “Step 3.5: Create Directory and Save File”Add code to save the file to disk:
-
Check if the directory exists using
is_dir($directory). If it does not, create it withmkdir($directory, 0755, true). Ifmkdirfails, return a failure result. -
Build the full destination path by combining
$directory . DIRECTORY_SEPARATOR . $filename. -
Wrap
$uploadedFile->moveTo($destination)in a try-catch block. If an exception is thrown, returnResult::failure('Failed to save uploaded file: ' . $e->getMessage()). -
On success, return
Result::success('File uploaded successfully', ['filename' => $filename]).
Step 4: Create the Upload Form View
Section titled “Step 4: Create the Upload Form View”Create app/Views/admin/upload/uploadView.php:
<?php
use App\Helpers\ViewHelper;
ViewHelper::loadAdminHeader($title);?>
<div class="card mb-4"> <div class="card-header"> <h5>Upload an Image</h5> </div> <div class="card-body"> <form method="POST" action="<?= APP_BASE_URL ?>/admin/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>
<?php ViewHelper::loadAdminFooter(); ?>Step 5: Create the UploadController
Section titled “Step 5: Create the UploadController”Create app/Controllers/UploadController.php:
<?php
namespace App\Controllers;
use App\Helpers\FileUploadHelper;use App\Helpers\FlashMessage;use App\Helpers\SessionManager;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'
// TODO: Render 'admin/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 $request->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: // 1. Get the filename from the result data // 2. Retrieve the existing 'uploaded_files' array from the session (default to []) // 3. Append the new filename to the array and save it back to the session // 4. Display a success flash message // If not successful: // 1. Get the error message from the result object // 2. Display an error flash message
// TODO: Redirect back to 'upload.index' }}Step 6: Register Routes
Section titled “Step 6: Register Routes”Objective: Add routes for file upload inside the /admin route group.
Instructions:
- Open
app/Routes/web-routes.php - Add the import at the top:
use App\Controllers\UploadController;- Inside the existing
/admingroup, add two routes:
$app->group('/admin', function ($group) { // ... existing routes ...
// TODO: Add GET route for /upload // - Controller: UploadController::class, 'index' // - Named route: 'upload.index'
// TODO: Add POST route for /upload // - Controller: UploadController::class, 'upload' // - Named route: 'upload.process'});- Save the file
Step 7: Display Uploaded Files
Section titled “Step 7: Display Uploaded Files”Objective: Show a gallery of previously uploaded images below the upload form.
Instructions:
- Open
app/Views/admin/upload/uploadView.php - Add the following code between the upload form card and the footer:
<!-- Uploaded Files Display --><?php$uploadedFiles = App\Helpers\SessionManager::get('uploaded_files', []);?><?php if (!empty($uploadedFiles)): ?><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($uploadedFiles) as $filename): ?> <div class="col-md-4 mb-3"> <div class="card"> <img src="<?= APP_BASE_URL ?>/public/uploads/images/<?= hs($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"> <?= hs($filename) ?> </p> </div> </div> </div> <?php endforeach; ?> </div> </div></div><?php endif; ?>- Save the file
Step 8: Test the Upload
Section titled “Step 8: Test the Upload”Objective: Verify that the file upload works correctly.
Instructions:
- Visit
http://localhost/[your-app]/admin/uploadin your browser - Verify the “File Upload” link appears in the admin sidebar
- Test the following scenarios:
- Upload a valid image (JPEG, PNG, GIF) and verify the success message and gallery display
- Try uploading a non-image file (.txt, .pdf) and verify the error message
- Try uploading a file larger than 2MB and verify the size error
- Upload multiple images and verify they all appear in the gallery