- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
24 KiB
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
-
Backend (
src/Framework/LiveComponents/)Controllers/LiveComponentController::handleUpload()- Upload Route HandlerLiveComponentHandler::handleUpload()- Business LogicContracts/SupportsFileUpload- Interface für uploadbare ComponentsValueObjects/FileUploadProgress- Progress Tracking VOValueObjects/UploadedComponentFile- File Metadata VO
-
Frontend (
resources/js/modules/livecomponent/)ComponentFileUploader.js- Core Upload ManagerFileUploadWidget.js- Pre-built UI ComponentDragDropZone- Drag & Drop HandlerFileValidator- Client-Side ValidationUploadProgress- Progress Tracking
-
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
use App\Framework\LiveComponents\Contracts\SupportsFileUpload;
use App\Framework\Http\UploadedFile;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
final class DocumentUploadComponent extends AbstractLiveComponent implements SupportsFileUpload
{
private array $uploadedFiles = [];
/**
* Handle file upload
*/
public function handleUpload(UploadedFile $file, ?ComponentEventDispatcher $events = null): ComponentData
{
// 1. Validate file (already done by framework, but you can add custom validation)
if (!$this->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: <binary file data>
state: <JSON string of current component state>
params: <JSON string of additional parameters>
_csrf_token: <CSRF token>
Response Format:
{
"success": true,
"html": "<updated component HTML>",
"state": {
"id": "document-upload:abc123",
"component": "DocumentUploadComponent",
"data": {
"uploaded_files": [...]
}
},
"events": [...],
"file": {
"name": "document.pdf",
"size": 1048576,
"type": "application/pdf"
}
}
Error Response:
{
"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:
<!-- In your LiveComponent template -->
<div data-live-component="document-upload:abc123">
<!-- Upload Widget Container -->
<div id="upload-widget"></div>
</div>
<script type="module">
import { FileUploadWidget } from '/resources/js/modules/livecomponent/FileUploadWidget.js';
// Initialize widget
const widget = new FileUploadWidget(
document.getElementById('upload-widget'),
{
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedMimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'],
maxFiles: 5,
multiple: true,
autoUpload: true,
showPreviews: true,
showProgress: true
}
);
</script>
Advanced: ComponentFileUploader
Für vollständige Kontrolle verwenden Sie direkt ComponentFileUploader:
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
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
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
{
// 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
{
// 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
{
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
{
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
{
fileId: 'unique-file-id',
file: File,
response: {
success: true,
html: '<updated component HTML>',
state: {...},
events: [...],
file: {
name: 'document.pdf',
size: 1048576,
type: 'application/pdf'
}
}
}
onUploadError
{
fileId: 'unique-file-id',
file: File,
error: 'File validation failed'
}
Use Cases & Examples
Basic Image Upload
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
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
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:
// 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:
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
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
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:
// TODO: Chunked upload support (geplant für v2.0)
// Aktuell empfohlen: maxFileSize Limit für große Dateien
Progress Throttling
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:
; php.ini
upload_max_filesize = 50M
post_max_size = 50M
Problem: CSRF Token fehlt
Lösung: Stelle sicher, dass Component CSRF Token hat:
<div data-live-component="upload:123" data-csrf-token="<?= $csrfToken ?>">
...
</div>
Problem: Uploads sind langsam
Lösungen:
- Erhöhe
maxConcurrentUploads - Implementiere Chunked Uploads
- Komprimiere Dateien vor Upload (z.B. Bilder)
- Verwende CDN für statische Assets
Problem: Browser hängt bei vielen Dateien
Lösung: Limitiere maxFiles und zeige Queue-Status:
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:
// 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
// ❌ Niemals nur Frontend-Validierung verlassen
// ✅ Immer Backend validateUpload() implementieren
2. Sichere Dateinamen
// ❌ User-provided filenames verwenden
$file->moveTo('/uploads/' . $file->getClientFilename());
// ✅ Sichere, generierte Dateinamen
$file->moveTo('/uploads/' . bin2hex(random_bytes(16)) . '.pdf');
3. Progress Feedback
// ✅ Immer Progress anzeigen für bessere UX
onUploadProgress: ({ percentage }) => {
updateProgressBar(percentage);
updateStatusText(`Uploading: ${percentage}%`);
}
4. Error Handling
// ✅ User-freundliche Fehlermeldungen
onUploadError: ({ error }) => {
const userMessage = translateError(error);
showNotification(userMessage, 'error');
logError(error); // Log für Debugging
}
5. File Size Limits
// ✅ 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)
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)
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