Media Gallery
The Tagixo Filament SDK ships a first-class Media Gallery feature: a standalone Filament resource page for browsing and uploading files, a MediaGalleryPickerField for forms, and a MediaGalleryColumn for tables. All three are opt-in and disabled by default.
1. Enabling the Media Gallery
Register the feature on the plugin instance inside your panel provider:
use Ccast\TagixoFilament\TagixoFilamentPlugin;
$panel->plugin(
TagixoFilamentPlugin::make()
->withMediaGallery()
);
This registers the MediaGalleryResource, its upload controller, and all related routes. If you need to publish the default config:
php artisan vendor:publish --tag=tagixo-config
Key entries under config/tagixo.php:
| Key | Default | Description |
|---|---|---|
media.disk |
'public' |
Storage disk for uploads |
media.directory |
'tagixo-media' |
Directory within the disk |
media.max_size |
10240 |
Max upload size in KB |
media.accepted_mime |
['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'application/pdf'] |
Allowed MIME types |
media.signed_urls |
false |
Generate signed URLs (S3) |
media.signed_ttl |
60 |
Signed URL TTL in minutes |
2. What the Media Gallery Adds
Once enabled, the plugin registers:
MediaGalleryResource— a Filament resource page accessible from the admin sidebar, showing a responsive thumbnail grid of all uploaded files stored intgx_media.- Per-item metadata — each item exposes filename, original name, MIME type, file size, disk, and path. Clicking a thumbnail opens a detail drawer.
- Navigation customisation — the resource accepts fluent builder methods to control its position in the sidebar:
TagixoFilamentPlugin::make()
->withMediaGallery()
->mediaGalleryNavigationLabel('Assets')
->mediaGalleryNavigationGroup('Content'),
3. MediaGalleryPickerField
MediaGalleryPickerField is a Filament form component that opens the gallery picker in a modal and writes the selected file reference back to the form state.
use Ccast\TagixoFilament\Forms\MediaGalleryPickerField;
MediaGalleryPickerField::make('featured_image_id')
->label('Featured Image')
->storeAs('id') // 'id' | 'url' | 'path'
->accept(['image/*']) // MIME filter shown in the picker
->clearable()
->columnSpanFull(),
storeAs() options
| Value | What is saved | Best for |
|---|---|---|
'id' |
The tgx_media.id integer |
FK relations, resolving URLs server-side |
'url' |
The public URL of the file | Simple display, no FK needed |
'path' |
The disk-relative path | Manual Storage::url() resolution |
Storing the ID is the safest option: if you rename a file or switch disks, the URL is regenerated from the stored ID at render time. Storing url or path creates a snapshot that may go stale.
accept()
Pass an array of MIME type strings. The picker filters the gallery grid and blocks non-matching uploads from within the modal. Server-side upload validation is governed by config/tagixo.php media.accepted_mime — accept() only narrows the client-side view.
clearable()
When set, a "Remove" button appears below the thumbnail, allowing the field to be reset to null. Pass clearable(false) to suppress it once a value is set.
4. MediaGalleryColumn
MediaGalleryColumn renders a fixed-size thumbnail preview of a media item inside a Filament table.
use Ccast\TagixoFilament\Tables\MediaGalleryColumn;
MediaGalleryColumn::make('featured_image_id')
->label('Image')
->size(48) // pixel size of the square thumbnail
->shape('rounded'), // 'square' | 'rounded' | 'circle'
When the stored value is an ID, the column resolves the MediaItem and uses its cached public URL. When it is a raw URL or path string, it is used directly.
Options
| Method | Values | Description |
|---|---|---|
size(int $px) |
any integer | Width and height of the rendered thumbnail |
shape(string) |
'square', 'rounded', 'circle' |
CSS border-radius preset |
Avoid N+1 queries. If you display
MediaGalleryColumnfor a list resource, eager-load the media relationship in your Eloquent query. The column does not auto-eager-load.
5. Upload Flow in the Builder
When withMediaGallery() is enabled, the visual builder's image slot component detects the feature flag and swaps the native file-input for the gallery modal. The six-step event sequence:
- Editor clicks an image placeholder or the "Change image" button on an existing image module.
- The builder emits
tagixo:open-media-pickeronwindowwith a{ callbackKey, accept }payload. - The gallery modal opens as a Filament overlay. Any
accept()restrictions declared by the PropType are passed to the modal. - The editor browses or uploads a new file within the modal.
- On selection, the modal dispatches
tagixo:media-selectedwith{ callbackKey, id, url, path }. - The builder canvas updates the prop value and re-renders the module preview.
No page navigation occurs. The builder state (unsaved canvas changes, selected element, scroll position, undo stack) is fully preserved across the picker open/close cycle.
6. Upload Restrictions
Upload validation runs server-side in Ccast\Tagixo\Http\Controllers\MediaUploadController before the file is written to disk. Client-side restrictions mirror the server config but are not enforced independently.
| Config key | Default | Description |
|---|---|---|
media.accepted_mime |
See config | Allowlist of MIME types. Upload rejected with 422 if not in list. |
media.max_size |
10240 (10 MB) |
Max file size in kilobytes. |
media.directory |
tagixo-media |
Storage directory prefix within the configured disk. |
To extend the MIME allowlist at runtime without republishing the config file, merge into the config in a service provider:
// In a service provider boot():
config(['tagixo.media.accepted_mime' => array_merge(
config('tagixo.media.accepted_mime', []),
['video/mp4', 'image/avif'],
)]);
7. Disk Configuration
Local public disk (default)
With TAGIXO_MEDIA_DISK=public, files land in storage/app/public/tagixo-media/ and are served via the standard storage symlink. Ensure php artisan storage:link has run and APP_URL is set correctly — the public URL is built with Storage::url().
S3 and other remote disks
Point TAGIXO_MEDIA_DISK at any Laravel filesystem disk name defined in config/filesystems.php:
TAGIXO_MEDIA_DISK=s3
TAGIXO_MEDIA_SIGNED_URLS=true
TAGIXO_MEDIA_SIGNED_TTL=60
// config/filesystems.php
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
],
With signed_urls=true, every URL returned by MediaItem::url() goes through Storage::temporaryUrl(). TTL is in minutes.
Signed URLs expire. Do not cache rendered page HTML that contains signed S3 URLs for longer than
TAGIXO_MEDIA_SIGNED_TTL. Use a short full-page cache TTL or bypass the page cache whensigned_urlsistrue.
Custom URL resolution (resolveUrlUsing)
For CDN rewrites or image-transform services (Imgix, Cloudflare Images), bypass the default URL resolution with an escape hatch registered in a service provider:
// In a service provider boot():
\Ccast\Tagixo\Models\MediaItem::resolveUrlUsing(function (MediaItem $item): string {
return 'https://cdn.example.com/' . ltrim($item->path, '/');
});
This callback replaces the default Storage::url() / Storage::temporaryUrl() logic entirely. The signed_urls config has no effect when a custom resolver is registered.
8. Spatie Media Library Compatibility
ccast/tagixo-filament's media gallery is entirely independent of spatie/laravel-medialibrary.
| Concern | Behaviour |
|---|---|
| Package conflict | None — both packages can be required simultaneously. |
| Shared file store | Not by default. Tagixo files go to tagixo-media/; Spatie files go to media/. |
| Shared picker UI | Not provided. MediaGalleryPickerField only sources from tgx_media. |
MediaGalleryColumn with Spatie |
Not compatible. Use Filament's built-in ImageColumn with a Spatie accessor instead. |
If you want a unified gallery that shows both Spatie and Tagixo media, project Spatie records into tgx_media via a model observer:
class SpatieMediaObserver
{
public function created(\Spatie\MediaLibrary\MediaCollections\Models\Media $media): void
{
\Ccast\Tagixo\Models\MediaItem::firstOrCreate(
['path' => $media->getPath()],
[
'disk' => $media->disk,
'mime_type' => $media->mime_type,
'size' => $media->size,
'name' => $media->file_name,
]
);
}
public function deleted(\Spatie\MediaLibrary\MediaCollections\Models\Media $media): void
{
\Ccast\Tagixo\Models\MediaItem::where('path', $media->getPath())->delete();
}
}
Register in AppServiceProvider::boot():
\Spatie\MediaLibrary\MediaCollections\Models\Media::observe(SpatieMediaObserver::class);
This is a one-way sync: Tagixo sees Spatie files but does not manage their lifecycle.
9. Permission Gating
The gallery enforces two distinct capability levels: browse/pick (read, select) and upload/delete (write, manage).
Default behaviour
By default, any authenticated panel user can browse, pick, upload, and delete media. No policies are applied out of the box.
Plugin-level callbacks
The plugin exposes two callback methods for simple role checks:
TagixoFilamentPlugin::make()
->withMediaGallery()
->mediaGalleryUploadPolicy(function (): bool {
return auth()->user()?->can('upload_media') ?? false;
})
->mediaGalleryDeletePolicy(function (): bool {
return auth()->user()?->hasRole('admin') ?? false;
}),
When mediaGalleryUploadPolicy returns false, the "Upload" button is hidden in both the resource page and the modal, and POST /tagixo/media/upload returns 403.
When mediaGalleryDeletePolicy returns false, the delete action is removed from the resource table and DELETE /tagixo/media/{id} returns 403.
Laravel Gate / Policy
Register a standard Laravel policy for Ccast\Tagixo\Models\MediaItem for finer-grained control:
Gate::policy(\Ccast\Tagixo\Models\MediaItem::class, \App\Policies\MediaItemPolicy::class);
namespace App\Policies;
use App\Models\User;
use Ccast\Tagixo\Models\MediaItem;
class MediaItemPolicy
{
public function viewAny(User $user): bool
{
return true; // all panel users can browse
}
public function create(User $user): bool
{
return $user->can('upload_media');
}
public function delete(User $user, MediaItem $item): bool
{
return $user->hasRole('admin') || $item->uploaded_by === $user->id;
}
}
MediaGalleryResource checks these gates via Filament's standard policy resolution. The ->mediaGalleryUploadPolicy() and ->mediaGalleryDeletePolicy() callbacks take precedence over Gate policies when both are defined.
Browse-only panels. If you want certain panel users to be able to pick from existing media but never upload or delete, set both callbacks to return
falsefor those users. The picker modal will still open — it loads the gallery in read-only mode.
10. Complete Example
Migration
// In a migration:
$table->unsignedBigInteger('featured_image_id')->nullable();
$table->foreign('featured_image_id')
->references('id')->on('tgx_media')
->nullOnDelete();
Eloquent relation
// app/Models/Post.php
public function featuredImage(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(\Ccast\Tagixo\Models\MediaItem::class, 'featured_image_id');
}
PostResource
namespace App\Filament\Resources;
use App\Models\Post;
use Ccast\TagixoFilament\Forms\MediaGalleryPickerField;
use Ccast\TagixoFilament\Tables\MediaGalleryColumn;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class PostResource extends Resource
{
protected static ?string $model = Post::class;
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('title')
->required()
->columnSpanFull(),
MediaGalleryPickerField::make('featured_image_id')
->label('Featured image')
->storeAs('id')
->accept(['image/jpeg', 'image/png', 'image/webp'])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table->columns([
MediaGalleryColumn::make('featured_image_id')
->label('')
->size(56)
->shape('rounded'),
TextColumn::make('title')->searchable()->sortable(),
TextColumn::make('created_at')->dateTime()->sortable(),
]);
}
}
Blade usage
@if ($post->featuredImage)
<img
src="{{ $post->featuredImage->url() }}"
alt="{{ $post->title }}"
class="w-full rounded-lg"
/>
@endif