FileResponse.php
PHP
Path: src/Http/Message/FileResponse.php
<?php
namespace mini\Http\Message;
/**
* Response for serving files with automatic MIME type detection
*
* Serves files from the filesystem with proper Content-Type, Content-Length,
* and optional Content-Disposition headers for downloads.
*
* // Serve an image inline (displayed in browser)
* return new FileResponse('/path/to/image.png');
*
* // Serve a file as download
* return new FileResponse('/path/to/file.pdf', download: true);
*
* // Serve with custom filename for download
* return new FileResponse('/path/to/file.pdf', download: 'report.pdf');
*
* // Override MIME type
* return new FileResponse('/path/to/file', mimeType: 'application/octet-stream');
*/
class FileResponse extends Response {
/**
* Common MIME types by extension
*/
private const MIME_TYPES = [
// Images
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'webp' => 'image/webp',
'ico' => 'image/x-icon',
'bmp' => 'image/bmp',
// Documents
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
// Text
'txt' => 'text/plain',
'csv' => 'text/csv',
'html' => 'text/html',
'htm' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'xml' => 'application/xml',
'md' => 'text/markdown',
// Archives
'zip' => 'application/zip',
'gz' => 'application/gzip',
'tar' => 'application/x-tar',
'rar' => 'application/vnd.rar',
'7z' => 'application/x-7z-compressed',
// Audio
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav',
'ogg' => 'audio/ogg',
// Video
'mp4' => 'video/mp4',
'webm' => 'video/webm',
'avi' => 'video/x-msvideo',
// Fonts
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'otf' => 'font/otf',
'eot' => 'application/vnd.ms-fontobject',
];
/**
* @param string $filePath Absolute path to the file
* @param bool|string $download false = inline, true = download with original name, string = download with custom name
* @param string|null $mimeType Override MIME type detection
* @param int $statusCode HTTP status code (default 200)
*/
public function __construct(
string $filePath,
bool|string $download = false,
?string $mimeType = null,
int $statusCode = 200
) {
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: $filePath");
}
if (!is_readable($filePath)) {
throw new \InvalidArgumentException("File not readable: $filePath");
}
$content = file_get_contents($filePath);
$size = strlen($content);
// Detect MIME type
if ($mimeType === null) {
$mimeType = $this->detectMimeType($filePath);
}
$headers = [
'Content-Type' => $mimeType,
'Content-Length' => (string) $size,
];
// Set Content-Disposition for downloads
if ($download !== false) {
$filename = is_string($download) ? $download : basename($filePath);
// Sanitize filename for header
$filename = preg_replace('/[^\w\.\-]/', '_', $filename);
$headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
}
parent::__construct($content, $headers, $statusCode);
}
/**
* Detect MIME type from file extension or content
*/
private function detectMimeType(string $filePath): string
{
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
// Check our mapping first (faster and more reliable for common types)
if (isset(self::MIME_TYPES[$extension])) {
return self::MIME_TYPES[$extension];
}
// Try finfo if available
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
if ($mimeType && $mimeType !== 'application/octet-stream') {
return $mimeType;
}
}
// Default fallback
return 'application/octet-stream';
}
}