Skip to content

File Uploads ​

Diffyne provides a simple, Livewire-like file upload system with temporary file storage and easy-to-use methods.

Overview ​

The file upload feature allows you to:

  • Upload files temporarily before form submission
  • Preview uploaded files before saving
  • Preserve original filenames (stored automatically with each upload)
  • Move files to permanent storage when ready (with option to use original filename)
  • Automatically clean up old temporary files

Basic File Upload ​

Component ​

php
use Diffyne\Attributes\Invokable;

class ProductForm extends Component
{
    public ?string $image = null;
    public bool $showAddImageModal = false;

    #[Invokable]
    public function addImage(): void
    {
        if ($this->image) {
            // Move temporary file to permanent storage
            // Option 1: Use original filename
            $imagePath = $this->moveTemporaryFile(
                $this->image,
                'products/' . $this->productId . '/',
                'public',
                true  // Use original filename
            );

            // Option 2: Use custom filename
            // $extension = pathinfo($this->image, PATHINFO_EXTENSION) ?: 'jpg';
            // $filename = uniqid() . '.' . $extension;
            // $imagePath = $this->moveTemporaryFile(
            //     $this->image,
            //     'products/' . $this->productId . '/' . $filename,
            //     'public'
            // );

            if ($imagePath) {
                ProductImage::create([
                    'image_path' => $imagePath,
                    'product_id' => $this->productId,
                ]);

                $this->showAddImageModal = false;
                $this->image = null;
            }
        }
    }
}

View ​

blade
<div>
    <input 
        type="file" 
        diff:model="image"
        accept="image/*">
    
    @if($image)
        <img 
            src="{{ $component->getTemporaryFilePreviewUrl($image) }}" 
            alt="Preview"
            class="w-32 h-32 object-cover">
    @endif
    
    <button diff:click="addImage">Save Image</button>
</div>

How It Works ​

  1. User selects file: The file is automatically uploaded to temporary storage
  2. Component receives identifier: The image property contains a temporary file identifier (e.g., "diffyne-abc123:filename.jpg")
  3. Preview available: Use getTemporaryFilePreviewUrl() to show a preview
  4. Move to permanent: Call moveTemporaryFile() when ready to save

Multiple File Uploads ​

Component ​

php
use Diffyne\Attributes\Invokable;

class GalleryForm extends Component
{
    public array $images = [];

    #[Invokable]
    public function saveGallery(): void
    {
        foreach ($this->images as $identifier) {
            $imagePath = $this->moveTemporaryFile(
                $identifier,
                'gallery/' . uniqid() . '.jpg',
                'public'
            );

            if ($imagePath) {
                GalleryImage::create([
                    'image_path' => $imagePath,
                ]);
            }
        }

        // Clean up temporary files
        $this->cleanupTemporaryFiles();
    }
}

View ​

blade
<div>
    <input 
        type="file" 
        diff:model="images"
        multiple
        accept="image/*">
    
    <div class="grid grid-cols-3 gap-4">
        @foreach($images as $image)
            <img 
                src="{{ $component->getTemporaryFilePreviewUrl($image) }}" 
                alt="Preview"
                class="w-full h-32 object-cover">
        @endforeach
    </div>
    
    <button diff:click="saveGallery">Save Gallery</button>
</div>

Available Methods ​

moveTemporaryFile() ​

Move a temporary file to permanent storage:

php
$permanentPath = $this->moveTemporaryFile(
    string $identifier,        // Temporary file identifier
    string $destinationPath,   // Destination path (e.g., 'avatars/user-123.jpg')
    ?string $disk = null,      // Storage disk (defaults to config)
    bool $useOriginalName = false // If true, uses original filename instead of destinationPath filename
);

Returns: Permanent file path or null on failure

Example:

php
// Use custom filename
$imagePath = $this->moveTemporaryFile(
    $this->avatar,
    'avatars/' . auth()->id() . '.jpg',
    'public'
);

// Use original filename
$imagePath = $this->moveTemporaryFile(
    $this->avatar,
    'avatars/',  // Directory only
    'public',
    true  // Use original filename
);
// Result: 'avatars/original-filename.jpg'

getTemporaryFilePreviewUrl() ​

Get a preview URL for a temporary file:

php
$previewUrl = $this->getTemporaryFilePreviewUrl(string $identifier);

Returns: URL to preview the temporary file

Example:

blade
<img src="{{ $component->getTemporaryFilePreviewUrl($image) }}" alt="Preview">

getTemporaryFileOriginalName() ​

Get the original filename for a temporary file:

php
$originalName = $this->getTemporaryFileOriginalName(string $identifier);

Returns: Original filename or null if not found

Example:

php
$originalName = $this->getTemporaryFileOriginalName($this->image);
// Returns: "my-document.pdf" or null

deleteTemporaryFile() ​

Delete a temporary file:

php
$deleted = $this->deleteTemporaryFile(string $identifier);

Returns: true if deleted, false otherwise

Example:

php
if ($this->image) {
    $this->deleteTemporaryFile($this->image);
    $this->image = null;
}

cleanupTemporaryFiles() ​

Clean up all old temporary files (static method):

php
$deletedCount = \Diffyne\Component::cleanupTemporaryFiles();

Returns: Number of files deleted

Note: This is typically run via the scheduled command, but can be called programmatically.

Configuration ​

Configure file uploads in config/diffyne.php:

php
'file_upload' => [
    // Storage disk for temporary files
    'disk' => env('DIFFYNE_FILE_DISK', 'local'),

    // Path for temporary file storage (relative to disk root)
    'temporary_path' => env('DIFFYNE_FILE_TEMP_PATH', 'diffyne/temp'),

    // Maximum file size in KB (default: 12MB)
    'max_size' => env('DIFFYNE_FILE_MAX_SIZE', 12288),

    // Allowed MIME types (null = all types allowed)
    // Example: ['image/jpeg', 'image/png', 'image/gif']
    'allowed_mimes' => env('DIFFYNE_FILE_MIMES') ? explode(',', env('DIFFYNE_FILE_MIMES')) : null,

    // Cleanup temporary files older than this many hours
    'cleanup_after_hours' => env('DIFFYNE_FILE_CLEANUP_HOURS', 24),
],

Environment Variables ​

Add these to your .env file:

bash
# File upload disk
DIFFYNE_FILE_DISK=local

# Temporary file path
DIFFYNE_FILE_TEMP_PATH=diffyne/temp

# Maximum file size in KB (12MB = 12288)
DIFFYNE_FILE_MAX_SIZE=12288

# Allowed MIME types (comma-separated)
DIFFYNE_FILE_MIMES=image/jpeg,image/png,image/gif

# Cleanup files older than 24 hours
DIFFYNE_FILE_CLEANUP_HOURS=24

Automatic Cleanup ​

Diffyne automatically cleans up old temporary files daily via a scheduled task. The cleanup runs based on the cleanup_after_hours configuration.

Manual Cleanup ​

You can also run cleanup manually:

bash
php artisan diffyne:cleanup-files

Programmatic Cleanup ​

php
use Diffyne\Component;

$deletedCount = Component::cleanupTemporaryFiles();

Validation ​

File Size Validation ​

Files are automatically validated against max_size configuration. If a file exceeds the limit, the upload will fail with a 422 error.

MIME Type Validation ​

If allowed_mimes is configured, only files with matching MIME types are allowed:

php
// config/diffyne.php
'allowed_mimes' => ['image/jpeg', 'image/png', 'image/gif'],

If a file doesn't match the allowed MIME types, the upload will fail with a 422 error.

Complete Example ​

Component ​

php
use App\Models\Product;
use App\Models\ProductImage;
use Diffyne\Attributes\Invokable;

class ProductImageUpload extends Component
{
    public int $productId;
    public array $images = [];
    public bool $showModal = false;

    public function mount(int $productId): void
    {
        $this->productId = $productId;
    }

    #[Invokable]
    public function openModal(): void
    {
        $this->showModal = true;
    }

    #[Invokable]
    public function closeModal(): void
    {
        $this->showModal = false;
        $this->images = [];
    }

    #[Invokable]
    public function saveImages(): void
    {
        foreach ($this->images as $identifier) {
            $extension = pathinfo($identifier, PATHINFO_EXTENSION) ?: 'jpg';
            $filename = uniqid() . '.' . $extension;
            
            $imagePath = $this->moveTemporaryFile(
                $identifier,
                'products/' . $this->productId . '/' . $filename,
                'public'
            );

            if ($imagePath) {
                ProductImage::create([
                    'image_path' => $imagePath,
                    'product_id' => $this->productId,
                ]);
            }
        }

        $this->closeModal();
        $this->dispatch('images-saved');
    }
}

View ​

blade
<div>
    <button diff:click="openModal">Add Images</button>

    @if($showModal)
        <div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
            <div class="bg-white p-6 rounded-lg max-w-2xl w-full">
                <h2 class="text-xl font-bold mb-4">Upload Product Images</h2>

                <input 
                    type="file" 
                    diff:model="images"
                    multiple
                    accept="image/*"
                    class="mb-4">

                @if(count($images) > 0)
                    <div class="grid grid-cols-3 gap-4 mb-4">
                        @foreach($images as $image)
                            <div class="relative">
                                <img 
                                    src="{{ $component->getTemporaryFilePreviewUrl($image) }}" 
                                    alt="Preview"
                                    class="w-full h-32 object-cover rounded">
                            </div>
                        @endforeach
                    </div>
                @endif

                <div class="flex gap-2">
                    <button 
                        diff:click="saveImages"
                        class="bg-blue-500 text-white px-4 py-2 rounded">
                        Save
                    </button>
                    <button 
                        diff:click="closeModal"
                        class="bg-gray-500 text-white px-4 py-2 rounded">
                        Cancel
                    </button>
                </div>
            </div>
        </div>
    @endif
</div>

Best Practices ​

1. Always Move Files to Permanent Storage ​

php
// ✅ Good - move to permanent storage
$path = $this->moveTemporaryFile($this->image, 'avatars/user.jpg', 'public');

// ❌ Bad - temporary files will be cleaned up
// Don't use the identifier directly in your database

2. Generate Unique Filenames or Use Original Names ​

php
// ✅ Good - unique filename
$extension = pathinfo($identifier, PATHINFO_EXTENSION) ?: 'jpg';
$filename = uniqid() . '.' . $extension;
$path = $this->moveTemporaryFile($identifier, "uploads/{$filename}", 'public');

// ✅ Good - use original filename (preserves user's filename)
$path = $this->moveTemporaryFile($identifier, 'uploads/', 'public', true);

// ❌ Bad - may overwrite existing files if using original name without directory
$path = $this->moveTemporaryFile($identifier, 'uploads/image.jpg', 'public', true);

3. Clean Up After Use ​

php
// ✅ Good - clean up after moving
foreach ($this->images as $identifier) {
    $path = $this->moveTemporaryFile($identifier, $destination, 'public');
    // File is automatically deleted from temp storage
}

4. Validate File Types ​

php
// Configure allowed MIME types in config
'allowed_mimes' => ['image/jpeg', 'image/png', 'image/gif'],

5. Set Appropriate File Size Limits ​

php
// config/diffyne.php
'max_size' => 5120, // 5MB for images
// or
'max_size' => 51200, // 50MB for documents

Troubleshooting ​

Files Not Uploading ​

  1. Check file size is within max_size limit
  2. Verify MIME type is allowed (if allowed_mimes is configured)
  3. Check storage disk permissions
  4. Verify temporary path exists and is writable

Preview Not Showing ​

  1. Ensure you're using getTemporaryFilePreviewUrl() method
  2. Check the preview route is accessible
  3. Verify file exists in temporary storage

Files Being Deleted ​

  1. Temporary files are automatically cleaned up after cleanup_after_hours
  2. Always move files to permanent storage before cleanup runs
  3. Increase cleanup_after_hours if needed

Next Steps ​

Learn more about file handling:

Released under the MIT License.