feat: implement Spatie Media Library integration

with CRUD operations and media management UI
This commit is contained in:
jon brookes 2026-01-02 18:59:24 +00:00
parent d40b87438d
commit 02884d4e2b
11 changed files with 499 additions and 4 deletions

View file

@ -0,0 +1,58 @@
<?php
namespace App\Filament\Resources\Media;
use App\Filament\Resources\Media\Pages\CreateMedia;
use App\Filament\Resources\Media\Pages\EditMedia;
use App\Filament\Resources\Media\Pages\ListMedia;
use App\Filament\Resources\Media\Pages\ViewMedia;
use App\Filament\Resources\Media\Schemas\MediaForm;
use App\Filament\Resources\Media\Schemas\MediaInfolist;
use App\Filament\Resources\Media\Tables\MediaTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class MediaResource extends Resource
{
protected static ?string $model = Media::class;
protected static ?string $recordTitleAttribute = 'file_name';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return MediaForm::configure($schema);
}
public static function infolist(Schema $schema): Schema
{
return MediaInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return MediaTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListMedia::route('/'),
'create' => CreateMedia::route('/create'),
'view' => ViewMedia::route('/{record}'),
'edit' => EditMedia::route('/{record}/edit'),
];
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class CreateMedia extends CreateRecord
{
protected static string $resource = MediaResource::class;
protected ?string $uploadedFile = null;
protected function mutateFormDataBeforeCreate(array $data): array
{
// Extract the file data before creating the media record
$file = $data['file'] ?? null;
unset($data['file']);
// Store file path for later
$this->uploadedFile = $file;
// Set required fields for Media model
$data['model_type'] = $data['model_type'] ?? 'temp';
$data['model_id'] = $data['model_id'] ?? 0;
$data['collection_name'] = $data['collection_name'] ?? 'default';
$data['disk'] = $data['disk'] ?? 'public';
$data['file_name'] = $file ? basename($file) : '';
$data['mime_type'] = $file && Storage::disk('public')->exists($file)
? Storage::disk('public')->mimeType($file)
: 'application/octet-stream';
$data['size'] = $file && Storage::disk('public')->exists($file)
? Storage::disk('public')->size($file)
: 0;
$data['manipulations'] = [];
$data['custom_properties'] = [];
$data['generated_conversions'] = [];
$data['responsive_images'] = [];
return $data;
}
protected function afterCreate(): void
{
if ($this->uploadedFile && $this->record) {
$disk = Storage::disk('public');
// Create the directory for this media ID (Spatie structure: {id}/{filename})
$mediaDirectory = (string) $this->record->id;
$disk->makeDirectory($mediaDirectory);
// Move file from temporary upload location to Spatie's expected location
if ($disk->exists($this->uploadedFile)) {
$newPath = $mediaDirectory.'/'.$this->record->file_name;
$disk->move($this->uploadedFile, $newPath);
}
}
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Storage;
class EditMedia extends EditRecord
{
protected static string $resource = MediaResource::class;
protected ?string $uploadedFile = null;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
// Extract the file data if a new file was uploaded
$file = $data['file'] ?? null;
unset($data['file']);
// Only update file-related fields if a new file was uploaded
if ($file && $file !== $this->record->getPathRelativeToRoot()) {
$this->uploadedFile = $file;
// Keep the original file_name to prevent breaking existing references
// $data['file_name'] is not updated - we preserve the original filename
$data['mime_type'] = Storage::disk('public')->exists($file)
? Storage::disk('public')->mimeType($file)
: 'application/octet-stream';
$data['size'] = Storage::disk('public')->exists($file)
? Storage::disk('public')->size($file)
: 0;
}
return $data;
}
protected function afterSave(): void
{
if ($this->uploadedFile && $this->record) {
$disk = Storage::disk('public');
$mediaDirectory = (string) $this->record->id;
// Delete old file if it exists
$oldPath = $mediaDirectory.'/'.$this->record->getOriginal('file_name');
if ($disk->exists($oldPath)) {
$disk->delete($oldPath);
}
// Move new file to Spatie's expected location using the original filename
if ($disk->exists($this->uploadedFile)) {
$disk->makeDirectory($mediaDirectory);
// Use the original file_name to preserve existing references
$newPath = $mediaDirectory.'/'.$this->record->file_name;
$disk->move($this->uploadedFile, $newPath);
}
// Redirect to the same page to refresh the form state
$this->redirect(static::getUrl(['record' => $this->record]), navigate: true);
}
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListMedia extends ListRecords
{
protected static string $resource = MediaResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewMedia extends ViewRecord
{
protected static string $resource = MediaResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make(),
];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Filament\Resources\Media\Schemas;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
class MediaForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('collection_name')
->default('default')
->required()
->maxLength(255),
Hidden::make('disk')
->default('public'),
FileUpload::make('file')
->label('File')
->imageEditor()
->imageEditorAspectRatios([
'16:9',
'4:3',
'1:1',
])
->columnSpanFull()
->disk('public')
->directory('media')
->visibility('public')
->acceptedFileTypes(['image/*', 'application/pdf'])
->maxSize(10240)
->required(fn ($context) => $context === 'create')
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
if (! $record) {
return;
}
$media = $record;
if (! $media instanceof SpatieMedia) {
return;
}
// Construct the correct path: {media_id}/{filename}
$path = $media->id.'/'.$media->file_name;
$component->state($path);
}),
]);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Filament\Resources\Media\Schemas;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
class MediaInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
ImageEntry::make('file_name')
->label('Preview')
->getStateUsing(fn ($record) => $record->getUrl())
->visible(fn ($record) => $record->mime_type && str_starts_with($record->mime_type, 'image/')),
TextEntry::make('name'),
TextEntry::make('file_name'),
TextEntry::make('mime_type'),
TextEntry::make('collection_name'),
TextEntry::make('size')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'),
TextEntry::make('model_type')
->label('Attached to Model'),
TextEntry::make('model_id'),
TextEntry::make('custom_properties')
->formatStateUsing(fn ($state) => json_encode($state, JSON_PRETTY_PRINT)),
TextEntry::make('created_at')
->dateTime(),
TextEntry::make('updated_at')
->dateTime(),
]);
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace App\Filament\Resources\Media\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class MediaTable
{
public static function configure(Table $table): Table
{
return $table
->modifyQueryUsing(fn ($query) => $query->where('collection_name', '!=', 'avatars'))
->columns([
ImageColumn::make('url')
->label('Preview')
->getStateUsing(fn ($record) =>
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
// fall back to Spatie's getUrl() when no stored_path exists.
($record->getCustomProperty('stored_path'))
? Storage::url($record->getCustomProperty('stored_path'))
: $record->getUrl()
)
->height(40)
->width(40),
TextColumn::make('name')
->searchable(),
TextColumn::make('file_name')
->searchable(),
TextColumn::make('collection_name')
->badge(),
TextColumn::make('mime_type'),
TextColumn::make('size')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'),
TextColumn::make('created_at')
->dateTime(),
])
->filters([
SelectFilter::make('collection_name')
->options([
'images' => 'Images',
'documents' => 'Documents',
]),
])
->recordActions([
EditAction::make(),
DeleteAction::make()
->action(function (Media $record) {
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
$stored = $record->getCustomProperty('stored_path');
if ($stored) {
Storage::disk($record->disk)->delete($stored);
} else {
Storage::disk($record->disk)->delete($record->getPath());
}
$record->delete();
}),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->action(function (Collection $records) {
$records->each(function (Media $record) {
$stored = $record->getCustomProperty('stored_path');
if ($stored) {
Storage::disk($record->disk)->delete($stored);
} else {
Storage::disk($record->disk)->delete($record->getPath());
}
$record->delete();
});
}),
]),
]);
}
}