├── README.md ├── composer.json └── src ├── Forms └── Components │ ├── RichEditor │ └── FileAttachmentProviders │ │ └── SpatieMediaLibraryFileAttachmentProvider.php │ └── SpatieMediaLibraryFileUpload.php ├── Infolists └── Components │ └── SpatieMediaLibraryImageEntry.php ├── SpatieLaravelMediaLibraryPlugin └── Collections │ └── AllMediaCollections.php ├── Support └── Concerns │ └── HasMediaFilter.php └── Tables └── Columns └── SpatieMediaLibraryImageColumn.php /README.md: -------------------------------------------------------------------------------- 1 | # Filament Spatie Media Library Plugin 2 | 3 | ## Installation 4 | 5 | Install the plugin with Composer: 6 | 7 | ```bash 8 | composer require filament/spatie-laravel-media-library-plugin:"^4.0" -W 9 | ``` 10 | 11 | If you haven't already done so, you need to publish the migration to create the media table: 12 | 13 | ```bash 14 | php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations" 15 | ``` 16 | 17 | Run the migrations: 18 | 19 | ```bash 20 | php artisan migrate 21 | ``` 22 | 23 | You must also [prepare your Eloquent model](https://spatie.be/docs/laravel-medialibrary/basic-usage/preparing-your-model) for attaching media. 24 | 25 | > For more information, check out [Spatie's documentation](https://spatie.be/docs/laravel-medialibrary). 26 | 27 | ## Form component 28 | 29 | You may use the field in the same way as the [original file upload](https://filamentphp.com/docs/forms/file-upload) field: 30 | 31 | ```php 32 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 33 | 34 | SpatieMediaLibraryFileUpload::make('avatar') 35 | ``` 36 | 37 | The media library file upload supports all the customization options of the [original file upload component](https://filamentphp.com/docs/forms/file-upload). 38 | 39 | ### Passing a collection 40 | 41 | Optionally, you may pass a [`collection()`](https://spatie.be/docs/laravel-medialibrary/working-with-media-collections/simple-media-collections) allows you to group files into categories: 42 | 43 | ```php 44 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 45 | 46 | SpatieMediaLibraryFileUpload::make('avatar') 47 | ->collection('avatars') 48 | ``` 49 | 50 | ### Configuring the storage disk and directory 51 | 52 | By default, files will be uploaded publicly to your storage disk defined in the [Filament configuration file](https://filamentphp.com/docs/forms/installation#publishing-configuration). You can also set the `FILESYSTEM_DISK` environment variable to change this. This is to ensure consistency between all Filament packages. Spatie's disk configuration will not be used, unless you [define a disk for a registered collection](https://spatie.be/docs/laravel-medialibrary/working-with-media-collections/defining-media-collections#content-using-a-specific-disk). 53 | 54 | Alternatively, you can manually set the disk with the `disk()` method: 55 | 56 | ```php 57 | use Filament\Forms\Components\FileUpload; 58 | 59 | FileUpload::make('attachment') 60 | ->disk('s3') 61 | ``` 62 | 63 | The base file upload component also has configuration options for setting the `directory()` and `visibility()` of uploaded files. These are not used by the media library file upload component. Spatie's package has its own system for determining the directory of a newly-uploaded file, and it does not support uploading private files out of the box. One way to store files privately is to configure this in your S3 bucket settings, in which case you should also use `visibility('private')` to ensure that Filament generates temporary URLs for your files. 64 | 65 | ### Reordering files 66 | 67 | In addition to the behavior of the normal file upload, Spatie's Media Library also allows users to reorder files. 68 | 69 | To enable this behavior, use the `reorderable()` method: 70 | 71 | ```php 72 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 73 | 74 | SpatieMediaLibraryFileUpload::make('attachments') 75 | ->multiple() 76 | ->reorderable() 77 | ``` 78 | 79 | You may now drag and drop files into order. 80 | 81 | ### Adding custom properties 82 | 83 | You may pass in [custom properties](https://spatie.be/docs/laravel-medialibrary/advanced-usage/using-custom-properties) when uploading files using the `customProperties()` method: 84 | 85 | ```php 86 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 87 | 88 | SpatieMediaLibraryFileUpload::make('attachments') 89 | ->multiple() 90 | ->customProperties(['zip_filename_prefix' => 'folder/subfolder/']) 91 | ``` 92 | 93 | You may use a function to dynamically set the properties based on the uploaded file: 94 | 95 | ```php 96 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 97 | use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; 98 | use Spatie\Image\Image; 99 | 100 | SpatieMediaLibraryFileUpload::make('image') 101 | ->image() 102 | ->customProperties(function (TemporaryUploadedFile $file): array { 103 | $image = Image::load($file->getRealPath()); 104 | 105 | return [ 106 | 'height' => $image->getHeight(), 107 | 'width' => $image->getWidth(), 108 | ]; 109 | }) 110 | ``` 111 | 112 | ### Adding custom headers 113 | 114 | You may pass in custom headers when uploading files using the `customHeaders()` method: 115 | 116 | ```php 117 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 118 | 119 | SpatieMediaLibraryFileUpload::make('attachments') 120 | ->multiple() 121 | ->customHeaders(['CacheControl' => 'max-age=86400']) 122 | ``` 123 | 124 | ### Generating responsive images 125 | 126 | You may [generate responsive images](https://spatie.be/docs/laravel-medialibrary/responsive-images/getting-started-with-responsive-images) when the files are uploaded using the `responsiveImages()` method: 127 | 128 | ```php 129 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 130 | 131 | SpatieMediaLibraryFileUpload::make('attachments') 132 | ->multiple() 133 | ->responsiveImages() 134 | ``` 135 | 136 | ### Using conversions 137 | 138 | You may also specify a `conversion()` to load the file from showing it in the form, if present: 139 | 140 | ```php 141 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 142 | 143 | SpatieMediaLibraryFileUpload::make('attachments') 144 | ->conversion('thumb') 145 | ``` 146 | 147 | #### Storing conversions on a separate disk 148 | 149 | You can store your conversions and responsive images on a disk other than the one where you save the original file. Pass the name of the disk where you want conversion to be saved to the `conversionsDisk()` method: 150 | 151 | ```php 152 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 153 | 154 | SpatieMediaLibraryFileUpload::make('attachments') 155 | ->conversionsDisk('s3') 156 | ``` 157 | 158 | ### Storing media-specific manipulations 159 | 160 | You may pass in [manipulations](https://spatie.be/docs/laravel-medialibrary/advanced-usage/storing-media-specific-manipulations#breadcrumb) that are run when files are uploaded using the `manipulations()` method: 161 | 162 | ```php 163 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 164 | 165 | SpatieMediaLibraryFileUpload::make('attachments') 166 | ->multiple() 167 | ->manipulations([ 168 | 'thumb' => ['orientation' => '90'], 169 | ]) 170 | ``` 171 | 172 | ### Filtering media 173 | 174 | It's possible to target a file upload component to only handle a certain subset of media in a collection. To do that, you can filter the media collection using the `filterMediaUsing()` method. This method accepts a function that receives the `$media` collection and manipulates it. You can use any [collection method](https://laravel.com/docs/collections#available-methods) to filter it. 175 | 176 | For example, you could scope the field to only handle media that has certain custom properties: 177 | 178 | ```php 179 | use Filament\Schemas\Components\Utilities\Get; 180 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 181 | use Illuminate\Support\Collection; 182 | 183 | SpatieMediaLibraryFileUpload::make('images') 184 | ->customProperties(fn (Get $get): array => [ 185 | 'gallery_id' => $get('gallery_id'), 186 | ]) 187 | ->filterMediaUsing( 188 | fn (Collection $media, Get $get): Collection => $media->where( 189 | 'custom_properties.gallery_id', 190 | $get('gallery_id') 191 | ), 192 | ) 193 | ``` 194 | 195 | ### Using media library for rich editor file attachments 196 | 197 | You can use media library to store file attachments in the [rich editor](https://filamentphp.com/docs/forms/rich-editor). To do this, you must [register a rich content attribute](https://filamentphp.com/docs/forms/rich-editor#registering-rich-content-attributes) on your model, similar to how a media library collection is registered. You should call `fileAttachmentProvider()` on the attribute registration, passing in a `SpatieMediaLibraryFileAttachmentProvider::make()` object: 198 | 199 | ```php 200 | use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider; 201 | use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent; 202 | use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent; 203 | use Illuminate\Database\Eloquent\Model; 204 | 205 | class Post extends Model implements HasRichContent 206 | { 207 | use InteractsWithRichContent; 208 | 209 | public function setUpRichContent(): void 210 | { 211 | $this->registerRichContent('content') 212 | ->fileAttachmentProvider(SpatieMediaLibraryFileAttachmentProvider::make()); 213 | } 214 | } 215 | ``` 216 | 217 | > Using `SpatieMediaLibraryFileAttachmentProvider` requires that the rich content attribute (`content` in this example) must be defined as nullable in database. 218 | 219 | A media collection with the same name as the attribute (`content` in this example) will be used for the file attachments. The collection must not contain any other media apart from file attachments for that attribute, since Filament will clear any unused media from the collection when the model is saved. To customize the name of the collection, you can pass it to the `collection()` method of the provider: 220 | 221 | ```php 222 | use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider; 223 | use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent; 224 | use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent; 225 | use Illuminate\Database\Eloquent\Model; 226 | 227 | class Post extends Model implements HasRichContent 228 | { 229 | use InteractsWithRichContent; 230 | 231 | public function setUpRichContent(): void 232 | { 233 | $this->registerRichContent('content') 234 | ->fileAttachmentProvider( 235 | SpatieMediaLibraryFileAttachmentProvider::make() 236 | ->collection('content-file-attachments'), 237 | ); 238 | } 239 | } 240 | ``` 241 | 242 | You may want to preserve the original filenames of the uploaded files, using the `preserveFilenames()` method: 243 | 244 | ```php 245 | use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider; 246 | 247 | SpatieMediaLibraryFileAttachmentProvider::make() 248 | ->preserveFilenames() 249 | ``` 250 | 251 | You can customize the [media name](https://spatie.be/docs/laravel-medialibrary/api/adding-files#content-usingname) using the `mediaName()` method: 252 | 253 | ```php 254 | use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider; 255 | use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; 256 | use Illuminate\Support\Str; 257 | 258 | SpatieMediaLibraryFileAttachmentProvider::make() 259 | ->mediaName(fn (TemporaryUploadedFile $file): string => Str::random() . '_' . $file->getClientOriginalName()) 260 | ``` 261 | 262 | You may pass in [custom properties](https://spatie.be/docs/laravel-medialibrary/advanced-usage/using-custom-properties) when uploading files using the `customProperties()` method: 263 | 264 | ```php 265 | use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider; 266 | 267 | SpatieMediaLibraryFileAttachmentProvider::make() 268 | ->customProperties(['archived' => false]) 269 | ``` 270 | 271 | ## Table column 272 | 273 | To use the media library image column: 274 | 275 | ```php 276 | use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; 277 | 278 | SpatieMediaLibraryImageColumn::make('avatar') 279 | ``` 280 | 281 | The media library image column supports all the customization options of the [original image column](https://filamentphp.com/docs/tables/columns/image). 282 | 283 | ### Passing a collection 284 | 285 | Optionally, you may pass a `collection()`: 286 | 287 | ```php 288 | use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; 289 | 290 | SpatieMediaLibraryImageColumn::make('avatar') 291 | ->collection('avatars') 292 | ``` 293 | 294 | The [collection](https://spatie.be/docs/laravel-medialibrary/working-with-media-collections/simple-media-collections) allows you to group files into categories. 295 | 296 | By default, only media without a collection (using the `default` collection) will be shown. If you want to show media from all collections, you can use the `allCollections()` method: 297 | 298 | ```php 299 | use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; 300 | 301 | SpatieMediaLibraryImageColumn::make('avatar') 302 | ->allCollections() 303 | ``` 304 | 305 | ### Using conversions 306 | 307 | You may also specify a `conversion()` to load the file from showing it in the table, if present: 308 | 309 | ```php 310 | use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; 311 | 312 | SpatieMediaLibraryImageColumn::make('avatar') 313 | ->conversion('thumb') 314 | ``` 315 | 316 | ### Filtering media 317 | 318 | It's possible to target the column to only display a subset of media in a collection. To do that, you can filter the media collection using the `filterMediaUsing()` method. This method accepts a function that receives the `$media` collection and manipulates it. You can use any [collection method](https://laravel.com/docs/collections#available-methods) to filter it. 319 | 320 | For example, you could scope the column to only display media that has certain custom properties: 321 | 322 | ```php 323 | use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; 324 | use Illuminate\Support\Collection; 325 | 326 | SpatieMediaLibraryImageColumn::make('images') 327 | ->filterMediaUsing( 328 | fn (Collection $media): Collection => $media->where( 329 | 'custom_properties.gallery_id', 330 | 12345, 331 | ), 332 | ) 333 | ``` 334 | 335 | ## Infolist entry 336 | 337 | To use the media library image entry: 338 | 339 | ```php 340 | use Filament\Infolists\Components\SpatieMediaLibraryImageEntry; 341 | 342 | SpatieMediaLibraryImageEntry::make('avatar') 343 | ``` 344 | 345 | The media library image entry supports all the customization options of the [original image entry](https://filamentphp.com/docs/infolists/entries/image). 346 | 347 | ### Passing a collection 348 | 349 | Optionally, you may pass a `collection()`: 350 | 351 | ```php 352 | use Filament\Infolists\Components\SpatieMediaLibraryImageEntry; 353 | 354 | SpatieMediaLibraryImageEntry::make('avatar') 355 | ->collection('avatars') 356 | ``` 357 | 358 | The [collection](https://spatie.be/docs/laravel-medialibrary/working-with-media-collections/simple-media-collections) allows you to group files into categories. 359 | 360 | By default, only media without a collection (using the `default` collection) will be shown. If you want to show media from all collections, you can use the `allCollections()` method: 361 | 362 | ```php 363 | use Filament\Infolists\Components\SpatieMediaLibraryImageEntry; 364 | 365 | SpatieMediaLibraryImageEntry::make('avatar') 366 | ->allCollections() 367 | ``` 368 | 369 | ### Using conversions 370 | 371 | You may also specify a `conversion()` to load the file from showing it in the infolist, if present: 372 | 373 | ```php 374 | use Filament\Infolists\Components\SpatieMediaLibraryImageEntry; 375 | 376 | SpatieMediaLibraryImageEntry::make('avatar') 377 | ->conversion('thumb') 378 | ``` 379 | 380 | ### Filtering media 381 | 382 | It's possible to target the entry to only display a subset of media in a collection. To do that, you can filter the media collection using the `filterMediaUsing()` method. This method accepts a function that receives the `$media` collection and manipulates it. You can use any [collection method](https://laravel.com/docs/collections#available-methods) to filter it. 383 | 384 | For example, you could scope the entry to only display media that has certain custom properties: 385 | 386 | ```php 387 | use Filament\Tables\Columns\SpatieMediaLibraryImageEntry; 388 | use Illuminate\Support\Collection; 389 | 390 | SpatieMediaLibraryImageEntry::make('images') 391 | ->filterMediaUsing( 392 | fn (Collection $media): Collection => $media->where( 393 | 'custom_properties.gallery_id', 394 | 12345, 395 | ), 396 | ) 397 | ``` 398 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filament/spatie-laravel-media-library-plugin", 3 | "description": "Filament support for `spatie/laravel-medialibrary`.", 4 | "license": "MIT", 5 | "homepage": "https://github.com/filamentphp/filament", 6 | "support": { 7 | "issues": "https://github.com/filamentphp/filament/issues", 8 | "source": "https://github.com/filamentphp/filament" 9 | }, 10 | "require": { 11 | "php": "^8.2", 12 | "filament/support": "self.version", 13 | "spatie/laravel-medialibrary": "^11.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Filament\\": "src" 18 | } 19 | }, 20 | "config": { 21 | "sort-packages": true 22 | }, 23 | "minimum-stability": "dev", 24 | "prefer-stable": true 25 | } 26 | -------------------------------------------------------------------------------- /src/Forms/Components/RichEditor/FileAttachmentProviders/SpatieMediaLibraryFileAttachmentProvider.php: -------------------------------------------------------------------------------- 1 | | Closure | null 32 | */ 33 | protected array | Closure | null $customProperties = null; 34 | 35 | public static function make(): static 36 | { 37 | return app(static::class); 38 | } 39 | 40 | public function collection(?string $collection): static 41 | { 42 | $this->collection = $collection; 43 | 44 | return $this; 45 | } 46 | 47 | public function preserveFilenames(bool | Closure $condition = true): static 48 | { 49 | $this->shouldPreserveFilenames = $condition; 50 | 51 | return $this; 52 | } 53 | 54 | public function mediaName(string | Closure | null $name): static 55 | { 56 | $this->mediaName = $name; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @param array | Closure | null $properties 63 | */ 64 | public function customProperties(array | Closure | null $properties): static 65 | { 66 | $this->customProperties = $properties; 67 | 68 | return $this; 69 | } 70 | 71 | public function attribute(RichContentAttribute $attribute): static 72 | { 73 | $this->attribute = $attribute; 74 | 75 | return $this; 76 | } 77 | 78 | public function getExistingModel(): ?HasMedia 79 | { 80 | $model = $this->attribute->getModel(); 81 | 82 | if (! $model->exists) { 83 | return null; 84 | } 85 | 86 | if (! ($model instanceof HasMedia)) { 87 | throw new LogicException('The [' . static::class . '] requires the model to implement the [' . HasMedia::class . '] interface from the Spatie Media Library package.'); 88 | } 89 | 90 | return $model; 91 | } 92 | 93 | public function getMedia(): ?MediaCollection 94 | { 95 | if (isset($this->media)) { 96 | return $this->media; 97 | } 98 | 99 | /** @var MediaCollection $media */ 100 | $media = $this->getExistingModel()?->getMedia($this->getCollection())->keyBy('uuid'); 101 | 102 | return $this->media = $media; 103 | } 104 | 105 | public function getFileAttachmentUrl(mixed $file): ?string 106 | { 107 | $media = $this->getMedia(); 108 | 109 | if (! $media) { 110 | return null; 111 | } 112 | 113 | if (! $media->has($file)) { 114 | return null; 115 | } 116 | 117 | $fileAttachment = $media->get($file); 118 | 119 | if ($this->attribute->getFileAttachmentsVisibility() === 'private') { 120 | try { 121 | return $fileAttachment->getTemporaryUrl( 122 | now()->addMinutes(30)->endOfHour(), 123 | ); 124 | } catch (Throwable $exception) { 125 | // This driver does not support creating temporary URLs. 126 | } 127 | } 128 | 129 | return $fileAttachment->getUrl(); 130 | } 131 | 132 | public function saveUploadedFileAttachment(TemporaryUploadedFile $file): mixed 133 | { 134 | $media = $this->getExistingModel() /** @phpstan-ignore method.notFound */ 135 | ->addMediaFromString($file->get()) 136 | ->usingFileName($this->shouldPreserveFilenames() ? $file->getClientOriginalName() : (Str::ulid() . '.' . $file->getClientOriginalExtension())) 137 | ->usingName($this->getMediaName($file) ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)) 138 | ->withCustomProperties($this->getCustomProperties()) 139 | ->toMediaCollection($this->getCollection(), diskName: $this->attribute->getFileAttachmentsDiskName() ?? ''); 140 | 141 | $this->getMedia()->put($media->uuid, $media); 142 | 143 | return $media->uuid; 144 | } 145 | 146 | /** 147 | * @param array $exceptIds 148 | */ 149 | public function cleanUpFileAttachments(array $exceptIds): void 150 | { 151 | $model = $this->getExistingModel(); 152 | $collectionName = $this->getCollection(); 153 | 154 | $model->clearMediaCollectionExcept( 155 | $collectionName, 156 | $model->getMedia($collectionName)->whereIn('uuid', $exceptIds), 157 | ); 158 | } 159 | 160 | public function getDefaultFileAttachmentVisibility(): ?string 161 | { 162 | return 'private'; 163 | } 164 | 165 | public function isExistingRecordRequiredToSaveNewFileAttachments(): bool 166 | { 167 | return true; 168 | } 169 | 170 | public function getCollection(): string 171 | { 172 | return $this->collection ?? $this->attribute->getName(); 173 | } 174 | 175 | public function shouldPreserveFilenames(): bool 176 | { 177 | return (bool) $this->evaluate($this->shouldPreserveFilenames); 178 | } 179 | 180 | public function getMediaName(TemporaryUploadedFile $file): ?string 181 | { 182 | return $this->evaluate($this->mediaName, [ 183 | 'file' => $file, 184 | ]); 185 | } 186 | 187 | /** 188 | * @return array 189 | */ 190 | public function getCustomProperties(): array 191 | { 192 | return $this->evaluate($this->customProperties) ?? []; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Forms/Components/SpatieMediaLibraryFileUpload.php: -------------------------------------------------------------------------------- 1 | | Closure | null 33 | */ 34 | protected array | Closure | null $customHeaders = null; 35 | 36 | /** 37 | * @var array | Closure | null 38 | */ 39 | protected array | Closure | null $customProperties = null; 40 | 41 | /** 42 | * @var array> | Closure | null 43 | */ 44 | protected array | Closure | null $manipulations = null; 45 | 46 | /** 47 | * @var array | Closure | null 48 | */ 49 | protected array | Closure | null $properties = null; 50 | 51 | protected function setUp(): void 52 | { 53 | parent::setUp(); 54 | 55 | $this->loadStateFromRelationshipsUsing(static function (SpatieMediaLibraryFileUpload $component, HasMedia $record): void { 56 | /** @var Model&HasMedia $record */ 57 | $media = $record->load('media')->getMedia($component->getCollection() ?? 'default') 58 | ->when( 59 | $component->hasMediaFilter(), 60 | fn (Collection $media) => $component->filterMedia($media) 61 | ) 62 | ->when( 63 | ! $component->isMultiple(), 64 | fn (Collection $media): Collection => $media->take(1), 65 | ) 66 | ->mapWithKeys(function (Media $media): array { 67 | $uuid = $media->getAttributeValue('uuid'); 68 | 69 | return [$uuid => $uuid]; 70 | }) 71 | ->toArray(); 72 | 73 | $component->rawState($media); 74 | }); 75 | 76 | $this->afterStateHydrated(null); 77 | 78 | $this->beforeStateDehydrated(null); 79 | 80 | $this->dehydrated(false); 81 | 82 | $this->getUploadedFileUsing(static function (SpatieMediaLibraryFileUpload $component, string $file): ?array { 83 | if (! $component->getRecord()) { 84 | return null; 85 | } 86 | 87 | /** @var ?Media $media */ 88 | $media = $component->getRecord()->getRelationValue('media')->firstWhere('uuid', $file); 89 | 90 | $url = null; 91 | 92 | if ($component->getVisibility() === 'private') { 93 | $conversion = $component->getConversion(); 94 | 95 | try { 96 | $url = $media?->getTemporaryUrl( 97 | now()->addMinutes(30)->endOfHour(), 98 | (filled($conversion) && $media->hasGeneratedConversion($conversion)) ? $conversion : '', 99 | ); 100 | } catch (Throwable $exception) { 101 | // This driver does not support creating temporary URLs. 102 | } 103 | } 104 | 105 | if ($component->getConversion() && $media?->hasGeneratedConversion($component->getConversion())) { 106 | $url ??= $media->getUrl($component->getConversion()); 107 | } 108 | 109 | $url ??= $media?->getUrl(); 110 | 111 | return [ 112 | 'name' => $media?->getAttributeValue('name') ?? $media?->getAttributeValue('file_name'), 113 | 'size' => $media?->getAttributeValue('size'), 114 | 'type' => $media?->getAttributeValue('mime_type'), 115 | 'url' => $url, 116 | ]; 117 | }); 118 | 119 | $this->saveRelationshipsUsing(static function (SpatieMediaLibraryFileUpload $component): void { 120 | $component->deleteAbandonedFiles(); 121 | $component->saveUploadedFiles(); 122 | }); 123 | 124 | $this->saveUploadedFileUsing(static function (SpatieMediaLibraryFileUpload $component, TemporaryUploadedFile $file, ?Model $record): ?string { 125 | if (! method_exists($record, 'addMediaFromString')) { 126 | return $file; 127 | } 128 | 129 | try { 130 | if (! $file->exists()) { 131 | return null; 132 | } 133 | } catch (UnableToCheckFileExistence $exception) { 134 | return null; 135 | } 136 | 137 | /** @var FileAdder $mediaAdder */ 138 | $mediaAdder = $record->addMediaFromString($file->get()); 139 | 140 | $filename = $component->getUploadedFileNameForStorage($file); 141 | 142 | $media = $mediaAdder 143 | ->addCustomHeaders($component->getCustomHeaders()) 144 | ->usingFileName($filename) 145 | ->usingName($component->getMediaName($file) ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)) 146 | ->storingConversionsOnDisk($component->getConversionsDisk() ?? '') 147 | ->withCustomProperties($component->getCustomProperties($file)) 148 | ->withManipulations($component->getManipulations()) 149 | ->withResponsiveImagesIf($component->hasResponsiveImages()) 150 | ->withProperties($component->getProperties()) 151 | ->toMediaCollection($component->getCollection() ?? 'default', $component->getDiskName()); 152 | 153 | return $media->getAttributeValue('uuid'); 154 | }); 155 | 156 | $this->reorderUploadedFilesUsing(static function (SpatieMediaLibraryFileUpload $component, ?Model $record, array $rawState): array { 157 | $uuids = array_filter(array_keys($rawState)); 158 | 159 | $mediaClass = ($record && method_exists($record, 'getMediaModel')) ? $record->getMediaModel() : null; 160 | $mediaClass ??= config('media-library.media_model', Media::class); 161 | 162 | $mappedIds = $mediaClass::query()->whereIn('uuid', $uuids)->pluck(app($mediaClass)->getKeyName(), 'uuid')->toArray(); 163 | 164 | $mediaClass::setNewOrder([ 165 | ...array_flip($uuids), 166 | ...$mappedIds, 167 | ]); 168 | 169 | return $rawState; 170 | }); 171 | } 172 | 173 | public function collection(string | Closure | null $collection): static 174 | { 175 | $this->collection = $collection; 176 | 177 | return $this; 178 | } 179 | 180 | public function conversion(string | Closure | null $conversion): static 181 | { 182 | $this->conversion = $conversion; 183 | 184 | return $this; 185 | } 186 | 187 | public function conversionsDisk(string | Closure | null $disk): static 188 | { 189 | $this->conversionsDisk = $disk; 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * @param array | Closure | null $headers 196 | */ 197 | public function customHeaders(array | Closure | null $headers): static 198 | { 199 | $this->customHeaders = $headers; 200 | 201 | return $this; 202 | } 203 | 204 | /** 205 | * @param array | Closure | null $properties 206 | */ 207 | public function customProperties(array | Closure | null $properties): static 208 | { 209 | $this->customProperties = $properties; 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * @param array> | Closure | null $manipulations 216 | */ 217 | public function manipulations(array | Closure | null $manipulations): static 218 | { 219 | $this->manipulations = $manipulations; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * @param array | Closure | null $properties 226 | */ 227 | public function properties(array | Closure | null $properties): static 228 | { 229 | $this->properties = $properties; 230 | 231 | return $this; 232 | } 233 | 234 | public function responsiveImages(bool | Closure $condition = true): static 235 | { 236 | $this->hasResponsiveImages = $condition; 237 | 238 | return $this; 239 | } 240 | 241 | public function deleteAbandonedFiles(): void 242 | { 243 | /** @var Model&HasMedia $record */ 244 | $record = $this->getRecord(); 245 | 246 | $record 247 | ->getMedia($this->getCollection() ?? 'default') 248 | ->whereNotIn('uuid', array_keys($this->getRawState() ?? [])) 249 | ->when($this->hasMediaFilter(), fn (Collection $media): Collection => $this->filterMedia($media)) 250 | ->each(fn (Media $media) => $media->delete()); 251 | } 252 | 253 | public function getDiskName(): string 254 | { 255 | $name = $this->evaluate($this->diskName); 256 | 257 | if (filled($name)) { 258 | return $name; 259 | } 260 | 261 | /** @var Model&HasMedia $model */ 262 | $model = $this->getModelInstance(); 263 | 264 | $collection = $this->getCollection() ?? 'default'; 265 | 266 | /** @phpstan-ignore-next-line */ 267 | $diskNameFromRegisteredConversions = $model 268 | ->getRegisteredMediaCollections() 269 | ->filter(fn (MediaCollection $mediaCollection): bool => $mediaCollection->name === $collection) 270 | ->first() 271 | ?->diskName; 272 | 273 | if ( 274 | ($diskNameFromRegisteredConversions === 'public') 275 | && ($this->getCustomVisibility() === 'private') 276 | ) { 277 | return 'local'; 278 | } 279 | 280 | if (filled($diskNameFromRegisteredConversions)) { 281 | return $diskNameFromRegisteredConversions; 282 | } 283 | 284 | $defaultName = config('filament.default_filesystem_disk'); 285 | 286 | if ( 287 | ($defaultName === 'public') 288 | && ($this->getCustomVisibility() === 'private') 289 | ) { 290 | return 'local'; 291 | } 292 | 293 | return $defaultName; 294 | } 295 | 296 | public function getCollection(): ?string 297 | { 298 | return $this->evaluate($this->collection); 299 | } 300 | 301 | public function getConversion(): ?string 302 | { 303 | return $this->evaluate($this->conversion); 304 | } 305 | 306 | public function getConversionsDisk(): ?string 307 | { 308 | return $this->evaluate($this->conversionsDisk); 309 | } 310 | 311 | /** 312 | * @return array 313 | */ 314 | public function getCustomHeaders(): array 315 | { 316 | return $this->evaluate($this->customHeaders) ?? []; 317 | } 318 | 319 | /** 320 | * @return array 321 | */ 322 | public function getCustomProperties(TemporaryUploadedFile $file): array 323 | { 324 | return $this->evaluate($this->customProperties, [ 325 | 'file' => $file, 326 | ]) ?? []; 327 | } 328 | 329 | /** 330 | * @return array> 331 | */ 332 | public function getManipulations(): array 333 | { 334 | return $this->evaluate($this->manipulations) ?? []; 335 | } 336 | 337 | /** 338 | * @return array 339 | */ 340 | public function getProperties(): array 341 | { 342 | return $this->evaluate($this->properties) ?? []; 343 | } 344 | 345 | public function hasResponsiveImages(): bool 346 | { 347 | return (bool) $this->evaluate($this->hasResponsiveImages); 348 | } 349 | 350 | public function mediaName(string | Closure | null $name): static 351 | { 352 | $this->mediaName = $name; 353 | 354 | return $this; 355 | } 356 | 357 | public function getMediaName(TemporaryUploadedFile $file): ?string 358 | { 359 | return $this->evaluate($this->mediaName, [ 360 | 'file' => $file, 361 | ]); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/Infolists/Components/SpatieMediaLibraryImageEntry.php: -------------------------------------------------------------------------------- 1 | defaultImageUrl(function (SpatieMediaLibraryImageEntry $component, Model $record): ?string { 28 | if ($component->hasStateRelationship($record)) { 29 | $record = $component->getStateRelationshipResults($record); 30 | } 31 | 32 | $records = Arr::wrap($record); 33 | 34 | $collection = $component->getCollection(); 35 | 36 | if (! is_string($collection)) { 37 | $collection = 'default'; 38 | } 39 | 40 | foreach ($records as $record) { 41 | $url = $record->getFallbackMediaUrl($collection, $component->getConversion() ?? ''); 42 | 43 | if (blank($url)) { 44 | continue; 45 | } 46 | 47 | return $url; 48 | } 49 | 50 | return null; 51 | }); 52 | } 53 | 54 | public function collection(string | AllMediaCollections | Closure | null $collection): static 55 | { 56 | $this->collection = $collection; 57 | 58 | return $this; 59 | } 60 | 61 | public function allCollections(): static 62 | { 63 | $this->collection(AllMediaCollections::make()); 64 | 65 | return $this; 66 | } 67 | 68 | public function conversion(string | Closure | null $conversion): static 69 | { 70 | $this->conversion = $conversion; 71 | 72 | return $this; 73 | } 74 | 75 | public function getCollection(): string | AllMediaCollections | null 76 | { 77 | return $this->evaluate($this->collection); 78 | } 79 | 80 | public function getConversion(): ?string 81 | { 82 | return $this->evaluate($this->conversion); 83 | } 84 | 85 | public function getImageUrl(?string $state = null): ?string 86 | { 87 | $record = $this->getRecord(); 88 | 89 | if (! $record) { 90 | return null; 91 | } 92 | 93 | if ($this->hasStateRelationship($record)) { 94 | $record = $this->getStateRelationshipResults($record); 95 | } 96 | 97 | $records = Arr::wrap($record); 98 | 99 | foreach ($records as $record) { 100 | /** @var Model $record */ 101 | 102 | /** @var ?Media $media */ 103 | $media = $record->getRelationValue('media')->first(fn (Media $media): bool => $media->uuid === $state); 104 | 105 | if (! $media) { 106 | continue; 107 | } 108 | 109 | $conversion = $this->getConversion(); 110 | 111 | if ($this->getVisibility() === 'private') { 112 | try { 113 | return $media->getTemporaryUrl( 114 | now()->addMinutes(30)->endOfHour(), 115 | $conversion ?? '', 116 | ); 117 | } catch (Throwable $exception) { 118 | // This driver does not support creating temporary URLs. 119 | } 120 | } 121 | 122 | return $media->getAvailableUrl(Arr::wrap($conversion)); 123 | } 124 | 125 | return null; 126 | } 127 | 128 | /** 129 | * @return array 130 | */ 131 | public function getState(): array 132 | { 133 | $record = $this->getRecord(); 134 | 135 | if ($this->hasStateRelationship($record)) { 136 | $record = $this->getStateRelationshipResults($record); 137 | } 138 | 139 | $records = Arr::wrap($record); 140 | 141 | $state = []; 142 | 143 | $collection = $this->getCollection() ?? 'default'; 144 | 145 | foreach ($records as $record) { 146 | /** @var Model $record */ 147 | $state = [ 148 | ...$state, 149 | ...$record->getRelationValue('media') 150 | ->when( 151 | ! $collection instanceof AllMediaCollections, 152 | fn (MediaCollection $mediaCollection) => $mediaCollection->filter(fn (Media $media): bool => $media->getAttributeValue('collection_name') === $collection), 153 | ) 154 | ->when( 155 | $this->hasMediaFilter(), 156 | fn (Collection $media) => $this->filterMedia($media) 157 | ) 158 | ->sortBy('order_column') 159 | ->pluck('uuid') 160 | ->all(), 161 | ]; 162 | } 163 | 164 | return array_unique($state); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/SpatieLaravelMediaLibraryPlugin/Collections/AllMediaCollections.php: -------------------------------------------------------------------------------- 1 | filterMediaUsing = $callback; 15 | 16 | return $this; 17 | } 18 | 19 | public function filterMedia(Collection $media): Collection 20 | { 21 | return $this->evaluate($this->filterMediaUsing, [ 22 | 'media' => $media, 23 | ]) ?? $media; 24 | } 25 | 26 | public function hasMediaFilter(): bool 27 | { 28 | return $this->filterMediaUsing instanceof Closure; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tables/Columns/SpatieMediaLibraryImageColumn.php: -------------------------------------------------------------------------------- 1 | defaultImageUrl(function (SpatieMediaLibraryImageColumn $column, Model $record): ?string { 30 | if ($column->hasRelationship($record)) { 31 | $record = $column->getRelationshipResults($record); 32 | } 33 | 34 | $records = Arr::wrap($record); 35 | 36 | $collection = $column->getCollection(); 37 | 38 | if (! is_string($collection)) { 39 | $collection = 'default'; 40 | } 41 | 42 | foreach ($records as $record) { 43 | $url = $record->getFallbackMediaUrl($collection, $column->getConversion() ?? ''); 44 | 45 | if (blank($url)) { 46 | continue; 47 | } 48 | 49 | return $url; 50 | } 51 | 52 | return null; 53 | }); 54 | } 55 | 56 | public function collection(string | AllMediaCollections | Closure | null $collection): static 57 | { 58 | $this->collection = $collection; 59 | 60 | return $this; 61 | } 62 | 63 | public function allCollections(): static 64 | { 65 | $this->collection(AllMediaCollections::make()); 66 | 67 | return $this; 68 | } 69 | 70 | public function conversion(string | Closure | null $conversion): static 71 | { 72 | $this->conversion = $conversion; 73 | 74 | return $this; 75 | } 76 | 77 | public function getCollection(): string | AllMediaCollections | null 78 | { 79 | return $this->evaluate($this->collection); 80 | } 81 | 82 | public function getConversion(): ?string 83 | { 84 | return $this->evaluate($this->conversion); 85 | } 86 | 87 | public function getImageUrl(?string $state = null): ?string 88 | { 89 | $record = $this->getRecord(); 90 | 91 | if ($this->hasRelationship($record)) { 92 | $record = $this->getRelationshipResults($record); 93 | } 94 | 95 | $records = Arr::wrap($record); 96 | 97 | foreach ($records as $record) { 98 | /** @var Model $record */ 99 | 100 | /** @var ?Media $media */ 101 | $media = $record->getRelationValue('media')->first(fn (Media $media): bool => $media->uuid === $state); 102 | 103 | if (! $media) { 104 | continue; 105 | } 106 | 107 | $conversion = $this->getConversion(); 108 | 109 | if ($this->getVisibility() === 'private') { 110 | try { 111 | return $media->getTemporaryUrl( 112 | now()->addMinutes(30)->endOfHour(), 113 | $conversion ?? '', 114 | ); 115 | } catch (Throwable $exception) { 116 | // This driver does not support creating temporary URLs. 117 | } 118 | } 119 | 120 | return $media->getAvailableUrl(Arr::wrap($conversion)); 121 | } 122 | 123 | return null; 124 | } 125 | 126 | /** 127 | * @return array 128 | */ 129 | public function getState(): array 130 | { 131 | return $this->cacheState(function (): array { 132 | $record = $this->getRecord(); 133 | 134 | if ($this->hasRelationship($record)) { 135 | $record = $this->getRelationshipResults($record); 136 | } 137 | 138 | $records = Arr::wrap($record); 139 | 140 | $state = []; 141 | 142 | $collection = $this->getCollection() ?? 'default'; 143 | 144 | foreach ($records as $record) { 145 | /** @var Model $record */ 146 | $state = [ 147 | ...$state, 148 | ...$record->getRelationValue('media') 149 | ->when( 150 | ! $collection instanceof AllMediaCollections, 151 | fn (MediaCollection $mediaCollection) => $mediaCollection->filter(fn (Media $media): bool => $media->getAttributeValue('collection_name') === $collection), 152 | ) 153 | ->when( 154 | $this->hasMediaFilter(), 155 | fn (Collection $media) => $this->filterMedia($media) 156 | ) 157 | ->sortBy('order_column') 158 | ->pluck('uuid') 159 | ->all(), 160 | ]; 161 | } 162 | 163 | return array_unique($state); 164 | }); 165 | } 166 | 167 | /** 168 | * @template TModel of Model 169 | * 170 | * @param Builder|Relation $query 171 | * @return Builder|Relation 172 | */ 173 | public function applyEagerLoading(Builder | Relation $query): Builder | Relation 174 | { 175 | if ($this->isHidden()) { 176 | return $query; 177 | } 178 | 179 | /** @phpstan-ignore-next-line */ 180 | $modifyMediaQuery = fn (Builder | Relation $query) => $query->ordered(); 181 | 182 | if ($this->hasRelationship($query->getModel())) { 183 | return $query->with([ 184 | "{$this->getRelationshipName($query->getModel())}.media" => $modifyMediaQuery, 185 | ]); 186 | } 187 | 188 | return $query->with(['media' => $modifyMediaQuery]); 189 | } 190 | } 191 | --------------------------------------------------------------------------------