File Uploading with BowPHP
This post explains how to securely integrate file uploads into a BowPHP application: routes, controllers, storage location, and serving of uploaded files.
Overviewβ
File uploading is common but risky. With BowPHP you need to:
- Register a route that receives the file.
- Validate and move the file in a controller.
- Store files outside the public directory (e.g. root/var/storage).
- Serve files through a controller or signed URLs to prevent direct access.
HTML form (client)β
Use multipart/form-data and point it to the BowPHP route:
<form action="/upload" method="post" enctype="multipart/form-data">
<label for="file">Choose a file</label>
<input type="file" name="file" id="file" required>
<button type="submit">Upload</button>
</form>
Register the route (BowPHP)β
Add a POST route that delegates to the controller:
// filepath: /routes/web.php
// ...existing code...
use App\Controllers\UploadController;
$app->post('/upload', [UploadController::class, 'store']);
// ...existing code...
UploadController (BowPHP)β
A minimal, secure handler tailored to BowPHP. Adjust the Request API to match your version; a fallback to $_FILES can be added if needed.
// filepath: /app/controllers/UploadController.php
namespace App\Controllers;
use Bow\Http\Request;
use Bow\Http\Response;
use Bow\Http\UploadedFile;
class UploadController
{
public function store(Request $request): Response
{
if (!$request->hasFile('file')) {
return response('No file provided', 400);
}
/** @var UploadedFile $file */
$file = $request->file('file');
if (!$file->isUploaded()) {
return response('Error during upload', 400);
}
// Limits
$maxBytes = 5 * 1024 * 1024; // 5 MB
if ($file->getFilesize() > $maxBytes) {
return response('File too large', 400);
}
// MIME validation (use finfo on the temporary file if available)
$allowed = [
'image/png' => 'png',
'image/jpeg' => 'jpg',
'application/pdf' => 'pdf',
];
$mime = $file->getTypeMime();
if (!isset($allowed[$mime])) {
return response('Invalid file type', 400);
}
$extension = $file->getExtension();
// Store outside the public folder - use the project's var/storage directory
$uploadsDirectory = storage_path('uploads'); // project_root/var/storage
if (!is_dir($uploadsDirectory)) {
mkdir($uploadsDirectory, 0750, true);
}
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
// Move the file via the UploadedFile API
$result = $file->moveTo($uploadsDirectory, $filename);
// Verify the move succeeded (moveTo may return void/true/path/object depending on implementation)
$dest = rtrim($uploadsDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
if (is_string($result) && is_file($result)) {
$dest = $result;
}
if (!is_file($dest)) {
return response('Failed to move the file', 500);
}
chmod($dest, 0640);
// Return a safe reference (do not expose the full path). Consider a signed URL or an ID.
return response()->json([
'status' => 'ok',
'file' => $filename,
]);
}
}
Serving uploaded files (via controller, secure)β
Serve files through BowPHP so you can apply authentication and headers instead of exposing the storage folder.
// filepath: /app/controllers/FileServeController.php
namespace App\Controllers;
use Bow\Http\Request;
use Bow\Http\Response;
class FileServeController
{
public function show(Request $request, $filename): Response
{
$uploadsDirectory = storage_path('uploads'); // project_root/var/storage
$file = $uploadsDirectory . DIRECTORY_SEPARATOR . basename($filename);
if (!is_file($file)) {
return response('Not found', 404);
}
// Optional: check permissions, ownership, or authentication here.
// Send the file with the appropriate headers
return response()->download($file, null, [
'Content-Type' => mime_content_type($file),
'Content-Length' => filesize($file),
'Content-Disposition' => 'attachment; filename="' . basename($file) . '"',
]);
}
}
Register the GET route for serving (apply authentication and rate-limiting middleware if needed):
// filepath: /routes/web.php
// ...existing code...
$app->get('/uploads/{filename}', [\App\Controllers\FileServeController::class, 'show'])
->middleware(['auth']); // apply authentication/rate-limiting middleware if needed
// ...existing code...
BowPHP configuration & storage notesβ
- Keep uploads under project_root/var/storage (outside the public folder).
- If you use config/storage.php or config/filesystems.php, declare a local disk pointing to var/storage.
- For large files, consider chunked uploads and streaming to avoid memory spikes.
- Use signed URLs or a controller to grant temporary access instead of direct links.
Deployment notesβ
- Make sure project_root/var/storage exists in every environment with the correct owner and permissions.
- Keep the php.ini limits in sync with those defined in the application.
- Consider migrating uploads to object storage (S3) for scalability; serve them via signed URLs.
Further readingβ
- PHP manual: Handling file uploads
- OWASP: File Upload Cheat Sheet
- BowPHP docs: routing, controllers, and response helpers
Is something missing?
If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.