PHPackages                             codenzia/filament-media - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Image &amp; Media](/categories/media)
4. /
5. codenzia/filament-media

ActiveLibrary[Image &amp; Media](/categories/media)

codenzia/filament-media
=======================

A powerful media manager plugin for Filament v4 with drag-and-drop uploads, folder organization, image editing, and thumbnail generation.

00PHPCI failing

Since Feb 28Pushed 2mo agoCompare

[ Source](https://github.com/Codenzia/filament-media)[ Packagist](https://packagist.org/packages/codenzia/filament-media)[ RSS](/packages/codenzia-filament-media/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependenciesVersions (1)Used By (0)

Filament Media Manager
======================

[](#filament-media-manager)

[![Latest Version on Packagist](https://camo.githubusercontent.com/8898e00736df92659bf91c957ef45e1776542db020b8d87e4230a68e728e7b79/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f636f64656e7a69612f66696c616d656e742d6d656469612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/codenzia/filament-media)[![GitHub Tests Action Status](https://camo.githubusercontent.com/323e54fd7705d44d6ff672073d63c27d70fa94e13a1558ce67585cd611c8d865/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f636f64656e7a69612f66696c616d656e742d6d656469612f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/codenzia/filament-media/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/4fb2039c01c9073ef3c901526e0cc1133b1368e919b5fa6624ca5d9dd93145f0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f636f64656e7a69612f66696c616d656e742d6d656469612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/codenzia/filament-media)

A full-featured Digital Asset Management plugin for Filament v4. Upload, organize, tag, version, and serve media files across local and cloud storage — with a modern UI, fine-grained access control, and a developer-friendly service architecture.

Features
--------

[](#features)

**File Management** — Drag-and-drop uploads with progress tracking, chunked uploads for large files, upload from URL (with SSRF protection), multi-file selection, batch operations, copy/move/rename, alt text, automatic thumbnails with optional watermarks.

**Folders** — Nested folder structure with unlimited depth, color-coded folders, drag-and-drop organization, automatic folder resolution from file paths.

**Tags &amp; Collections** — Tag files and folders, organize into named collections, filter and search by tags, bulk tagging, popular tags with usage counts.

**Custom Metadata** — Define custom fields (text, number, date, select, boolean, URL), attach to files, search and filter by metadata, auto-extract EXIF data.

**Search** — Database search out of the box, optional Laravel Scout integration, search by name, tags, metadata, file type, and date range.

**Versioning** — Upload new versions, view history with changelogs, revert to any previous version, configurable retention with auto-prune.

**Export &amp; Import** — Export as ZIP with metadata manifest, import from ZIP or local folder with automatic metadata restoration.

**Organization** — Favorites, recent items, type filters (image, video, document, audio, archive), sort by name/date/size, grid and list views, breadcrumb navigation.

**Trash &amp; Recovery** — Soft delete with trash folder, restore, permanent delete.

**Preview** — Full-screen gallery modal with version history, image/video/audio preview, document preview (PDF, Office via Google/Microsoft viewers), keyboard navigation.

**UI** — Responsive design, dark mode, configurable theme colors, context menu, details panel, drag-and-drop between folders, multi-select with Ctrl/Cmd and Shift.

**Visibility &amp; Access Control** — Per-file public/private visibility, HMAC-SHA256 hash verification for private URLs, custom authorization callbacks, automatic file movement between storage disks, per-user media scoping.

**Cloud Storage** — Local, Amazon S3, Cloudflare R2, DigitalOcean Spaces, Wasabi, Backblaze B2, BunnyCDN.

**Developer Tools** — 15 singleton services with DI, 16 Laravel events, `MediaFileUpload` and `MediaPickerField` form components, `MediaFileGrid` / `MediaFileList` / `MediaFiles` embeddable Livewire components, `FilesUploadWidget`, `HasMediaFiles` and `InteractsWithMediaCollections` traits, `MediaAdder` fluent builder, typed exceptions, query scopes, per-panel page visibility, configurable navigation.

Keyboard Shortcuts
------------------

[](#keyboard-shortcuts)

ShortcutAction`Arrow Keys`Navigate between items`Enter`Open folder or preview file`Space`Toggle selection`Ctrl/Cmd+A`Select all`Delete`Move to trash`F2`Rename`Escape`Clear selection / Close preview`Arrow Left/Right`Previous/next in previewRequirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11+
- Filament 4.0+
- GD or Imagick PHP extension (for thumbnails)
- ZIP PHP extension (for export/import and multi-file download)

Installation
------------

[](#installation)

```
composer require codenzia/filament-media
```

Publish and run migrations:

```
php artisan vendor:publish --tag="filament-media-migrations"
php artisan migrate
```

Publish the config:

```
php artisan vendor:publish --tag="filament-media-config"
```

Optionally publish views:

```
php artisan vendor:publish --tag="filament-media-views"
```

Setup
-----

[](#setup)

### Register the Plugin

[](#register-the-plugin)

```
use Codenzia\FilamentMedia\FilamentMediaPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            FilamentMediaPlugin::make(),
        ]);
}
```

Control which pages are registered per panel:

```
// Admin panel — full access
FilamentMediaPlugin::make(),

// User dashboard — picker only, no standalone pages
FilamentMediaPlugin::make()
    ->showMediaManager(false)
    ->showSettings(false),
```

### Storage Link

[](#storage-link)

For local storage:

```
php artisan storage:link
```

### Custom Theme (Tailwind v4)

[](#custom-theme-tailwind-v4)

If your panel uses a custom theme (`->viteTheme()`), add these `@source` directives to your theme CSS so Tailwind discovers the package's utility classes:

```
@source '../../../../vendor/codenzia/filament-media/resources/views/**/*.blade.php';
@source '../../../../vendor/codenzia/filament-media/src/**/*.php';
```

Then rebuild: `npm run build`

> This is only needed with custom themes. Filament's default theme works without changes.

Configuration
-------------

[](#configuration)

The config file (`config/media.php`) provides full control over all options.

### Feature Flags

[](#feature-flags)

```
'features' => [
    'tags' => true,
    'collections' => true,
    'metadata' => true,
    'versioning' => true,
    'search' => true,
    'export_import' => true,
],
```

### Storage Driver

[](#storage-driver)

```
'driver' => 'public', // 'public', 's3', 'r2', 'do_spaces', 'wasabi', 'bunnycdn', 'backblaze'
```

### Navigation

[](#navigation)

```
'navigation' => [
    'media' => [
        'label' => null,                // null = use translation key
        'icon' => 'heroicon-o-photo',
        'group' => null,
        'sort' => 1,
        'visible' => true,
    ],
    'settings' => [
        'label' => null,
        'icon' => 'heroicon-o-cog-6-tooth',
        'group' => null,
        'sort' => 2,
        'visible' => true,
    ],
],
```

### Theme Colors

[](#theme-colors)

Colors are injected as CSS custom properties (`--fm-*`) with separate light and dark mode values:

```
'theme' => [
    'light' => [
        'primary' => '#6366f1',
        'surface' => '#ffffff',
        'border' => '#e5e7eb',
        'text' => '#111827',
        // ... see config for all options
    ],
    'dark' => [
        'primary' => '#818cf8',
        'surface' => '#111827',
        // ...
    ],
],
```

### Upload Limits

[](#upload-limits)

```
'max_file_size' => 10 * 1024 * 1024, // 10MB
'allowed_mime_types' => 'jpg,jpeg,png,gif,pdf,doc,docx,...',
'allowed_download_domains' => [], // empty = all domains allowed for URL uploads
```

### Thumbnails &amp; Watermarks

[](#thumbnails--watermarks)

```
'sizes' => ['thumb' => '150x150'],
'generate_thumbnails_enabled' => true,
'watermark' => [
    'enabled' => false,
    'source' => null,
    'size' => 10,
    'opacity' => 70,
    'position' => 'bottom-right',
],
```

### Private Files

[](#private-files)

```
'private_files' => [
    'enabled' => true,
    'signed_url_expiry' => 30, // minutes (cloud temporary URLs)
    'private_disk' => 'local', // storage disk for private files
],
```

### Search &amp; Versioning

[](#search--versioning)

```
'search' => ['driver' => 'database', 'min_query_length' => 2], // or 'scout'
'versioning' => ['max_versions' => 10, 'auto_prune' => true],
```

Usage
-----

[](#usage)

### Media Manager Page

[](#media-manager-page)

Available at `/admin/media` (or your panel prefix + `/media`).

### Programmatic Access

[](#programmatic-access)

All operations use dedicated service classes:

```
use Codenzia\FilamentMedia\Services\UploadService;
use Codenzia\FilamentMedia\Services\FileOperationService;
use Codenzia\FilamentMedia\Services\MediaUrlService;
use Codenzia\FilamentMedia\Services\TagService;
use Codenzia\FilamentMedia\Exceptions\MediaUploadException;

// Upload a file
try {
    $file = app(UploadService::class)->handleUpload($uploadedFile, $folderId);
} catch (MediaUploadException $e) {
    // Handle: invalidFileType, fileTooLarge, unableToWrite, etc.
}

// Get URL, copy, tag
$url = app(MediaUrlService::class)->url($file->url);
$copy = app(FileOperationService::class)->copyFile($file, $targetFolderId);
app(TagService::class)->attachTags($file, ['nature', 'landscape']);
```

Upload methods return `MediaFile` directly and throw `MediaUploadException` on failure.

### File Visibility &amp; Access Control

[](#file-visibility--access-control)

Files have a `visibility` attribute — `public` (default) or `private`. Public files are served via direct storage URL. Private files are served through an authenticated controller with HMAC-SHA256 hash verification.

#### Changing Visibility

[](#changing-visibility)

```
$fileOps = app(FileOperationService::class);
$fileOps->changeVisibility($file, 'private'); // moves to private disk
$fileOps->changeVisibility($file, 'public');  // moves back
```

On local storage, files and thumbnails are physically moved between disks.

#### How Private URLs Work

[](#how-private-urls-work)

Public files: `/storage/photos/image.jpg`Private files: `/media/private/{hash}/{id}`

The hash is an HMAC-SHA256 of the file ID, keyed to `APP_KEY`. URLs cannot be guessed or enumerated without the secret key. The controller verifies authentication and authorization before streaming. Append `?download=1` to force download.

#### Custom Authorization

[](#custom-authorization)

```
use Codenzia\FilamentMedia\FilamentMedia;
use Codenzia\FilamentMedia\Models\MediaFile;

// In a service provider's boot() method:
app(FilamentMedia::class)->authorizeFileAccessUsing(function (MediaFile $file, $user) {
    return $user && $file->user_id === $user->id;
});
```

The callback receives the `MediaFile` and the authenticated user (or `null`). Return `true` to allow, `false` to deny. Public files bypass the callback.

#### Per-User Media Scoping

[](#per-user-media-scoping)

Control which files each user sees in the Media page:

```
app(FilamentMedia::class)->scopeMediaQueryUsing(function ($query, $user) {
    $query->where('media_files.created_by_user_id', $user->id);
});
```

The callback applies a global scope on `MediaFile` and `MediaFolder` queries across all views (media, trash, recent, favorites, collections). Not invoked when no user is authenticated.

> `scopeMediaQueryUsing()` controls query-level filtering (what appears in the UI), while `authorizeFileAccessUsing()` controls file-level download authorization. Use both together for complete access control.

### Artisan Commands

[](#artisan-commands)

```
php artisan media:cleanup              # Remove DB entries for missing files
php artisan media:cleanup --dry-run    # Preview changes
php artisan media:cleanup --force      # Skip confirmation
```

Form Components
---------------

[](#form-components)

### MediaFileUpload

[](#mediafileupload)

Pre-configured `FileUpload` that reads settings from `config/media.php`:

```
use Codenzia\FilamentMedia\Forms\MediaFileUpload;

MediaFileUpload::make(),              // inherits all config settings
MediaFileUpload::make('avatars'),     // upload to specific directory
```

Automatically resolves MIME types, respects max file size and server limits, uses the configured storage disk, and preserves original filenames.

### MediaPickerField

[](#mediapickerfield)

File picker for selecting existing media:

```
use Codenzia\FilamentMedia\Forms\MediaPickerField;

MediaPickerField::make('featured_image')
    ->imageOnly()
    ->required(),

MediaPickerField::make('attachments')
    ->multiple()
    ->maxFiles(10),

MediaPickerField::make('contracts')
    ->documentOnly()
    ->directory('contracts')
    ->collection('legal'),
```

#### Direct Upload

[](#direct-upload)

By default, the field shows a single "Browse Media" button that opens the full media library picker. Enable `directUpload()` to add a quick "Upload File" option alongside it — the button becomes a dropdown with both choices:

```
MediaPickerField::make('featured_image')
    ->imageOnly()
    ->directUpload(),
```

The upload zone supports drag-and-drop and uses the same upload endpoint as the media library. Uploaded files are saved to the root folder and the field state is updated automatically.

To enable direct upload globally for all `MediaPickerField` instances, set the config default:

```
// config/media.php
'picker' => [
    'direct_upload' => true,
],
```

You can still override the global default per-field:

```
// Disable direct upload for a specific field even when the global default is true
MediaPickerField::make('logo')->directUpload(false),
```

#### Per-Field File Type Control

[](#per-field-file-type-control)

By default, uploads are validated against the global `allowed_mime_types` in `config/media.php`. Two methods let you customize this per field:

```
// Add extra extensions to the global list for this field only
// (e.g., .ico is not in the global list, but favicons need it)
MediaPickerField::make('favicon')
    ->imageOnly()
    ->includeFileTypes(['ico']),

// Restrict to ONLY these extensions, ignoring the global config entirely
MediaPickerField::make('contract')
    ->allowedFileTypesOnly(['pdf', 'docx']),
```

Both methods enforce validation on both client-side (browser) and server-side (upload endpoint). Server-side overrides are protected with HMAC-SHA256 signatures to prevent tampering.

#### Display Styles

[](#display-styles)

The field supports five visual styles via `displayStyle()`:

```
// Compact (default): Text links for browse/upload with chip-style file list
MediaPickerField::make('document')
    ->documentOnly()
    ->displayStyle('compact'),

// Dropdown: Button with dropdown menu for browse/upload options
MediaPickerField::make('document')
    ->documentOnly()
    ->directUpload()
    ->displayStyle('dropdown'),

// Thumbnail: Visual preview card — click to browse, hover for actions, drag & drop
MediaPickerField::make('featured_image')
    ->imageOnly()
    ->displayStyle('thumbnail'),

// Integrated Links: Thumbnail preview + text links below, drag & drop
MediaPickerField::make('avatar')
    ->imageOnly()
    ->displayStyle('integratedLinks'),

// Integrated Dropdown: Thumbnail preview + dropdown button below, drag & drop
MediaPickerField::make('logo')
    ->imageOnly()
    ->directUpload()
    ->displayStyle('integratedDropdown'),
```

StyleBest forDrag &amp; DropDescription`compact`Documents, mixed filesNoText links + chip list with small icons`dropdown`Documents, mixed filesNoDropdown button + chip list with small icons`thumbnail`Images, visual contentYesLarge preview card, hover overlay with change/remove actions`integratedLinks`Images + text linksYesThumbnail preview area with text links below`integratedDropdown`Images + dropdown buttonYesThumbnail preview area with dropdown button belowTo set a global default for all fields:

```
// config/media.php
'picker' => [
    'display_style' => 'integratedLinks',
],
```

Per-field values always override the config default.

#### Preview Size

[](#preview-size)

Control the dimensions of the thumbnail preview container (used by `thumbnail`, `integratedLinks`, and `integratedDropdown` styles). The image itself always maintains its natural aspect ratio via `object-contain`.

```
// Square 256px (aspect-square kept when only width is set)
MediaPickerField::make('logo')
    ->displayStyle('integratedDropdown')
    ->previewWidth('16rem'),

// Rectangle 256x128px (aspect-square removed when height is set)
MediaPickerField::make('banner')
    ->displayStyle('integratedDropdown')
    ->previewWidth('16rem')
    ->previewHeight('8rem'),

// Only change height, keep default width
MediaPickerField::make('icon')
    ->displayStyle('thumbnail')
    ->previewHeight('6rem'),
```

Default: `12rem` width with `aspect-square` (192x192px). Any CSS length value works (`rem`, `px`, `%`, etc.). Global defaults can be set in config:

```
// config/media.php
'picker' => [
    'preview_width' => '12rem',    // CSS value, e.g. '12rem', '256px'
    'preview_height' => null,       // null = aspect-square, or e.g. '8rem'
],
```

#### Chip Size

[](#chip-size)

Control the size of the file chips used in `compact` and `dropdown` display styles. This affects the thumbnail size, icon size, and text size within each chip.

```
MediaPickerField::make('avatar')
    ->displayStyle('dropdown')
    ->chipSize('lg'),    // 64px thumbnails, 16px text

MediaPickerField::make('documents')
    ->displayStyle('compact')
    ->chipSize('xs'),    // 20px thumbnails, 12px text
```

Available sizes:

SizeThumbnailTextDescription`xs`20px12pxTiny — minimal footprint`sm`32px14pxSmall — default`md`48px14pxMedium — easier to see previews`lg`64px16pxLarge — prominent file display`xl`80px18pxExtra large — visual emphasis`2xl`96px20pxHuge — maximum preview sizeGlobal default can be set in config:

```
// config/media.php
'picker' => [
    'chip_size' => 'sm',    // 'xs', 'sm', 'md', 'lg', 'xl', '2xl'
],
```

#### Lightbox Size

[](#lightbox-size)

Control the maximum dimensions of the full-screen image preview (lightbox) that appears when clicking a thumbnail. By default, the image fills the available viewport.

```
// Constrain the lightbox to a smaller area
MediaPickerField::make('avatar')
    ->displayStyle('thumbnail')
    ->lightboxMaxWidth('600px')
    ->lightboxMaxHeight('400px'),
```

Global defaults can be set in config:

```
// config/media.php
'picker' => [
    'lightbox_max_width' => null,     // null = full viewport, or e.g. '800px', '50vw'
    'lightbox_max_height' => null,    // null = full viewport, or e.g. '600px', '80vh'
],
```

#### Lightbox Opacity

[](#lightbox-opacity)

Control the backdrop opacity of the lightbox overlay. The value is a percentage from 0 (fully transparent) to 100 (fully opaque). Default: 80.

```
// More opaque backdrop
MediaPickerField::make('avatar')
    ->displayStyle('thumbnail')
    ->lightboxOpacity(95),
```

Global default can be set in config:

```
// config/media.php
'picker' => [
    'lightbox_opacity' => 80,  // 0 = transparent, 100 = fully opaque
],
```

MethodDescription`multiple()`Allow selecting multiple files`imageOnly()`Restrict to images`videoOnly()`Restrict to videos`documentOnly()`Restrict to documents`acceptedFileTypes(array)`Custom MIME types for picker filtering`includeFileTypes(array)`Add extra file extensions to the global allowed list for this field`allowedFileTypesOnly(array)`Restrict uploads to ONLY these file extensions (ignores global config)`maxFiles(int)`Limit selections`directory(string)`Default upload directory`collection(string)`Auto-assign collection`directUpload(bool)`Show inline upload option alongside media browser (default: `false`, or from config)`displayStyle(string)`Visual style: `'compact'`, `'dropdown'`, `'thumbnail'`, `'integratedLinks'`, or `'integratedDropdown'` (default: `'compact'`, or from config)`previewWidth(string)`Preview container width as CSS value, e.g. `'16rem'`, `'256px'` (default: `'12rem'`, or from config)`previewHeight(string)`Preview container height as CSS value, e.g. `'8rem'`, `'128px'`. Setting height removes aspect-square (default: `null` / aspect-square, or from config)`chipSize(string)`Chip size preset: `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`, `'2xl'`. Controls thumbnail, icon, and text size in compact/dropdown styles (default: `'sm'`, or from config)`lightboxMaxWidth(string)`Lightbox image max width as CSS value, e.g. `'800px'`, `'50vw'` (default: `null` / full viewport, or from config)`lightboxMaxHeight(string)`Lightbox image max height as CSS value, e.g. `'600px'`, `'80vh'` (default: `null` / full viewport, or from config)`lightboxOpacity(int)`Lightbox backdrop opacity as percentage 0–100 (default: `80`, or from config)Livewire Components
-------------------

[](#livewire-components)

### MediaFileGrid

[](#mediafilegrid)

Displays a grid of media files with full context menu. The parent model must use `HasMediaFiles`:

```
{{-- Single model --}}

{{-- Multi-model mode --}}

```

### MediaFileList

[](#mediafilelist)

Same features as `MediaFileGrid` in a table/list layout:

```

```

### MediaFiles

[](#mediafiles)

Unified viewer with a toggle between grid and list layouts:

```

{{-- Disable layout toggle --}}

```

### Shared Props

[](#shared-props)

All three components accept the same props:

PropTypeDefaultDescription`record`Model|null`null`Parent model (single-model mode)`relationship`string`'files'`Relationship method name`fileableType`string|null`null`Morph class (multi-model mode)`fileableIds`array`[]`Model IDs (multi-model mode)`deletable`bool`false`Enable trash/delete`columns`stringresponsive gridTailwind grid classes`emptyMessage`string`'No files attached'`Empty state message`contextMenu`bool`true`Enable right-click menu`contextMenuExclude`array`[]`Menu items to hide**Context menu keys:** `preview`, `download`, `copy_link`, `view_parent`, `rename`, `alt_text`, `tags`, `collections`, `versions`, `metadata`, `export`, `visibility`, `favorites`, `trash`.

### FilesUploadWidget

[](#filesuploadwidget)

Filament widget for adding uploads to any resource page:

```
use Codenzia\FilamentMedia\Widgets\FilesUploadWidget;

protected function getFooterWidgets(): array
{
    return [
        FilesUploadWidget::make([
            'record' => $this->record,
            'directory' => 'project-files',
            'submitLabel' => 'Upload Files',
            'submitColor' => 'success',
            'submitAlignment' => 'center',
            'visibility' => 'public',
        ]),
    ];
}
```

Attaching Media to Models
-------------------------

[](#attaching-media-to-models)

### Setup

[](#setup-1)

```
use Codenzia\FilamentMedia\Traits\HasMediaFiles;

class Product extends Model
{
    use HasMediaFiles;
}
```

### Uploading

[](#uploading)

```
$product->addMedia($uploadedFile)->save();

$product->addMedia($uploadedFile)
    ->usingName('Product Photo')
    ->toCollection('gallery')
    ->save();

$product->addMediaFromUrl('https://example.com/photo.jpg')
    ->withAlt('Product hero image')
    ->toCollection('gallery')
    ->toFolder($folderId)
    ->save();

$product->addMedia('/path/to/file.jpg')
    ->usingName('Local import')
    ->save();
```

### Attaching &amp; Detaching

[](#attaching--detaching)

```
$product->attachMediaFile($mediaFile);
$product->attachMediaFiles($mediaFiles);
$product->attachMediaWithMeta($mediaFile, ['alt' => 'Product photo']);

$product->syncMediaFiles($mediaFiles);
$product->syncMediaByIds([1, 2, 3]);

$product->detachMediaFile($mediaFile);
$product->detachAllMediaFiles();
$product->deleteMediaFile($fileId);
```

### Querying

[](#querying)

```
$product->files;
$product->images;
$product->videos;
$product->documents;
$product->audio;

$product->mediaByCollection('gallery')->get();
$product->mediaByTag('featured')->get();

$url = $product->getFirstImageUrl();
$url = $product->getFirstImageUrl('/images/placeholder.jpg');
$file = $product->getFirstMediaFile();
$urls = $product->getMediaUrls('gallery');

$product->hasMedia();
$product->hasImages();
$product->clearMedia('gallery');
```

### URL Attributes on MediaFile

[](#url-attributes-on-mediafile)

```
$file->preview_url;    // Full public URL for display
$file->indirect_url;   // Controller-routed URL with hash verification
$file->url;            // Raw relative storage path
```

### Physical File Deletion

[](#physical-file-deletion)

```
$file->deleteWithFile();                                  // deletes file + soft-deletes record
$file->deleteWithFile('Deleted!', 'Failed to delete');    // with Filament notifications
$product->deleteMediaFile($fileId);                       // verifies ownership first
```

Named Media Collections
-----------------------

[](#named-media-collections)

For models that need structured, constrained media:

```
use Codenzia\FilamentMedia\Traits\HasMediaFiles;
use Codenzia\FilamentMedia\Traits\InteractsWithMediaCollections;

class User extends Model
{
    use HasMediaFiles, InteractsWithMediaCollections;

    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('avatar')
            ->singleFile()
            ->acceptsMimeTypes(['image/*'])
            ->useFallbackUrl('/images/default-avatar.png');

        $this->addMediaCollection('gallery')
            ->onlyKeepLatest(20);

        $this->addMediaCollection('documents')
            ->acceptsMimeTypes(['application/pdf', 'application/msword']);
    }
}
```

MethodDescription`singleFile()`One file only — new uploads auto-detach the previous`onlyKeepLatest(int)`Keep at most N files, auto-prune oldest`acceptsMimeTypes(array)`Restrict MIME types (supports `image/*` wildcards)`useFallbackUrl(string)`URL returned when collection is empty```
$user->addMedia($file)->toCollection('avatar')->save();
$avatarUrl = $user->getFirstCollectionUrl('avatar');
$user->validateCollectionMimeType('avatar', 'image/jpeg'); // true
```

Collections build on the tag system (`MediaTag` with `type='collection'`). No additional migrations needed.

Error Handling
--------------

[](#error-handling)

```
use Codenzia\FilamentMedia\Exceptions\MediaUploadException;

try {
    $file = $product->addMedia($uploadedFile)->save();
} catch (MediaUploadException $e) {
    logger()->error('Upload failed: ' . $e->getMessage());
}
```

MethodWhen Thrown`invalidFileType()`MIME type not in allowed list`fileTooLarge(string $size)`Exceeds configured max size`unableToWrite(string $folder)`Storage write failed`networkError(string $url)`URL download failed`ssrfBlocked(string $message)`URL targets internal network`invalidUrl()`Malformed or empty URL`invalidPath()`Local file path doesn't exist`noFileDetected()`Could not detect file type`tempFileError()`Temp file creation failedAutomatic Folder Resolution
---------------------------

[](#automatic-folder-resolution)

When creating a `MediaFile` without an explicit `folder_id`, the folder is resolved from the `url` path:

```
// Creates "Avatars" folder automatically
MediaFile::create([
    'url' => 'avatars/photo.jpg',
    'name' => 'Profile Photo',
    'mime_type' => 'image/jpeg',
    'size' => 12345,
    'visibility' => 'public',
    'user_id' => $user->id,
]);

// Nested: creates Products > Gallery
MediaFile::create(['url' => 'products/gallery/photo.jpg', ...]);

// Explicit folder_id skips auto-resolution
MediaFile::create(['url' => 'avatars/photo.jpg', 'folder_id' => $id, ...]);
```

Auto-resolution only runs when `folder_id` is `0` or not set. Uses `firstOrCreate()` internally, so concurrent uploads are safe.

Tags &amp; Collections
----------------------

[](#tags--collections)

```
$tagService = app(TagService::class);

$tagService->attachTags($file, ['nature', 'landscape']);
$tagService->syncTags($file, ['nature', 'updated']);
$tagService->detachTags($file, [$tagId]);
$popular = $tagService->getPopularTags(20);
$tagService->mergeTags([$sourceId1, $sourceId2], $targetId);

// Collections (special tags with type='collection')
$collection = $tagService->createCollection('Hero Banners', 'Homepage banners');
$tagService->addToCollection($collection->id, [$fileId1, $fileId2]);
$files = $tagService->getCollectionContents($collection->id);

// Query scopes
$files = MediaFile::tagged([$tagId1, $tagId2])->get();
$files = MediaFile::inCollection($collectionId)->get();
```

Custom Metadata
---------------

[](#custom-metadata)

```
$metadata = app(MetadataService::class);

$metadata->createField([
    'name' => 'Copyright', 'slug' => 'copyright',
    'type' => 'text', 'is_searchable' => true,
]);

$metadata->setMetadata($file, [$fieldId => '2025 Acme Inc.']);
$value = $metadata->getMetadataValue($file, 'copyright');

// Query scope
$files = MediaFile::withMetadataValue('license', 'MIT')->get();
```

Search
------

[](#search)

```
$search = app(SearchService::class);

$results = $search->search('annual report');
$results = $search->searchFiles('report', $folderId);
$results = $search->searchByTag('nature');
$results = $search->searchByMetadata('copyright', 'Acme');
$results = $search->advancedSearch([
    'name' => 'report', 'type' => 'document',
    'date_from' => '2025-01-01', 'date_to' => '2025-12-31',
]);
```

File Versioning
---------------

[](#file-versioning)

```
$versions = app(VersionService::class);

$version = $versions->createVersion($file, $uploadedFile, 'Updated design v2');
$history = $versions->getVersions($file);
$file = $versions->revertToVersion($file, $versionId);
$versions->deleteVersion($versionId);
$deleted = $versions->pruneOldVersions($file, keepCount: 5);
```

Export &amp; Import
-------------------

[](#export--import)

```
$exporter = app(ExportImportService::class);

$response = $exporter->exportFiles([$fileId1, $fileId2]);
$response = $exporter->exportFolder($folderId, includeSubfolders: true);
$response = $exporter->exportWithMetadata([$fileId1, $fileId2]);

$result = $exporter->importFromZip($uploadedZipFile, $targetFolderId);
$result = $exporter->importFromFolder('/path/to/folder', $targetFolderId);
```

Exports with metadata include a `manifest.json` preserving tags, collections, and custom metadata.

Orphan File Management
----------------------

[](#orphan-file-management)

Manage files in storage that have no database record:

```
$scanner = app(OrphanScanService::class);

$orphans = $scanner->scan();
$imported = $scanner->import(
    paths: ['uploads/photo.jpg'],
    folderId: 0,
    userId: auth()->id(),
);
$deleted = $scanner->delete(['uploads/old-file.jpg']);
```

Also available via the **Storage Scanner** section in Media Settings.

Events
------

[](#events)

All operations dispatch Laravel events:

### File Events

[](#file-events)

EventProperties`MediaFileUploaded``MediaFile $file``MediaFileRenaming``MediaFile $file`, `string $newName`, `bool $renameOnDisk``MediaFileRenamed``MediaFile $file``MediaFileDeleting``MediaFile $file``MediaFileDeleted``MediaFile $file``MediaFileTrashed``MediaFile $file``MediaFileRestored``MediaFile $file``MediaFileMoved``MediaFile $file`, `$oldFolderId`, `$newFolderId``MediaFileCopied``MediaFile $newFile`, `MediaFile $originalFile``MediaFileTagged``MediaFile $file`, `array $tagIds``MediaFileVersionCreated``MediaFile $file`, `MediaFileVersion $version`### Folder Events

[](#folder-events)

EventProperties`MediaFolderCreated``MediaFolder $folder``MediaFolderRenaming``MediaFolder $folder`, `string $newName`, `bool $renameOnDisk``MediaFolderRenamed``MediaFolder $folder``MediaFolderDeleted``MediaFolder $folder``MediaFolderMoved``MediaFolder $folder`, `$oldParentId`, `$newParentId`Permissions
-----------

[](#permissions)

PermissionDescription`folders.create`Create new folders`folders.edit`Edit/rename folders`folders.trash`Move folders to trash`folders.destroy`Permanently delete folders`folders.favorite`Add folders to favorites`files.create`Upload files`files.read`View files`files.edit`Edit files (rename, alt text)`files.trash`Move files to trash`files.destroy`Permanently delete files`files.favorite`Add files to favorites`settings.access`Access settings page```
$media = app(FilamentMedia::class);
$media->hasPermission('files.create');
$media->hasAnyPermission(['files.edit', 'files.trash']);
$media->addPermission('files.export');
```

Security
--------

[](#security)

- **Private File Access** — Authenticated controller with customizable authorization callbacks
- **HMAC-SHA256 URLs** — Private file URLs keyed to the application secret, not guessable
- **SSRF Protection** — URL downloads validated against internal networks, cloud metadata IPs, and configurable domain allowlists
- **XSS Prevention** — User content escaped via `SafeContentService`
- **File Validation** — Uploads validated for MIME type and size
- **Rate Limiting** — Private file routes throttled
- **CSRF Protection** — All Livewire actions protected

Architecture
------------

[](#architecture)

### Services (registered as singletons)

[](#services-registered-as-singletons)

ServiceResponsibility`UploadService`File uploads, validation, SSRF checks`FileOperationService`Rename, copy, move, delete, visibility changes`ImageService`Thumbnails, watermarks, image processing`MediaUrlService`URL generation, path resolution, MIME detection`StorageDriverService`Cloud disk configuration (S3, R2, DO, etc.)`FavoriteService`Favorites and recent items`TagService`Tags and collections`MetadataService`Custom metadata fields`SearchService`Full-text search (DB or Scout)`VersionService`File versioning`ExportImportService`ZIP export/import with metadata`OrphanScanService`Storage scan, orphan import/delete`ThumbnailService`Image resize and crop### Support Classes

[](#support-classes)

ClassPurpose`MediaAdder`Fluent builder for uploads (`->usingName()->toCollection()->save()`)`MediaCollection`Collection definition with constraints`MediaHash`HMAC-SHA256 hash generation for URL obfuscation`MediaUploadException`Typed exceptions for upload failures### Traits

[](#traits)

TraitPurpose`HasMediaFiles`Polymorphic relationships, attach/detach/sync, fluent upload builder`InteractsWithMediaCollections`Named collections with constraints### Livewire Components

[](#livewire-components-1)

ComponentPurpose`Media`Main media manager page`UploadModal`File uploads with progress tracking`PreviewModal`Gallery-style preview with version history`MediaPicker`Embeddable file browser for `MediaPickerField``MediaFileGrid`File grid with context menu`MediaFileList`File list/table with context menu`MediaFiles`Unified viewer with grid/list toggleExtending
---------

[](#extending)

Override any service:

```
$this->app->singleton(TagService::class, MyCustomTagService::class);
```

Listen to events:

```
protected $listen = [
    MediaFileUploaded::class => [
        GenerateAiDescription::class,
        SyncToExternalCdn::class,
    ],
];
```

Testing
-------

[](#testing)

```
composer test
```

Image Gallery Component
-----------------------

[](#image-gallery-component)

A standalone Alpine.js image gallery with lightbox, thumbnails, keyboard navigation, and RTL support. Works with any array of image URLs — no dependency on the media manager models.

```

```

PropTypeDefaultDescription`urls`array`[]`Array of image URLs`alt`string`''`Alt text for accessibility and lightbox titleFeatures: main image display, prev/next arrows, thumbnail strip, fullscreen lightbox overlay, keyboard navigation (arrow keys + Escape), image counter badge, RTL-aware arrow direction, no-images placeholder.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for recent changes.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Codenzia](https://github.com/Codenzia)
- [All Contributors](../../contributors)

License
-------

[](#license)

This project uses a **dual license**:

- **Open Source** — Available under the [MIT License](LICENSE.md) for OSI-approved open source projects.
- **Commercial** — A commercial license is required for proprietary projects. Visit [codenzia.com](https://codenzia.com) for options.

See [LICENSE.md](LICENSE.md) for full details.

###  Health Score

19

—

LowBetter than 10% of packages

Maintenance56

Moderate activity, may be stable

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

 Bus Factor1

Top contributor holds 72.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/7c09a47187ca823dff0650b985b6b1d0632bf550fffbd692005cb12ffae5e8ac?d=identicon)[mh2x](/maintainers/mh2x)

---

Top Contributors

[![sehsah](https://avatars.githubusercontent.com/u/8730764?v=4)](https://github.com/sehsah "sehsah (59 commits)")[![mh2x](https://avatars.githubusercontent.com/u/10361843?v=4)](https://github.com/mh2x "mh2x (22 commits)")

### Embed Badge

![Health badge](/badges/codenzia-filament-media/health.svg)

```
[![Health](https://phpackages.com/badges/codenzia-filament-media/health.svg)](https://phpackages.com/packages/codenzia-filament-media)
```

###  Alternatives

[milon/barcode

Barcode generator like Qr Code, PDF417, C39, C39+, C39E, C39E+, C93, S25, S25+, I25, I25+, C128, C128A, C128B, C128C, 2-Digits UPC-Based Extention, 5-Digits UPC-Based Extention, EAN 8, EAN 13, UPC-A, UPC-E, MSI (Variation of Plessey code)

1.5k13.3M39](/packages/milon-barcode)[bkwld/croppa

Image thumbnail creation through specially formatted URLs for Laravel

510496.0k23](/packages/bkwld-croppa)[goat1000/svggraph

Generates SVG graphs

132849.6k3](/packages/goat1000-svggraph)[cohensive/embed

Media Embed (for Laravel or as a standalone).

120370.4k](/packages/cohensive-embed)[netresearch/rte-ckeditor-image

Image support in CKEditor for the TYPO3 ecosystem - by Netresearch

63991.3k4](/packages/netresearch-rte-ckeditor-image)[humanmade/tachyon-plugin

Rewrites WordPress image URLs to use Tachyon

87338.5k2](/packages/humanmade-tachyon-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
