Blog

Relation manager with spatie media library

Jul 11, 2023
Sushant Aryal
Admin panel, Form builder

Introduction

In this article we will view how to add spatie medialibrary as a relation.

Installation

To get started install new Laravel project called filamentmedia

laravel new filamentmedia

New install filament use this command

cd filamentmedia
composer require filament/filament

Filament recommend adding this to your composer.json's post-update-cmd:

"post-update-cmd": [
// ...
"@php artisan filament:upgrade"
],

Create Model

php artisan make:model Adventure -m

Edit your migration for adventure table

<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('adventures', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->longText('description')->nullable();
$table->integer('price')->nullable();
$table->boolean('published')->default(1);
$table->timestamps();
});
}
 
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('adventures');
}
};

Now install filament/spatie-laravel-media-library-plugin

composer require filament/spatie-laravel-media-library-plugin

Now publish the migration to create the media table.

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"

Migrate

Migrate the table

php artisan migrate

Prepare your Adventure model for attaching media also add attributes to fillable for mass assignment.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
 
class Adventure extends Model implements HasMedia
{
use HasFactory;
use InteractsWithMedia;
 
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = ['name', 'description', 'price', 'published'];
}

Create Resource and Relation

Create Adventure Resource and Relation Manager for media

php artisan make:filament-resource AdventureResource Adventure
php artisan make:filament-relation-manager AdventureResource media name

Navigate to app\Filament\Resources\AdventureResource.php file. Define form and table.

public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Card::make()
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('price'),
Forms\Components\RichEditor::make('description')
->columnSpan('full'),
Forms\Components\Toggle::make('published')
->default(true)
->columnSpan('full'),
Forms\Components\SpatieMediaLibraryFileUpload::make('photos')
->collection('adventures')
->multiple()
->image(),
])
->columns(2),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('price'),
Tables\Columns\IconColumn::make('published')
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->date(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}

You must register the new media relation manager in your resource's getRelations() method:

public static function getRelations(): array
{
return [
RelationManagers\MediaRelationManager::class
];
}

Create View Field to preview image

resources\views\filament\forms\components\image-preview.blade.php

<x-dynamic-component
:component="$getFieldWrapperView()"
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-action="$getHintAction()"
:hint-color="$getHintColor()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div x-data="{ state: $wire.entangle('{{ $getStatePath() }}').defer }">
<img src="{{ $getState() }}" style="height: 185px;" class="object-cover" />
</div>
</x-dynamic-component>

Navigate to app\Filament\Resources\AdventureResource\RelationManagers\MediaRelationManager.php file.

public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Hidden::make('id'),
Forms\Components\ViewField::make('file_name')
->label('Image Preview')
->disabled()
->view('filament.forms.components.image-preview')
->afterStateHydrated(function ($component, $get, $state) {
$component->state(Storage::url($get('id') . '/' . $state));
})
->dehydrated(false),
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->columnSpan('full'),
]);
}

Change Title and Label

protected static ?string $title = 'Images';
 
protected static ?string $modelLabel = 'Images';

Disable CreateAction and add DeleteAction

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\ImageColumn::make('file_name')
->label('Image')
->getStateUsing(function ($record) {
return $record->getFullUrl();
}),
Tables\Columns\TextColumn::make('name'),
])
->filters([
//
])
->headerActions([
// Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->reorderable('order_column')
->defaultSort('order_column');
}

Disable Image preview on edit form. To do this you add this to SpatieMediaLibraryFileUpload

Forms\Components\SpatieMediaLibraryFileUpload::make('photos')
->loadStateFromRelationshipsUsing(fn ($component) => $component->state([]))

This will not load the previously added media to file upload.

To prevent automatic deletion of the previous added file.

Forms\Components\SpatieMediaLibraryFileUpload::make('photos')
->saveRelationshipsUsing(fn ($component) => $component->saveUploadedFiles())

Now the Form Component will look like this

Forms\Components\SpatieMediaLibraryFileUpload::make('photos')
->collection('adventures')
->multiple()
->image()
->loadStateFromRelationshipsUsing(fn ($component) => $component->state([]))
->saveRelationshipsUsing(fn ($component) => $component->saveUploadedFiles()),

Refresh

To refresh the page after the submission navigate to app\Filament\Resources\AdventureResource\Pages\EditAdventure.php file.

<?php
 
namespace App\Filament\Resources\AdventureResource\Pages;
 
use App\Filament\Resources\AdventureResource;
use Filament\Pages\Actions;
use Filament\Resources\Pages\EditRecord;
 
class EditAdventure extends EditRecord
{
protected static string $resource = AdventureResource::class;
 
protected function getActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
 
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('edit', ['record' => $this->record]);
}
}
avatar

Hi! I'm trying to add an action and a bulkAction to the table to download files. When it downloads a file, it works correctly, but when it downloads selected files using the bulkAction, it doesn't display the actual file name inside the ZIP, but rather the name under which it's stored. Is there a way to create the ZIP with the real file names?

->actions([
Tables\Actions\Action::make('download')
->icon('heroicon-o-cloud-arrow-down')
->action(function (Model $record) {
return Storage::download($record->custom_properties['path'] . $record->id .'/' . $record->file_name, $record->name);
})
->button()
->color(Color::Green),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
 
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\BulkAction::make('downloadSelected')
->label('Download')
->icon('heroicon-o-cloud-arrow-down')
->deselectRecordsAfterCompletion()
->action(function (Collection $selectedRecords) {
return MediaStream::create('you-file.zip')->addMedia($selectedRecords);
})
->color(Color::Green),
Tables\Actions\DeleteBulkAction::make(),
]),
])