Skip to content

Lab 7: Implementing File Uploads

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.


  • 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.

  • Working Slim 4 application with BaseController pattern.
  • Flash Messages lab completed.
  • Result Pattern tutorial completed.
  • SessionMiddleware registered.
  • public/ directory available.

Section titled “Step 1: Add File Upload Link to Admin Sidebar”

Objective: Add a navigation link for file uploads in the admin sidebar.

Instructions:

  1. Open app/Views/admin/adminHeader.php
  2. Find the navigation list (<ul class="nav flex-column ...">) in the sidebar
  3. 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>
  1. Save the file

Objective: Set up the directory structure for storing uploaded files.

Instructions:

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

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 3.2: Extract and Validate Configuration

Section titled “Step 3.2: Extract and Validate Configuration”

Inside the upload() method, add code to:

  1. Extract configuration values from the $config array using the 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 the required configuration values. If any are missing, return a Result::failure() with an appropriate error message:

    • 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')

Continue in the upload() method:

  1. Check for upload errors using $uploadedFile->getError(). If it does not equal UPLOAD_ERR_OK, return Result::failure('Error uploading file').

  2. Validate the file size using $uploadedFile->getSize(). If it exceeds $maxSize, calculate the limit in MB with round($maxSize / (1024 * 1024), 1) and return a failure message.

  3. Validate the media type using $uploadedFile->getClientMediaType(). Use in_array() to check it against the $allowedTypes array. If not allowed, return a failure with the list of accepted types.


User-provided filenames can be malicious (e.g., ../../etc/passwd, script.php) or collide with existing files. Add code to generate a safe filename:

  1. Extract the file extension using pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION).

  2. Generate a unique filename using uniqid($filenamePrefix) . '.' . $extension. This produces filenames like upload_67a3f2b4c5e1d.jpg.


Add code to save the file to disk:

  1. Check if the directory exists using is_dir($directory). If it does not, create it with mkdir($directory, 0755, true). If mkdir fails, return a failure result.

  2. Build the full destination path by combining $directory . DIRECTORY_SEPARATOR . $filename.

  3. Wrap $uploadedFile->moveTo($destination) in a try-catch block. If an exception is thrown, return Result::failure('Failed to save uploaded file: ' . $e->getMessage()).

  4. On success, return Result::success('File uploaded successfully', ['filename' => $filename]).


Create app/Views/admin/upload/uploadView.php:

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

Create app/Controllers/UploadController.php:

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

Objective: Add routes for file upload inside the /admin route group.

Instructions:

  1. Open app/Routes/web-routes.php
  2. Add the import at the top:
use App\Controllers\UploadController;
  1. Inside the existing /admin group, add two routes:
app/Routes/web-routes.php
$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'
});
  1. Save the file

Objective: Show a gallery of previously uploaded images below the upload form.

Instructions:

  1. Open app/Views/admin/upload/uploadView.php
  2. 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; ?>
  1. Save the file

Objective: Verify that the file upload works correctly.

Instructions:

  1. Visit http://localhost/[your-app]/admin/upload in your browser
  2. Verify the “File Upload” link appears in the admin sidebar
  3. 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