# LiveComponent File Uploads Komplette Dokumentation des File Upload Systems für LiveComponents mit Drag & Drop, Multi-File Support und Progress Tracking. ## Übersicht Das File Upload System ermöglicht es, Dateien direkt in LiveComponents hochzuladen mit: - **Drag & Drop Support** - Intuitive Dateiauswahl durch Ziehen & Ablegen - **Multi-File Uploads** - Mehrere Dateien gleichzeitig hochladen - **Progress Tracking** - Echtzeit-Fortschrittsanzeige pro Datei und gesamt - **Preview Funktionalität** - Bild-Vorschauen und Datei-Icons - **Client-Side Validation** - Validierung vor dem Upload (MIME-Type, Größe, Extension) - **CSRF Protection** - Automatische CSRF-Token-Integration - **Component State Management** - Nahtlose Integration mit LiveComponent State ## Architektur ``` ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ FileUploadWidget │───▶│ ComponentFileUploader│───▶│ Backend Route │ │ (UI Component) │ │ (Upload Manager) │ │ /upload endpoint │ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │ │ │ Drop Zone UI Multi-File Queue State Update File List Progress Tracking HTML Refresh Progress Bars CSRF Handling Event Dispatch ``` ### Komponenten 1. **Backend** (`src/Framework/LiveComponents/`) - `Controllers/LiveComponentController::handleUpload()` - Upload Route Handler - `LiveComponentHandler::handleUpload()` - Business Logic - `Contracts/SupportsFileUpload` - Interface für uploadbare Components - `ValueObjects/FileUploadProgress` - Progress Tracking VO - `ValueObjects/UploadedComponentFile` - File Metadata VO 2. **Frontend** (`resources/js/modules/livecomponent/`) - `ComponentFileUploader.js` - Core Upload Manager - `FileUploadWidget.js` - Pre-built UI Component - `DragDropZone` - Drag & Drop Handler - `FileValidator` - Client-Side Validation - `UploadProgress` - Progress Tracking 3. **Styling** (`resources/css/components/`) - `file-upload-widget.css` - Complete UI Styling ## Backend Implementation ### Interface: SupportsFileUpload LiveComponents, die Uploads unterstützen, müssen das `SupportsFileUpload` Interface implementieren: ```php isValidDocument($file)) { throw new \InvalidArgumentException('Invalid document type'); } // 2. Process uploaded file $savedPath = $this->saveFile($file); // 3. Update component state $this->uploadedFiles[] = [ 'name' => $file->getClientFilename(), 'path' => $savedPath, 'size' => $file->getSize(), 'uploaded_at' => time() ]; // 4. Dispatch events if needed if ($events) { $events->dispatch('file-uploaded', [ 'filename' => $file->getClientFilename(), 'path' => $savedPath ]); } // 5. Return updated component data return ComponentData::fromArray([ 'uploaded_files' => $this->uploadedFiles ]); } /** * Validate uploaded file */ public function validateUpload(UploadedFile $file): array { $errors = []; // File type validation $allowedTypes = $this->getAllowedMimeTypes(); if (!empty($allowedTypes) && !in_array($file->getClientMediaType(), $allowedTypes)) { $errors[] = "File type {$file->getClientMediaType()} is not allowed"; } // File size validation $maxSize = $this->getMaxFileSize(); if ($file->getSize() > $maxSize) { $errors[] = "File size exceeds maximum allowed size"; } // Custom validation if (!$this->isValidDocument($file)) { $errors[] = "Invalid document format"; } return $errors; } /** * Get allowed MIME types */ public function getAllowedMimeTypes(): array { return [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/png' ]; } /** * Get maximum file size in bytes */ public function getMaxFileSize(): int { return 10 * 1024 * 1024; // 10MB } private function isValidDocument(UploadedFile $file): bool { // Custom validation logic return true; } private function saveFile(UploadedFile $file): string { // Save file to storage $uploadDir = '/var/www/storage/uploads'; $filename = uniqid() . '_' . $file->getClientFilename(); $file->moveTo($uploadDir . '/' . $filename); return $filename; } } ``` ### Upload Endpoint **Route**: `POST /live-component/{id}/upload` **Request Format**: ``` Content-Type: multipart/form-data file: state: params: _csrf_token: ``` **Response Format**: ```json { "success": true, "html": "", "state": { "id": "document-upload:abc123", "component": "DocumentUploadComponent", "data": { "uploaded_files": [...] } }, "events": [...], "file": { "name": "document.pdf", "size": 1048576, "type": "application/pdf" } } ``` **Error Response**: ```json { "success": false, "error": "File validation failed", "errors": [ "File type application/octet-stream is not allowed" ] } ``` ## Frontend Implementation ### Quick Start: FileUploadWidget Der einfachste Weg, File Uploads zu implementieren, ist die Verwendung des `FileUploadWidget`: ```html
``` ### Advanced: ComponentFileUploader Für vollständige Kontrolle verwenden Sie direkt `ComponentFileUploader`: ```javascript import { ComponentFileUploader } from '/resources/js/modules/livecomponent/ComponentFileUploader.js'; const componentElement = document.querySelector('[data-live-id="document-upload:abc123"]'); const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const uploader = new ComponentFileUploader(componentElement, { // Configuration maxFileSize: 10 * 1024 * 1024, allowedMimeTypes: ['application/pdf', 'image/jpeg'], maxFiles: 10, autoUpload: true, multiple: true, maxConcurrentUploads: 2, // UI Elements dropZone: dropZone, fileInput: fileInput, // Callbacks onFileAdded: ({ fileId, file, progress }) => { console.log('File added:', file.name); // Update UI }, onUploadStart: ({ fileId, file }) => { console.log('Upload started:', file.name); }, onUploadProgress: ({ fileId, percentage, uploadSpeed, remainingTime }) => { console.log(`Upload progress: ${percentage}%`); // Update progress bar }, onUploadComplete: ({ fileId, file, response }) => { console.log('Upload complete:', file.name); // Handle success }, onUploadError: ({ fileId, file, error }) => { console.error('Upload failed:', error); // Handle error }, onAllUploadsComplete: ({ totalFiles, successCount, errorCount }) => { console.log(`All uploads complete: ${successCount}/${totalFiles} succeeded`); } }); // Programmatically add files uploader.addFiles([file1, file2, file3]); // Start uploads (if autoUpload is false) uploader.uploadAll(); // Cancel all uploads uploader.cancelAll(); // Get statistics const stats = uploader.getStats(); console.log(`Progress: ${stats.overallProgress}%`); ``` ### Custom Drag & Drop ```javascript import { DragDropZone } from '/resources/js/modules/livecomponent/ComponentFileUploader.js'; const dropZone = new DragDropZone(document.getElementById('drop-area'), { onFilesDropped: (files) => { console.log('Files dropped:', files); uploader.addFiles(files); }, onDragEnter: () => { console.log('Drag enter'); }, onDragLeave: () => { console.log('Drag leave'); } }); ``` ### Client-Side Validation ```javascript import { FileValidator } from '/resources/js/modules/livecomponent/ComponentFileUploader.js'; const validator = new FileValidator({ maxFileSize: 5 * 1024 * 1024, // 5MB allowedMimeTypes: ['image/jpeg', 'image/png'], allowedExtensions: ['jpg', 'jpeg', 'png'], minFileSize: 1024 // 1KB minimum }); // Validate single file const errors = validator.validate(file); if (errors.length > 0) { console.error('Validation errors:', errors); } // Quick validation if (!validator.isValid(file)) { console.error('File is not valid'); } ``` ## Configuration Options ### ComponentFileUploader Options ```javascript { // File Constraints maxFileSize: 10 * 1024 * 1024, // Maximum file size in bytes (default: 10MB) allowedMimeTypes: [], // Array of allowed MIME types (empty = allow all) allowedExtensions: [], // Array of allowed file extensions (empty = allow all) maxFiles: 10, // Maximum number of files (default: 10) // Upload Behavior autoUpload: true, // Auto-upload on file add (default: true) multiple: true, // Allow multiple files (default: true) maxConcurrentUploads: 2, // Max concurrent uploads (default: 2) // Endpoints endpoint: '/live-component/{id}/upload', // Upload endpoint (default: auto-detected) // UI Elements (optional) dropZone: HTMLElement, // Drop zone element fileInput: HTMLElement, // File input element // Callbacks onFileAdded: (data) => {}, // Called when file is added onFileRemoved: (data) => {}, // Called when file is removed onUploadStart: (data) => {}, // Called when upload starts onUploadProgress: (data) => {}, // Called during upload onUploadComplete: (data) => {}, // Called on upload success onUploadError: (data) => {}, // Called on upload error onAllUploadsComplete: (data) => {} // Called when all uploads are done } ``` ### FileUploadWidget Options ```javascript { // Inherits all ComponentFileUploader options, plus: // UI Configuration showPreviews: true, // Show image previews (default: true) showProgress: true, // Show progress bars (default: true) showFileList: true, // Show file list (default: true) // Text Configuration dropZoneText: 'Drag & drop files here or click to browse', browseButtonText: 'Browse Files', uploadButtonText: 'Upload All' } ``` ## Callback Data Structures ### onFileAdded ```javascript { fileId: 'unique-file-id', file: File, // Native File object progress: { fileId: 'unique-file-id', fileName: 'document.pdf', fileSize: 1048576, uploadedBytes: 0, percentage: 0, status: 'pending', error: null, uploadSpeed: 0, remainingTime: 0 } } ``` ### onUploadProgress ```javascript { fileId: 'unique-file-id', fileName: 'document.pdf', fileSize: 1048576, uploadedBytes: 524288, // Bytes uploaded so far percentage: 50, // Upload percentage (0-100) status: 'uploading', uploadSpeed: 1048576, // Bytes per second remainingTime: 0.5 // Seconds remaining } ``` ### onUploadComplete ```javascript { fileId: 'unique-file-id', file: File, response: { success: true, html: '', state: {...}, events: [...], file: { name: 'document.pdf', size: 1048576, type: 'application/pdf' } } } ``` ### onUploadError ```javascript { fileId: 'unique-file-id', file: File, error: 'File validation failed' } ``` ## Use Cases & Examples ### Basic Image Upload ```php final class ProfileImageUpload extends AbstractLiveComponent implements SupportsFileUpload { private ?string $profileImage = null; public function handleUpload(UploadedFile $file, ?ComponentEventDispatcher $events = null): ComponentData { // Save image $filename = $this->imageService->save($file, 'profiles'); // Update state $this->profileImage = $filename; return ComponentData::fromArray([ 'profile_image' => $this->profileImage ]); } public function validateUpload(UploadedFile $file): array { $errors = []; // Only allow images if (!str_starts_with($file->getClientMediaType(), 'image/')) { $errors[] = 'Only images are allowed'; } // Max 2MB if ($file->getSize() > 2 * 1024 * 1024) { $errors[] = 'Image must be smaller than 2MB'; } return $errors; } public function getAllowedMimeTypes(): array { return ['image/jpeg', 'image/png', 'image/webp']; } public function getMaxFileSize(): int { return 2 * 1024 * 1024; } } ``` ### Multi-Document Upload with Progress ```javascript import { ComponentFileUploader } from './ComponentFileUploader.js'; const uploader = new ComponentFileUploader(componentElement, { maxFiles: 20, maxFileSize: 50 * 1024 * 1024, // 50MB allowedMimeTypes: [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ], onUploadProgress: ({ fileId, percentage, uploadSpeed, remainingTime }) => { // Update progress UI const progressBar = document.querySelector(`[data-file-id="${fileId}"] .progress-bar`); progressBar.style.width = `${percentage}%`; const progressText = document.querySelector(`[data-file-id="${fileId}"] .progress-text`); progressText.textContent = `${percentage}% - ${formatSpeed(uploadSpeed)} - ${formatTime(remainingTime)} remaining`; }, onAllUploadsComplete: ({ successCount, errorCount }) => { if (errorCount === 0) { alert(`All ${successCount} files uploaded successfully!`); } else { alert(`${successCount} files uploaded, ${errorCount} failed`); } } }); ``` ### Custom Validation Messages ```javascript const validator = new FileValidator({ maxFileSize: 10 * 1024 * 1024, allowedMimeTypes: ['application/pdf'] }); const errors = validator.validate(file); // Translate errors const translatedErrors = errors.map(error => { if (error.includes('File size')) { return 'Dateigröße überschreitet das Maximum'; } else if (error.includes('File type')) { return 'Nur PDF-Dateien sind erlaubt'; } return error; }); if (translatedErrors.length > 0) { showErrorMessages(translatedErrors); } ``` ## Security Considerations ### CSRF Protection Das System verwendet automatisch CSRF-Tokens: ```javascript // CSRF token wird automatisch aus Component-Element gelesen const csrfToken = componentElement.dataset.csrfToken; // Und in jedem Upload-Request gesendet xhr.setRequestHeader('X-CSRF-Form-ID', csrfTokens.form_id); xhr.setRequestHeader('X-CSRF-Token', csrfTokens.token); ``` ### File Validation **Backend Validation ist PFLICHT**: ```php public function validateUpload(UploadedFile $file): array { $errors = []; // 1. MIME type validation $allowedTypes = $this->getAllowedMimeTypes(); if (!in_array($file->getClientMediaType(), $allowedTypes)) { $errors[] = 'Invalid file type'; } // 2. File extension validation $extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION); if (!in_array(strtolower($extension), ['pdf', 'jpg', 'png'])) { $errors[] = 'Invalid file extension'; } // 3. File size validation if ($file->getSize() > $this->getMaxFileSize()) { $errors[] = 'File too large'; } // 4. File content validation (check magic bytes) $finfo = new \finfo(FILEINFO_MIME_TYPE); $actualMimeType = $finfo->file($file->getStream()->getMetadata('uri')); if ($actualMimeType !== $file->getClientMediaType()) { $errors[] = 'File content does not match declared type'; } return $errors; } ``` ### Secure File Storage ```php private function saveFile(UploadedFile $file): string { // 1. Generate secure filename (no user input) $filename = bin2hex(random_bytes(16)) . '.' . $this->getSecureExtension($file); // 2. Store outside web root $uploadDir = '/var/www/storage/uploads'; // 3. Set restrictive permissions $file->moveTo($uploadDir . '/' . $filename); chmod($uploadDir . '/' . $filename, 0600); return $filename; } private function getSecureExtension(UploadedFile $file): string { // Use MIME type to determine extension, not user-provided extension return match ($file->getClientMediaType()) { 'application/pdf' => 'pdf', 'image/jpeg' => 'jpg', 'image/png' => 'png', default => 'bin' }; } ``` ## Performance Optimization ### Concurrent Uploads ```javascript const uploader = new ComponentFileUploader(componentElement, { maxConcurrentUploads: 3, // Upload 3 files simultaneously autoUpload: true }); ``` ### Chunked Uploads (Large Files) Für große Dateien sollten Chunked Uploads implementiert werden: ```javascript // TODO: Chunked upload support (geplant für v2.0) // Aktuell empfohlen: maxFileSize Limit für große Dateien ``` ### Progress Throttling ```javascript let lastProgressUpdate = 0; onUploadProgress: ({ fileId, percentage }) => { const now = Date.now(); // Only update UI every 100ms if (now - lastProgressUpdate > 100) { updateProgressBar(fileId, percentage); lastProgressUpdate = now; } } ``` ## Troubleshooting ### Problem: Upload schlägt fehl mit 413 (Request Entity Too Large) **Lösung**: Erhöhe PHP Upload Limits: ```ini ; php.ini upload_max_filesize = 50M post_max_size = 50M ``` ### Problem: CSRF Token fehlt **Lösung**: Stelle sicher, dass Component CSRF Token hat: ```html
...
``` ### Problem: Uploads sind langsam **Lösungen**: 1. Erhöhe `maxConcurrentUploads` 2. Implementiere Chunked Uploads 3. Komprimiere Dateien vor Upload (z.B. Bilder) 4. Verwende CDN für statische Assets ### Problem: Browser hängt bei vielen Dateien **Lösung**: Limitiere `maxFiles` und zeige Queue-Status: ```javascript const uploader = new ComponentFileUploader(componentElement, { maxFiles: 20, // Limit gleichzeitig ausgewählte Dateien maxConcurrentUploads: 2 // Aber nur 2 gleichzeitig hochladen }); ``` ### Problem: Drag & Drop funktioniert nicht **Lösung**: Prüfe Event Listener Setup: ```javascript // Stelle sicher, dass Drop Zone Element existiert const dropZone = document.getElementById('drop-zone'); if (!dropZone) { console.error('Drop zone element not found'); } // Prüfe CSS cursor dropZone.style.cursor = 'pointer'; ``` ## Best Practices ### 1. Immer Backend-Validierung ```php // ❌ Niemals nur Frontend-Validierung verlassen // ✅ Immer Backend validateUpload() implementieren ``` ### 2. Sichere Dateinamen ```php // ❌ User-provided filenames verwenden $file->moveTo('/uploads/' . $file->getClientFilename()); // ✅ Sichere, generierte Dateinamen $file->moveTo('/uploads/' . bin2hex(random_bytes(16)) . '.pdf'); ``` ### 3. Progress Feedback ```javascript // ✅ Immer Progress anzeigen für bessere UX onUploadProgress: ({ percentage }) => { updateProgressBar(percentage); updateStatusText(`Uploading: ${percentage}%`); } ``` ### 4. Error Handling ```javascript // ✅ User-freundliche Fehlermeldungen onUploadError: ({ error }) => { const userMessage = translateError(error); showNotification(userMessage, 'error'); logError(error); // Log für Debugging } ``` ### 5. File Size Limits ```php // ✅ Realistische Limits setzen public function getMaxFileSize(): int { // 10MB für Dokumente return 10 * 1024 * 1024; // 5MB für Bilder // return 5 * 1024 * 1024; } ``` ## Testing ### Unit Tests (Frontend) ```javascript import { FileValidator } from './ComponentFileUploader.js'; describe('FileValidator', () => { it('validates file size', () => { const validator = new FileValidator({ maxFileSize: 1024 * 1024 // 1MB }); const file = new File(['x'.repeat(2 * 1024 * 1024)], 'large.pdf'); const errors = validator.validate(file); expect(errors.length).toBeGreaterThan(0); expect(errors[0]).toContain('File size'); }); it('validates MIME type', () => { const validator = new FileValidator({ allowedMimeTypes: ['application/pdf'] }); const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }); const errors = validator.validate(file); expect(errors.length).toBeGreaterThan(0); expect(errors[0]).toContain('not allowed'); }); }); ``` ### Integration Tests (Backend) ```php it('handles file upload successfully', function () { $component = new DocumentUploadComponent(); $file = createUploadedFile('test.pdf', 'application/pdf', 1024); $result = $component->handleUpload($file); expect($result->toArray())->toHaveKey('uploaded_files'); }); it('validates file type', function () { $component = new DocumentUploadComponent(); $file = createUploadedFile('test.exe', 'application/octet-stream', 1024); $errors = $component->validateUpload($file); expect($errors)->not->toBeEmpty(); }); ``` ## Zusammenfassung Das File Upload System bietet: - ✅ **Einfache Integration** - FileUploadWidget für schnellen Start - ✅ **Flexible API** - ComponentFileUploader für vollständige Kontrolle - ✅ **Drag & Drop** - Intuitive Dateiauswahl - ✅ **Multi-File Support** - Mehrere Dateien gleichzeitig - ✅ **Progress Tracking** - Echtzeit-Fortschrittsanzeige - ✅ **Validation** - Client & Server-Side - ✅ **Security** - CSRF Protection, sichere Dateinamen - ✅ **Performance** - Concurrent Uploads, Queue Management - ✅ **Responsive Design** - Mobile-optimiert - ✅ **Dark Mode** - Automatische Theme-Unterstützung **Framework Integration**: - Value Objects für Type Safety - Event System Integration - Component State Management - CSRF Token Handling - Automatic HTML Refresh