thingmabobby / file-upload-service
A comprehensive PHP file upload service supporting base64 data URIs, traditional file uploads, HEIC/HEIF conversion, and multiple file types
Fund package maintenance!
thingmabobby
Installs: 8
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/thingmabobby/file-upload-service
Requires
- php: ^8.1
- maestroerror/php-heic-to-jpg: ^1.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/php-code-coverage: ^10.0
- phpunit/phpunit: ^10.0
Suggests
- ext-fileinfo: Required for MIME type detection and enhanced file validation security
README
A comprehensive PHP service for handling file uploads from base64 encoded data URIs or traditional file uploads ($_FILES array). Supports images, PDFs, CAD drawings, videos, and other common file types with pluggable storage backends, advanced security features, and flexible validation options.
Features
- Multiple Input Types: Handles both
$_FILESarrays and base64 data URIs seamlessly - Comprehensive File Type Support: Images, PDFs, CAD files, documents, archives, and videos
- Custom MIME Type Support: Allow specific MIME types and custom extensions beyond predefined categories
- Advanced Security: Path traversal protection, filename sanitization, MIME type validation, atomic file operations
- HEIC/HEIF Conversion: Automatically converts HEIC/HEIF files to JPEG with graceful degradation
- Collision Resolution: Multiple strategies for handling filename conflicts (increment, UUID, timestamp, custom)
- Error Handling: Comprehensive error tracking with rollback support
- High Performance Mode: Optimized for network storage and high-collision scenarios
- Pluggable Storage: Extensible storage backends via FileSaverInterface (filesystem, cloud, custom)
- Type Safety: Full enum-based type system for file types and strategies
- Separate DTOs: Specialized data transfer objects for different input types
- Cross-Platform: Full Windows, Linux, and macOS compatibility with proper path handling
Minimum Requirements
- PHP 8.1 or higher
Suggested Requirements
ext-fileinfofor MIME type validation to workmaestroerror/php-heic-to-jpgfor HEIC/HEIF conversion
Installation
composer require thingmabobby/file-upload-service
Basic Usage
use FileUploadService\FileUploadService; // Simple usage - FilesystemSaver auto-configured from uploadDestination $service = new FileUploadService(['image', 'pdf']); try { $result = $service->save( input: $_FILES['files'], uploadDestination: __DIR__ . '/uploads', // Absolute path - basePath derived automatically! filenames: ['photo.jpg', 'document.pdf'] ); if ($result->hasSuccessfulUploads()) { echo "Uploaded " . $result->successfulCount . " files\n"; foreach ($result->successfulFiles as $filePath) { echo "Saved: " . $filePath . "\n"; } } if ($result->hasErrors()) { foreach ($result->getErrorMessages() as $errorMessage) { echo "Error: " . $errorMessage . "\n"; } } } catch (RuntimeException $e) { echo "Critical error: " . $e->getMessage() . "\n"; }
Traditional Usage (Backward Compatible)
use FileUploadService\FileUploadService; use FileUploadService\FilesystemSaver; // Configure FilesystemSaver explicitly $fileSaver = new FilesystemSaver('/var/www/uploads', 0755, true); $service = new FileUploadService(['image', 'pdf'], $fileSaver); $result = $service->save( input: $_FILES['files'], uploadDestination: 'photos', // Relative to basePath filenames: ['photo1.jpg', 'photo2.jpg'] );
Custom MIME Type Support
The service now supports custom MIME types and extensions beyond the predefined categories:
Using Constructor with MIME Types
// Allow specific MIME types using 'mime:' prefix $service = new FileUploadService([ 'image', // All image types 'mime:text/csv', // Custom CSV MIME type 'mime:application/x-custom', // Custom application MIME type '.xyz' // Custom extension ]);
Using Setter Methods
$service = new FileUploadService(['image']); // Set allowed categories $service->setAllowedCategories(['image', 'video']); // Set custom extensions $service->setAllowedExtensions(['xyz', 'custom']); // Set custom MIME types $service->setAllowedMimeTypes(['text/csv', 'application/x-custom']); // Get current settings $categories = $service->getAllowedCategories(); // Returns array<FileTypeEnum> $extensions = $service->getAllowedExtensions(); // Returns array<string> $mimeTypes = $service->getAllowedMimeTypes(); // Returns array<string>
Mixed Configuration Example
$service = new FileUploadService([ 'image', // Image category 'video', // Video category 'mime:text/csv', // Custom CSV MIME type 'mime:application/x-php', // Custom PHP MIME type '.xyz', // Custom extension '.custom' // Another custom extension ]); // Additional configuration via setters $service->setAllowedMimeTypes(['text/plain', 'application/json']); $service->setAllowedExtensions(['txt', 'json']);
Video File Support
The service now includes comprehensive video file support:
// Allow video files $service = new FileUploadService(['image', 'video']); // Supported video formats: // MP4, AVI, MOV, WMV, FLV, WebM, MKV, MPEG, MPG, 3GP, M4V, OGV
Exception Handling
The FileUploadService uses a two-tier exception handling strategy:
Critical Errors (Thrown Exceptions)
These errors prevent the entire operation from proceeding and should be caught with try-catch:
- Directory Issues: Upload destination doesn't exist and can't be created
- Permission Issues: Upload destination is not writable
- Parameter Mismatches: Filename count doesn't match input count
- Configuration Errors: Invalid file types provided
Individual File Errors (Captured in Result)
These errors affect individual files but allow the operation to continue:
- File Validation Failures: Invalid file types, corrupted data
- Upload Errors: PHP upload errors (file too large, partial upload, etc.)
- Processing Errors: HEIC conversion failures, file saving issues
Best Practice
Always wrap the save() method in a try-catch block to handle critical errors, then check the result for individual file errors:
try { $result = $fileUploadService->save($input, $uploadDestination, $filenames); // Handle individual file results if ($result->hasSuccessfulUploads()) { // Process successful uploads } if ($result->hasErrors()) { // Handle individual file errors } } catch (RuntimeException $e) { // Handle critical system errors error_log("File upload failed: " . $e->getMessage()); }
Configuration
File Type Restrictions
// Simple usage with raw strings (recommended) $service = new FileUploadService([ 'image', 'pdf', 'video', 'cad' ]); // Allow all file types $service = new FileUploadService(['all']); // Mix categories, custom extensions, and MIME types $service = new FileUploadService([ 'image', 'video', 'mime:text/csv', 'mime:application/x-custom', '.xyz', '.custom' ]); // Advanced usage with enums (optional) use FileUploadService\Enum\FileTypeEnum; $service = new FileUploadService([ FileTypeEnum::IMAGE, FileTypeEnum::VIDEO, FileTypeEnum::PDF ]); // Get all available file types - returns array of FileTypeEnum cases $allFileTypes = FileUploadService::getAvailableFileTypeCategories();
HEIC/HEIF Conversion
The service automatically attempts to convert HEIC/HEIF files to JPEG format. If the conversion library is not available, it gracefully degrades by saving the original HEIC/HEIF file as-is.
// Enable HEIC to JPEG conversion (default) $service = new FileUploadService( allowedFileTypes: ['image'], convertHeicToJpg: true ); // Disable HEIC conversion (save as-is) $service = new FileUploadService( allowedFileTypes: ['image'], convertHeicToJpg: false ); // Check if HEIC conversion is available if ($service->isHeicConversionEnabled()) { echo "HEIC conversion is enabled"; }
HEIC Conversion Behavior:
- Default behavior: HEIC/HEIF files are automatically detected via MIME type and converted to JPEG format
- Detection method: Uses MIME type from uploaded file (primary), finfo() detection (fallback), and binary header checks (last resort)
- Extension-agnostic: Files are detected by content, not file extension, preventing misidentification
- Conversion fails: Falls back to saving the original HEIC/HEIF file
- Always graceful: Never fails uploads due to conversion issues
- Library dependency: Uses
maestroerror/php-heic-to-jpgpackage (required dependency)
Collision Resolution Strategies
// Increment strategy (default): filename_1.jpg, filename_2.jpg $service = new FileUploadService( allowedFileTypes: ['image'], collisionStrategy: 'increment' ); // UUID strategy: filename_a1b2c3d4.jpg $service = new FileUploadService( allowedFileTypes: ['image'], collisionStrategy: 'uuid' ); // Timestamp strategy: filename_1234567890.jpg $service = new FileUploadService( allowedFileTypes: ['image'], collisionStrategy: 'timestamp' ); // Advanced usage with enums (optional) $service = new FileUploadService( allowedFileTypes: ['image'], collisionStrategy: CollisionStrategyEnum::UUID ); // Custom strategy $service = new FileUploadService( allowedFileTypes: ['image'], collisionStrategy: fn($base, $dir, $exts, $used) => $base . '_custom_' . bin2hex(random_bytes(4)) );
High Performance Mode
// Optimized for network storage or high-collision scenarios $service = new FileUploadService( allowedFileTypes: ['image'], highPerformanceMode: true ); // Automatically uses 'uuid' strategy and filters extensions to reduce filesystem calls
Error Handling and Rollback
// Enable rollback on error (default: false) // When enabled, removes all successfully uploaded files if any error occurs $service = new FileUploadService( allowedFileTypes: ['image'], rollbackOnError: true ); // Default behavior (rollback disabled) $service = new FileUploadService(['image']); // rollbackOnError defaults to false // Check if rollback is enabled if ($service->isRollbackOnErrorEnabled()) { echo "Rollback is enabled"; }
Storage Backends
use FileUploadService\FileUploadService; // Simple approach: FilesystemSaver is auto-created from uploadDestination $service = new FileUploadService(['image']); $result = $service->save($_FILES['image'], '/var/www/uploads', ['photo.jpg']); // Automatically creates FilesystemSaver with basePath derived from uploadDestination // Advanced: Provide FilesystemSaver in constructor (backward compatible) use FileUploadService\FilesystemSaver; $filesystemSaver = new FilesystemSaver('/var/uploads', 0755, true); $service = new FileUploadService( allowedFileTypes: ['image'], fileSaver: $filesystemSaver ); // Advanced: Provide FileSaverInterface per save() call $result = $service->save( input: $_FILES['image'], uploadDestination: 'uploads', filenames: ['photo.jpg'], fileSaver: $customFileSaver // Optional override ); // Cloud storage example (implement FileSaverInterface) // $cloudSaver = new CloudStorageSaver('bucket-name', 'region', 'credentials'); // $service = new FileUploadService(['image']); // $result = $service->save($input, 'uploads', $filenames, fileSaver: $cloudSaver);
Input Types
$_FILES Structure Handling
The service automatically detects and handles different $_FILES array structures:
Single File Upload:
$_FILES['files'] = [ 'name' => 'image.jpg', // String 'type' => 'image/jpeg', // String 'tmp_name' => '/tmp/abc123', // String 'error' => 0, // Integer 'size' => 12345 // Integer ];
Multiple File Upload:
$_FILES['files'] = [ 'name' => ['image1.jpg', 'image2.png'], // Array of strings 'type' => ['image/jpeg', 'image/png'], // Array of strings 'tmp_name' => ['/tmp/abc123', '/tmp/def456'], // Array of strings 'error' => [0, 0], // Array of integers 'size' => [12345, 67890] // Array of integers ];
Base64 Data URIs
$dataUri = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...';
Mixed Input Types
$input = [ 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...', // base64 $_FILES['uploaded_file'], // $_FILES 'data:application/pdf;base64,JVBERi0xLjQKMSAwIG9i...', // base64 $_FILES['another_file'] // $_FILES ]; $filenames = ['image1.jpg', 'upload1.pdf', 'document.pdf', 'file2.doc']; $result = $fileUploadService->save($input, $uploadDestination, $filenames);
Supported File Types
The service supports a comprehensive range of file types through the SupportedFileTypesEnum. Here are all supported file types organized by category:
Images
| Extension | MIME Type | Description |
|---|---|---|
jpg |
image/jpeg |
JPEG images |
png |
image/png |
PNG images |
gif |
image/gif |
GIF images |
webp |
image/webp |
WebP images |
avif |
image/avif |
AVIF images |
jxl |
image/jxl |
JPEG XL images |
bmp |
image/bmp |
BMP images |
tiff |
image/tiff |
TIFF images |
heic |
image/heic |
HEIC images (converted to JPEG) |
heif |
image/heif |
HEIF images (converted to JPEG) |
Videos
| Extension | MIME Type | Description |
|---|---|---|
mp4 |
video/mp4 |
MP4 videos |
avi |
video/x-msvideo |
AVI videos |
mov |
video/quicktime |
QuickTime videos |
wmv |
video/x-ms-wmv |
Windows Media videos |
flv |
video/x-flv |
Flash videos |
webm |
video/webm |
WebM videos |
mkv |
video/x-matroska |
Matroska videos |
mpeg |
video/mpeg |
MPEG videos |
mpg |
video/mpeg |
MPEG videos |
3gp |
video/3gpp |
3GPP videos |
m4v |
video/x-m4v |
iTunes videos |
ogv |
video/ogg |
Ogg videos |
PDF Documents
| Extension | MIME Type | Description |
|---|---|---|
pdf |
application/pdf |
Standard PDF documents |
pdf |
application/x-pdf |
Alternative PDF MIME type |
pdf |
application/acrobat |
Adobe Acrobat PDF |
pdf |
application/vnd.pdf |
Vendor-specific PDF |
Documents
| Extension | MIME Type | Description |
|---|---|---|
doc |
application/msword |
Microsoft Word documents |
docx |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
Microsoft Word XML documents |
xls |
application/vnd.ms-excel |
Microsoft Excel spreadsheets |
xlsx |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
Microsoft Excel XML spreadsheets |
ppt |
application/vnd.ms-powerpoint |
Microsoft PowerPoint presentations |
pptx |
application/vnd.openxmlformats-officedocument.presentationml.presentation |
Microsoft PowerPoint XML presentations |
txt |
text/plain |
Plain text files |
rtf |
application/rtf |
Rich Text Format documents |
csv |
text/csv |
Comma-separated values |
xml |
application/xml |
XML documents |
json |
application/json |
JSON documents |
odt |
application/vnd.oasis.opendocument.text |
OpenDocument text |
ods |
application/vnd.oasis.opendocument.spreadsheet |
OpenDocument spreadsheet |
odp |
application/vnd.oasis.opendocument.presentation |
OpenDocument presentation |
CAD Files
| Extension | MIME Type | Description |
|---|---|---|
dwg |
application/dwg |
AutoCAD drawings |
dxf |
application/dxf |
AutoCAD DXF files |
step |
application/step |
STEP 3D models |
iges |
application/iges |
IGES 3D models |
stl |
application/stl |
STL 3D models |
sldprt |
application/sldprt |
SolidWorks part files |
sldasm |
application/sldasm |
SolidWorks assembly files |
Archives
| Extension | MIME Type | Description |
|---|---|---|
zip |
application/zip |
ZIP archives |
rar |
application/x-rar-compressed |
RAR archives |
7z |
application/x-7z-compressed |
7-Zip archives |
tar |
application/x-tar |
TAR archives |
gz |
application/gzip |
GZIP compressed files |
File Type Categories
The service organizes file types into the following categories (accessible via FileTypeEnum):
image- All image formats (JPEG, PNG, GIF, WebP, AVIF, JXL, BMP, TIFF, HEIC, HEIF)video- All video formats (MP4, AVI, MOV, WMV, FLV, WebM, MKV, MPEG, 3GP, M4V, OGV)pdf- PDF documents (all MIME variants)doc- All document formats (Office, OpenDocument, text files)cad- CAD and technical drawing filesarchive- Compressed archive formatsall- Allow all file types (no restrictions)
Note: For programmatic access to this information, use the SupportedFileTypesEnum class which provides methods to get extensions, MIME types, and categories for each supported file type.
Security Features
Path Traversal Protection
FilesystemSaver::resolvePath()prevents../attacks in filenames- Upload destinations can use legitimate directory navigation (e.g.,
../images/) - All paths are validated to stay within
basePath - Absolute paths are rejected for security
Filename Sanitization
FilenameSanitizer::cleanFilename()removes dangerous characters- Null byte removal prevents security bypasses
- Unicode normalization prevents confusion attacks
- Length limits prevent filesystem issues
MIME Type Validation
FileServiceValidator::isFileTypeAllowed()validates actual file content- Uses
finfo_open()to detect real MIME types - Prevents file type spoofing attacks
- Custom MIME types are validated against actual file content
Atomic File Operations
- All file saves use temporary files +
rename()for atomicity - Prevents race conditions and partial file writes
- Secure temporary directory creation with restricted permissions
Double Extension Protection
- Files like
malware.php.txtare treated as.txtfiles (safe) - Files like
malware.txt.phpare blocked by MIME validation - MIME type detection prevents execution of disguised files
API Reference
FileUploadService
Constructor
public function __construct( private readonly array $allowedFileTypes = [FileTypeEnum::IMAGE, FileTypeEnum::PDF, FileTypeEnum::CAD], private readonly ?FileSaverInterface $fileSaver = null, private readonly bool $createDirectory = true, private readonly int $directoryPermissions = 0775, private readonly bool $rollbackOnError = false, string|callable|CollisionStrategyEnum $collisionStrategy = CollisionStrategyEnum::INCREMENT, private readonly bool $highPerformanceMode = false, private readonly bool $convertHeicToJpg = true )
Public Methods
File Management:
save(array $inputs, string $uploadDestination, array $filenames, bool $overwriteExisting = false, bool $generateUniqueFilenames = false, ?FileSaverInterface $fileSaver = null): FileUploadResult
Configuration:
-
setAllowedFileTypes(array $allowedFileTypes): void -
getAllowedFileTypes(): array<FileTypeEnum|string> -
setAllowedCategories(array $categories): void -
getAllowedCategories(): array<FileTypeEnum> -
setAllowedExtensions(array $extensions): void -
getAllowedExtensions(): array<string> -
setAllowedMimeTypes(array $mimeTypes): void -
getAllowedMimeTypes(): array<string> -
allowFileType(string|array $fileTypes): void -
disallowFileType(string|array $fileTypes): void
Status Checks:
isRollbackOnErrorEnabled(): boolisHeicConversionEnabled(): boolisFileTypeCategoryAllowed(string $fileType): boolisFileTypeAllowedByExtension(string $extension): boolisUnrestricted(): bool
Utilities:
getFileTypeCategoryFromExtension(string $extension): FileTypeEnum|stringgetRestrictionDescription(): stringcleanFilename(string $filename, bool $removeUnderscores = false): string
Static Methods
getAvailableFileTypeCategories(): array
FileUploadResult
Properties:
public readonly array $successfulFiles- Array of successfully uploaded file pathspublic readonly array $errors- Array of upload errorspublic readonly int $totalFiles- Total number of files attemptedpublic readonly int $successfulCount- Number of successfully uploaded files
Methods:
hasErrors(): boolisCompleteSuccess(): boolhasSuccessfulUploads(): boolgetErrorMessages(): arraygetErrorForFile(string $filename): ?FileUploadError
FileUploadError
Properties:
public readonly string $filename- The filename that caused the errorpublic readonly string $message- The error messagepublic readonly string $code- The error code (optional, defaults to empty string)
Methods:
getDescription(): string
Architecture
The service is built with a clean separation of concerns:
- FileUploadService: Main orchestrator and public API
- FileServiceValidator: Handles file validation and type checking
- FileCollisionResolver: Manages filename collision resolution
- FileUploadSave: Handles actual file processing and saving operations
- FileSaverInterface: Pluggable storage backend interface
- FilesystemSaver: Default filesystem storage implementation
- CloudStorageSaver: Example cloud storage implementation
- Enum Classes: Type-safe constants and enumerations (FileTypeEnum, CollisionStrategyEnum, UploadErrorCodeEnum, SupportedFileTypesEnum)
- DTO Classes: Specialized data transfer objects (FileUploadDTO, DataUriDTO)
- Utils: Utility classes (FilenameSanitizer)
Storage Backend Interface
The service uses the FileSaverInterface to abstract file storage operations, allowing for different storage backends:
- FilesystemSaver: Local filesystem storage (included)
- Cloud Storage: AWS S3, Google Cloud Storage, Azure Blob Storage (implementations can be added)
- Custom Storage: Any storage system can be implemented by implementing
FileSaverInterface
The interface provides methods for:
saveFile(string $source, string $targetPath, bool $overwriteExisting = false): stringresolveTargetPath(string $uploadDestination, string $filename): stringensureUploadDestinationExists(string $uploadDestination): voidfileExists(string $targetPath): booldeleteFile(string $targetPath): boolgetBasePath(): string
Testing
The package includes comprehensive test coverage with PHPUnit:
# Run all tests composer test # Run tests with HTML coverage report composer test-coverage # Run tests with text coverage report composer test-coverage-text # Run tests with verbose output composer test-verbose # Run tests and stop on first failure composer test-stop-on-failure
License
Unlicense - See LICENSE file for details.