tigusigalpa/filament-fileoutput

A Laravel Filament plugin for displaying uploaded files (including private files)

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/tigusigalpa/filament-fileoutput

v1.1.12 2026-01-21 03:13 UTC

This package is auto-updated.

Last update: 2026-01-21 03:17:13 UTC


README

📁 Filament FileOutput

Display uploaded files beautifully in Filament forms

Laravel Filament FileOutput

Latest Version Total Downloads License

A powerful Laravel Filament plugin for displaying uploaded files with support for private storage, **multiple files **, and smart deletion.

InstallationQuick StartFeaturesExamplesAPI Reference

✨ Features

🎯 Core Features

  • 📦 Any Storage Disk - public, private, S3, etc.
  • 🖼️ Smart Preview - automatic image detection
  • 📥 Download Links - for non-image files
  • 🔒 Private Files - temporary signed URLs
  • 🗑️ Delete Action - with callback support

🚀 Advanced Features

  • 📚 Multiple Files - array support out of the box
  • 🎨 Filament Design - fully styled components
  • 🌙 Dark Mode - complete theme support
  • Auto State Sync - smart form updates
  • 🔐 Conditional Actions - permission-based controls

📦 Installation

Install via Composer:

composer require tigusigalpa/filament-fileoutput

🎉 That's it! The package auto-registers its service provider.

🚀 Quick Start

Basic Usage

use Tigusigalpa\FileOutput\FileOutput;

FileOutput::make('file_preview')
    ->field('file_path')
    ->label('Current File')

With Private Storage

FileOutput::make('document_preview')
    ->field('document')
    ->disk('private')
    ->label('Private Document')

Complete Form Example

use Filament\Forms\Components\FileUpload;
use Tigusigalpa\FileOutput\FileOutput;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            FileUpload::make('document')
                ->disk('private')
                ->directory('documents')
                ->label('Upload Document'),
                
            FileOutput::make('document_preview')
                ->field('document')
                ->disk('private')
                ->label('Current Document')
                ->onDelete(function ($record, $filePath, $disk) {
                    Storage::disk($disk)->delete($filePath);
                }),
        ]);
}

💡 Auto-Sync: When a file is deleted, the linked field state is automatically cleared!

📚 Examples

🎯 Using path() Method

Direct Path

FileOutput::make('contract')
    ->path('contracts/2024/contract-001.pdf')
    ->disk('private')
    ->label('Contract')

Dynamic Path with Closure

FileOutput::make('user_avatar')
    ->path(fn ($record) => 'avatars/' . $record->user_id . '.jpg')
    ->disk('public')
    ->label('User Avatar')

Public URL

FileOutput::make('external_file')
    ->path('https://example.com/files/document.pdf')
    ->label('External Document')

Conditional Logic

FileOutput::make('file_preview')
    ->path(function ($record) {
        if ($record->file_type === 'contract') {
            return 'contracts/' . $record->file_name;
        }
        return 'documents/' . $record->file_name;
    })
    ->disk('private')

Multiple Files with Custom Labels

FileOutput::make('documents')
    ->path([
        'contracts/main-contract.pdf' => 'Main Contract Document',
        'contracts/addendum-1.pdf' => 'Addendum #1',
        'contracts/addendum-2.pdf' => 'Addendum #2',
        'invoices/invoice-2024.pdf' => 'Invoice 2024'
    ])
    ->disk('private')
    ->label('Contract Documents')

Dynamic Labels with Closure

FileOutput::make('user_documents')
    ->path(function ($record) {
        return [
            $record->contract_path => 'Contract for ' . $record->client_name,
            $record->invoice_path => 'Invoice #' . $record->invoice_number,
            $record->receipt_path => 'Payment Receipt'
        ];
    })
    ->disk('private')

📦 Multiple Files

Basic Multiple Files

FileOutput::make('attachments_preview')
    ->field('attachments')  // Array of file paths
    ->disk('private')
    ->label('Attachments')

Complete Multiple Files Example

FileUpload::make('attachments')
    ->disk('private')
    ->directory('attachments')
    ->multiple()
    ->maxFiles(10)
    ->acceptedFileTypes(['application/pdf', 'image/*']),
    
FileOutput::make('attachments_preview')
    ->field('attachments')
    ->disk('private')
    ->label('Current Attachments')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
        // Auto-updates array - removes only this file
    })

Multiple Files via path() with Array

FileOutput::make('documents')
    ->path(['contracts/file1.pdf', 'contracts/file2.pdf'])
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
    })

Multiple Files with Closure

FileOutput::make('photos_preview')
    ->path(fn ($record) => $record->photos ?? [])
    ->field('photos')  // ⚠️ Important for auto-sync
    ->disk('public')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
        
        // Update database
        $photos = array_values(array_filter(
            $this->record->photos ?? [],
            fn($p) => $p !== $filePath
        ));
        $this->record->update(['photos' => $photos]);
    })

⚠️ Important: When using path() with arrays, also specify field() to enable automatic state updates!

🗑️ Delete Button Control

Hide Delete Button

FileOutput::make('document')
    ->field('document')
    ->disk('private')
    ->onDelete(fn ($record, $filePath, $disk) => Storage::disk($disk)->delete($filePath))
    ->hideDeleteButton()

Conditional Delete Button

// Hide for locked records
FileOutput::make('file')
    ->field('file_path')
    ->onDelete(fn ($record, $filePath, $disk) => Storage::disk($disk)->delete($filePath))
    ->hideDeleteButton(fn ($record) => $record->is_locked)

Show Only for Admins

FileOutput::make('sensitive_doc')
    ->field('document')
    ->disk('private')
    ->onDelete(fn ($record, $filePath, $disk) => Storage::disk($disk)->delete($filePath))
    ->showDeleteButton(fn () => auth()->user()->isAdmin())

Complex Conditions

FileOutput::make('invoice')
    ->field('invoice_file')
    ->onDelete(fn ($record, $filePath, $disk) => Storage::disk($disk)->delete($filePath))
    ->showDeleteButton(function ($record) {
        $isOwnerOrAdmin = auth()->user()->isAdmin() || 
                          auth()->id() === $record->user_id;
        $isNotPaid = $record->status !== 'paid';
        
        return $isOwnerOrAdmin && $isNotPaid;
    })

🖼️ Image Gallery Example

FileOutput::make('product_images')
    ->field('images')
    ->disk('public')
    ->label('Product Gallery')
    ->onDelete(function ($record, $filePath, $disk) {
        // Delete original
        Storage::disk($disk)->delete($filePath);
        
        // Delete thumbnails
        $directory = dirname($filePath);
        $filename = basename($filePath);
        Storage::disk($disk)->delete($directory . '/thumbs/' . $filename);
        
        // Update record
        $record->increment('images_deleted_count');
    })

☁️ S3 Storage Example

FileOutput::make('backup')
    ->field('backup_path')
    ->disk('s3')
    ->label('Backup File')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
        
        Log::info('Backup deleted', [
            'path' => $filePath,
            'user' => auth()->id(),
            'record_id' => $record?->id,
        ]);
    })

🔔 With Notifications

FileOutput::make('document')
    ->field('document')
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        try {
            if (Storage::disk($disk)->exists($filePath)) {
                Storage::disk($disk)->delete($filePath);
                
                Notification::make()
                    ->title('File deleted successfully')
                    ->success()
                    ->send();
            } else {
                throw new \Exception('File not found');
            }
        } catch (\Exception $e) {
            Notification::make()
                ->title('Error deleting file')
                ->body($e->getMessage())
                ->danger()
                ->send();
                
            Log::error('File deletion error', [
                'path' => $filePath,
                'record_id' => $record?->id,
                'error' => $e->getMessage(),
            ]);
        }
    })

💬 Custom Delete Confirmation

Basic Custom Confirmation

FileOutput::make('contract')
    ->field('contract_file')
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
    })
    ->deleteConfirmationTitle('Delete Contract?')
    ->deleteConfirmationDescription('This action cannot be undone. The contract file will be permanently deleted.')

Dynamic Confirmation Message

FileOutput::make('document')
    ->field('document_path')
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
    })
    ->deleteConfirmationTitle(fn ($record) => "Delete {$record->name}?")
    ->deleteConfirmationDescription(fn ($record) => "Are you sure you want to delete the document for {$record->client_name}? This action cannot be undone.")

Confirmation for Critical Files

FileOutput::make('sensitive_document')
    ->field('document')
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
        Log::warning('Sensitive document deleted', [
            'path' => $filePath,
            'record_id' => $record?->id
        ]);
    })
    ->deleteLabel('Remove Document')
    ->deleteConfirmationTitle('⚠️ Delete Sensitive Document?')
    ->deleteConfirmationDescription('WARNING: This is a sensitive document. Deletion will be logged and cannot be undone. Please confirm you want to proceed.')

Custom Button Label

FileOutput::make('attachment')
    ->field('attachment_path')
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
    })
    ->deleteLabel('Remove Attachment')

🎭 Conditional Visibility

FileOutput::make('contract')
    ->field('contract_file')
    ->disk('private')
    ->visible(fn ($record) => $record?->contract_file !== null)
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
        $record->update([
            'contract_file' => null,
            'contract_signed_at' => null,
        ]);
    })

🎨 Custom Empty State

Basic Custom Message

FileOutput::make('document')
    ->field('document_path')
    ->disk('private')
    ->emptyState('No document has been uploaded yet')

Dynamic Empty State

FileOutput::make('contract')
    ->field('contract_file')
    ->disk('private')
    ->emptyState(fn ($record) => "No contract uploaded for {$record->client_name}")

Multilingual Empty State

FileOutput::make('invoice')
    ->field('invoice_path')
    ->disk('private')
    ->emptyState(__('invoices.no_file_uploaded'))

📝 File Descriptions

Basic Description

FileOutput::make('contract')
    ->field('contract_file')
    ->disk('private')
    ->description('Contract signed on 2024-01-15')

Dynamic Description

FileOutput::make('document')
    ->field('document_path')
    ->disk('private')
    ->description(fn ($record) => "Uploaded by {$record->user->name} on {$record->created_at->format('Y-m-d')}")

Multiple Files with Descriptions (Indexed Array)

FileOutput::make('attachments')
    ->field('attachments')
    ->disk('private')
    ->description([
        'Main contract document',
        'Signed addendum',
        'Supporting documentation'
    ])

Multiple Files with Descriptions (Associative Array)

FileOutput::make('documents')
    ->path([
        'contracts/contract.pdf' => 'Main Contract',
        'contracts/addendum.pdf' => 'Addendum',
        'invoices/invoice.pdf' => 'Invoice'
    ])
    ->disk('private')
    ->description([
        'contracts/contract.pdf' => 'Signed on 2024-01-15',
        'contracts/addendum.pdf' => 'Additional terms and conditions',
        'invoices/invoice.pdf' => 'Payment due: 2024-02-01'
    ])

Dynamic Multiple Descriptions

FileOutput::make('certificates')
    ->field('certificates')
    ->disk('private')
    ->description(function ($state) {
        if (is_array($state)) {
            return array_map(function($path, $index) {
                return 'Certificate #' . ($index + 1) . ' - ' . basename($path);
            }, $state, array_keys($state));
        }
        return null;
    })

Description Based on Other Fields

FileOutput::make('invoice')
    ->field('invoice_file')
    ->disk('private')
    ->description(function ($record, $get) {
        $status = $get('payment_status');
        $date = $record->invoice_date->format('d.m.Y');
        return "Invoice from {$date} - Status: {$status}";
    })

📊 Advanced Multiple Files

Limit Deletion (Keep at Least One)

FileOutput::make('documents')
    ->field('documents')
    ->disk('private')
    ->onDelete(fn ($filePath, $disk) => Storage::disk($disk)->delete($filePath))
    ->showDeleteButton(function ($record) {
        $documents = $record->documents ?? [];
        return is_array($documents) && count($documents) > 1;
    })

With Database Counter Update

FileOutput::make('certificates')
    ->field('certificates')
    ->disk('private')
    ->onDelete(function ($record, $filePath, $disk) {
        Storage::disk($disk)->delete($filePath);
        
        $certificates = array_values(array_filter(
            $this->record->certificates ?? [],
            fn($cert) => $cert !== $filePath
        ));
        
        $this->record->update([
            'certificates' => empty($certificates) ? null : $certificates,
            'certificates_count' => count($certificates),
        ]);
        
        Notification::make()
            ->title('Certificate deleted')
            ->body('Remaining: ' . count($certificates))
            ->success()
            ->send();
    })

📖 API Reference

Methods

field(string $fieldName)

Required (if path not specified)

Specifies the field name to read the file path from.

FileOutput::make('preview')->field('file_path')

path(string|array|Closure $path)

Required (if field not specified)

Specifies the direct path to the file. Supports:

  • String: Direct file path
  • Array: Multiple file paths
  • Associative Array: File paths with custom labels (path => label)
  • Closure: Dynamic path (can return string or array)
  • Public URL: External file URL
// String
->path('documents/report.pdf')

// Array (indexed)
->path(['file1.pdf', 'file2.pdf'])

// Associative Array with custom labels
->path([
    'contracts/contract-2024.pdf' => 'Main Contract 2024',
    'contracts/addendum.pdf' => 'Contract Addendum',
    'documents/invoice.pdf' => 'Invoice #12345'
])

// Closure (single)
->path(fn ($record) => 'users/' . $record->user_id . '/avatar.jpg')

// Closure (multiple)
->path(fn ($record) => $record->files ?? [])

// Closure with labels
->path(fn ($record) => [
    $record->contract_path => 'Contract for ' . $record->name,
    $record->invoice_path => 'Invoice #' . $record->invoice_number
])

// Public URL
->path('https://example.com/file.pdf')

💡 File Labels: Use associative arrays to set custom labels for download links. If no label is provided, the default "Download File" text will be used.

💡 Priority: path() takes priority for reading, but field() is still used for auto-updates.

⚠️ Best Practice: When using path() with arrays, also specify field() for automatic state updates:

->path(fn ($record) => $record->files ?? [])
->field('files')  // Enables auto-sync

disk(string $disk)

Optional

Specifies the storage disk (public, private, s3, etc.).

->disk('private')

onDelete(Closure $callback)

Optional

Adds delete button with callback. Receives $record, $filePath, and $disk parameters.

->onDelete(function ($record, $filePath, $disk) {
    Storage::disk($disk)->delete($filePath);
})

// With record usage
->onDelete(function ($record, $filePath, $disk) {
    Storage::disk($disk)->delete($filePath);
    
    // Update record
    $record->update(['file_path' => null]);
    
    // Log deletion
    Log::info('File deleted', [
        'file' => $filePath,
        'user' => auth()->id(),
        'record_id' => $record->id
    ]);
})

Parameters:

  • $record - Current model/record instance (can be null)
  • $filePath - Path to the file being deleted
  • $disk - Storage disk name

🔄 Auto-Sync: Field state is automatically cleared after deletion (if field() is specified).

hideDeleteButton(bool|Closure $condition = true)

Optional

Hides the delete button.

// Always hide
->hideDeleteButton()

// Conditional
->hideDeleteButton(fn ($record) => $record->is_locked)

showDeleteButton(bool|Closure $condition = true)

Optional

Shows the delete button (default). Useful for conditional display.

->showDeleteButton(fn () => auth()->user()->isAdmin())

deleteLabel(string|Closure $label)

Optional

Sets a custom label for the delete button.

// Static label
->deleteLabel('Remove')

// Dynamic label
->deleteLabel(fn () => 'Remove File')

emptyState(string|Closure $message)

Optional

Sets a custom message to display when no file is uploaded. Default: "No file uploaded".

// Static message
->emptyState('No document available')

// Dynamic message
->emptyState(fn ($record) => "No file uploaded for {$record->name}")

// Multilingual
->emptyState(__('custom.no_file'))

deleteConfirmationTitle(string|Closure $title)

Optional

Sets a custom title for the delete confirmation modal.

// Static title
->deleteConfirmationTitle('Delete File?')

// Dynamic title
->deleteConfirmationTitle(fn () => 'Delete this document?')

deleteConfirmationDescription(string|Closure $description)

Optional

Sets a custom description for the delete confirmation modal.

// Static description
->deleteConfirmationDescription('This action cannot be undone.')

// Dynamic description
->deleteConfirmationDescription(fn ($record) => "Are you sure you want to delete {$record->name}?")

description(string|array|Closure $description)

Optional

Adds a description text for the file(s). Supports:

  • String: Single description for one file
  • Indexed Array: Multiple descriptions (one per file) for multiple files
  • Associative Array: Descriptions mapped by file path (path => description)
  • Closure: Dynamic description based on record/state
// String (single file)
->description('Contract signed on 2024-01-15')

// Indexed Array (multiple files)
->description([
    'First document description',
    'Second document description',
    'Third document description'
])

// Associative Array (path => description)
->description([
    'contracts/contract.pdf' => 'Main contract signed on 2024-01-15',
    'contracts/addendum.pdf' => 'Additional terms',
    'invoices/invoice.pdf' => 'Payment due: 2024-02-01'
])

// Closure with record
->description(fn ($record) => "Document for: {$record->name}")

// Closure with state
->description(function ($state) {
    if (is_array($state)) {
        return array_map(fn($path) => 'File: ' . basename($path), $state);
    }
    return 'Single file uploaded';
})

// Closure with multiple parameters
->description(function ($record, $state, $get) {
    $type = $get('document_type');
    return "Document type: {$type} for {$record->name}";
})

Available Closure Parameters:

  • $record - Current model/record
  • $state - Current field value (file path or array of paths)
  • $component - The FileOutput component instance
  • $get - Function to get other field values: $get('field_name')
  • $set - Function to set other field values: $set('field_name', $value)

💡 Tip: For multiple files, you can use either indexed arrays (matched by position) or associative arrays (matched by file path). Associative arrays are recommended when using path() with custom labels.

🔧 How It Works

🖼️ Images

Automatic detection of image files (jpg, jpeg, png, gif, bmp, svg, webp, ico) with preview display.

📄 Documents

Download links for non-image files with filename display.

🔒 Private Files

Temporary signed URLs or custom download routes for secure access.

🌐 Public URLs

Direct display of external file URLs.

📚 Multiple Files

Automatic array detection with individual previews and delete buttons.

🔄 Auto-Sync

Smart state updates - removes only deleted files from arrays.

⚙️ Requirements

  • PHP: 8.1 or higher
  • Laravel: 10.x, 11.x, or 12.x
  • Filament: 3.x or 4.x

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📝 License

This package is open-sourced software licensed under the MIT license.

👨‍💻 Author

Igor Sazonov

🔗 Links

If you find this package helpful, please consider giving it a ⭐ on GitHub!

Made with ❤️ for the Filament community