--- url: /compare.md description: >- Compare Media Library Pro with Media Library Pro by ralphjsmit, Curator, tomatophp's Media Manager, the official Spatie plugin and Image Library. --- # Filament media library alternatives and comparisons Several good tools handle media in Filament. Most are an upload field or a picker. Media Library Pro is a full asset manager: one shared library with folders, tags, a focal point, a non-destructive editor, usage tracking, chunked uploads and per-tenant isolation, built on spatie/laravel-medialibrary. ::: warning Two products, one name ralphjsmit's plugin is also called "Media Library Pro". It is a different product by a different author (Ralph J. Smit, `ralphjsmit/laravel-filament-media-library`). This plugin is by Majd Hocein (`hoceineel/filament-media-library-pro`). The two are unrelated: no shared code, no affiliation, only a similar name. When this site says "ralphjsmit", it means his product. ::: ## Comparison matrix | | Media Library Pro | ralphjsmit Media Library Pro | awcodes Curator | tomatophp Media Manager | Official Spatie plugin | outerweb Image Library | |---|---|---|---|---|---|---| | Filament 4 and 5 | Yes | Yes | Yes (5.x) | Yes | Yes | Yes | | Folders | Yes, nested | Yes | Not documented | Yes, optional subfolders | Not documented | Not documented | | Tags | Yes | Yes, with spatie/laravel-tags | Not documented | Not documented | Not documented | Not documented | | Focal point | Yes | Not documented | Not documented | Not documented | Not documented | Not documented | | Image editor | Yes, non-destructive with versions | Yes, needs Upload Pro, overwrites the original | Per-image crops ("curations") | Not documented | Not documented | Yes, crop | | Usage tracking and delete guard | Yes | Not documented | Not documented | Not documented | Not documented | Not documented | | Chunked uploads | Yes | Through Upload Pro | Not documented | Not documented | Not documented | Not documented | | Duplicate detection | Yes | Not documented | Not documented | Not documented | Not documented | Not documented | | URL import | Yes | Not documented | Not documented | Not documented | Not documented | Not documented | | Private per-user files | Yes | Not documented | Not documented | Password-protected folders and per-user folder access | Not documented | Not documented | | Tenancy | Yes | Yes | Yes (config flag) | Not documented | Not documented | Not documented | | Price model | Commercial licence, see [pricing](/pricing) | Paid: Solo EUR 99, Unlimited EUR 199, Enterprise EUR 1,250 a year; discounted renewals | No paid tier documented | No paid tier documented | Free | MIT | | Own compiled CSS, no theme setup | Yes | Install guide lists a CSS import into your theme | Install guide adds a CSS import and `@source` line to your theme | Not documented | Not documented | README says to add the plugin's CSS source to your theme | Each row comes from the project's own README, composer.json and docs, checked in October 2026. "Not documented" means we could not find it in those sources, not that it is impossible. Open-source projects move fast, so check their repositories before you decide, and tell us if a row is out of date. A few notes on reading the table: * The official plugin is a form field, table column, infolist entry and rich editor integration for Spatie media. It is not a shared library, so most rows do not apply to it. * ralphjsmit sells the editor and upload add-on separately as Upload Pro. Both are paid. * Curator's own docs say it is not meant to be combined with Spatie Media Library. This plugin and the official plugin are built on Spatie. ## Where Media Library Pro pulls ahead **An editor that keeps your original.** Crop, rotate and flip, then save as a copy or a new version, and restore any version later. A focal point drives every cropped conversion. [See the inspector](/guide/inspector) **Know where every file is used.** The inspector lists the records that use a file, an Unused view finds orphans, and deleting a file that is still in use is blocked. [Folders, tags and trash](/guide/organizing) **Uploads that survive big files.** Chunked, parallel uploads with progress, duplicate detection, URL import and folder upload. [Uploading](/guide/uploading) **Private files that stay private.** Mark files and folders private to their owner, with signed links and temporary S3 URLs. [Private files](/guide/private-files) **One library per customer.** Filament tenancy, stancl/tenancy or your own resolver, with strict mode and storage quotas. [Multi-tenancy](/guide/tenancy) **No theme setup.** The library ships its own compiled CSS and follows your panel colors, fonts and dark mode. English and Arabic with right-to-left layout are included. [Theming and RTL](/reference/translations) ## Head to head * [Media Library Pro vs Curator](/compare/vs-curator): a free media picker and manager with Glide transformations and per-image crops. * [Media Library Pro vs ralphjsmit's Media Library Pro](/compare/vs-ralphjsmit): a different paid product with the same name, built around drivers. * [Media Library Pro vs tomatophp's Media Manager](/compare/vs-tomatophp): a free Spatie-based manager with password-protected folders. ## When a free tool is the better pick * You only need an upload field on a few resources. Use the official Spatie plugin: it is maintained by the Filament team, free, and files stay attached to their model. * You want one shared set of images with on-the-fly resizing and per-image crops, and you do not need folders or tenancy. Curator is built for that. * You need folders with a password and your files are already Spatie media. tomatophp's Media Manager covers that for free. * You want a simple image picker with crop contexts and nothing else. outerweb's Image Library fits. Pick Media Library Pro when several people manage a shared library, when files must be tracked, protected or isolated per customer, or when large uploads and editing without losing originals matter. ## Still deciding? Read the [introduction](/guide/introduction), try the [quick start](/guide/quick-start), and see [pricing](/pricing). --- --- url: /compare/vs-curator.md description: >- Media Library Pro vs awcodes Curator: what each does well, the differences that matter, how to migrate with media-library:import and when to pick which. --- # Media Library Pro vs Curator [Curator](https://github.com/awcodes/filament-curator) by awcodes is a free media picker and manager for Filament. Version 5.x supports Filament 4 and 5. It is for teams that want a picker field plus a media resource, with image resizing handled by Glide. For the full matrix and a note on the similarly named ralphjsmit product, see the [comparison overview](/compare/). ## What Curator does well * **Glide-backed images.** The `` component builds signed Glide URLs to resize, crop and convert formats on demand. * **Curations.** Per-image crops saved against a named preset, for when one automatic crop is not good enough. * **Simple model.** A picker field (`CuratorPicker`), a Filament resource for browsing and editing media, a table column and a rich editor integration. * **Configuration.** Disk, directory, visibility, file swap, directory restriction and a `features.tenancy` flag, set globally or per field. * **Free and open source.** No licence to buy. ## Differences that matter | | Media Library Pro | Curator | |---|---|---| | Storage model | A library table with a file managed by spatie/laravel-medialibrary. | Its own `curator` table. Its docs say it is not meant to be used alongside Spatie Media Library. | | Image processing | Conversions through Spatie, driven by a focal point. | Glide, on demand, with curations. | | Organization | Nested folders, tags, favorites, smart views, trash. | Folders, tags and trash are not documented. | | Usage tracking | Per-file "Used in" list and a delete guard. | Not documented. | | Uploads | Chunked and parallel, with duplicate detection and URL import. | Chunking, duplicate detection and URL import are not documented. | | Editing | Non-destructive editor with versions. | Curations. | | Theme | Own compiled CSS. | Adds a CSS import and an `@source` line to your Filament theme. | | Price | Commercial licence. | Free. | Choose by workflow. Curator resizes at request time through Glide, which suits sites that serve many sizes of the same image. Media Library Pro generates conversions through Spatie and keeps them with the file. ## Migrating from Curator Use the importer. It reads Curator's table, copies each file into the library, and rewrites the foreign keys in your own tables. ```bash php artisan media-library:import curator --dry-run php artisan media-library:import curator --update=posts.cover_id --update=pages.hero_id --map=curator-map.json ``` `--table` sets the Curator table if you renamed it, `--folder` places everything in a folder (default `Imported/Curator`), and `--map` writes a JSON map from old to new ids. The import is repeatable: files that are already imported are skipped. The full steps, including the picker swap, are in [Importing existing files](/guide/importing). Curations (per-image crops) have no equivalent import. Recreate them with the focal point and conversions. ## When to pick which Pick Curator when it is free, you want Glide's on-demand sizes, and a picker plus a media resource is enough. Pick Media Library Pro when you need folders and tags, to know where files are used, large and duplicate-safe uploads, private files, or a library per tenant. See the [overview](/compare/) for the rest of the field. --- --- url: /compare/vs-ralphjsmit.md description: >- Media Library Pro vs ralphjsmit's Media Library Pro: two unrelated products with the same name. What each does, key differences and when to pick which. --- # Media Library Pro vs ralphjsmit's Media Library Pro ::: warning Same name, different product This page compares two unrelated products. ralphjsmit's "Media Library Pro" (`ralphjsmit/laravel-filament-media-library`) is a paid Filament plugin by Ralph J. Smit. Filament Media Library Pro (`hoceineel/filament-media-library-pro`) is by Majd Hocein. There is no shared code, no partnership and no affiliation. They only share a name. ::: ## Who ralphjsmit's product is for It targets teams that want a polished, extensible media library inside Filament and need it to fit an existing storage setup. Its docs describe three data drivers plus custom drivers: a Media Library Item driver, a Spatie Media Library driver and a Storage (filesystem) driver. Version 4 supports Filament 4 and 5 on Laravel 11 or higher. ## What it does well * **Driver flexibility.** You can use its own item table, Spatie media, or plain storage disks, and run several drivers at once. * **Customizable actions.** Built-in actions such as create folder, delete and download are separate classes you configure with `configureUsing()` or replace. * **Folders and tags.** Scoped folders, and a tags input when spatie/laravel-tags is installed. * **Integrations.** Filament RichEditor, TipTap and Spatie Tags integrations, plus Media Picker, Media Column and Media Entry components. * **Replace and duplicate.** Replacing keeps the record's primary key, so references keep working. Duplicate creates an independent copy. * **Authorization policies, multi-tenancy and theming** sections in the docs. * **Free public docs.** Documentation is open at filamentplugins.com. ## Differences that matter | | Media Library Pro (this plugin) | ralphjsmit Media Library Pro | |---|---|---| | Filament | 4 and 5 | 4 and 5 | | Image editor | Included. Non-destructive, with versions. | Needs the paid Filament Upload Pro package. Saving overwrites the original file in place and keeps no version history, per its docs. | | Focal point | Included. | Not documented. | | Chunked uploads | Included. | Through Upload Pro, which is a separate paid package. | | Usage tracking and delete guard | Included. | Not documented. | | Duplicate detection and URL import | Included. | Not documented. | | Private per-user files | Included. | Not documented. | | Data model | One model on Spatie. | Choose a driver. | | Price | Commercial licence, see [pricing](/pricing). | Solo EUR 99, Unlimited EUR 199 and Enterprise EUR 1,250 a year. Annual renewals are discounted, and you keep access to your version after a licence expires. | | Theme | Own compiled CSS, no theme setup. | Install guide lists importing its CSS into your theme. | ## Migrating There is no dedicated importer for ralphjsmit's item table. Use the source that matches how your files are stored: * Spatie driver: `php artisan media-library:import spatie`, optionally with `--model` and `--collection`. See [Importing existing files](/guide/importing). * Storage driver or any folder of files: `php artisan media-library:import directory path/on/disk --disk=s3`. Sub-folders become library folders. Both are repeatable and support `--dry-run`. If your own tables store ralphjsmit item ids, map them with `--map` and update the columns yourself. ## When to pick which Pick ralphjsmit's product when you need to plug a library into an existing storage layout through its driver system, or you want its action-class customization model and its price suits you. Pick Media Library Pro when you want the editor, focal point, usage tracking, chunked uploads and tenant isolation in the one package, without a second paid add-on. The [overview](/compare/) has the full matrix. --- --- url: /compare/vs-tomatophp.md description: >- Media Library Pro vs tomatophp's Media Manager: who each is for, what the free plugin does well, the differences and how to migrate Spatie media. --- # Media Library Pro vs Media Manager (tomatophp) [tomatophp/filament-media-manager](https://github.com/tomatophp/filament-media-manager) is a free Filament plugin that manages Spatie media through a GUI. It supports Filament 4 and 5. It suits projects that already use Spatie media and want folders and an input that picks from them. For the full matrix and the note on the ralphjsmit product, see the [comparison overview](/compare/). ## What it does well * **Spatie underneath.** "Manage your media files using spatie media library" through a GUI, so existing collections keep working. * **Folders.** Folders and optional subfolders (`->allowSubFolders()`), with auto-created folders for a model, collection or record. * **Access control.** Password-protected folders, and per-user folder access with `->allowUserAccess()` and the `InteractsWithMediaFolders` trait. * **Two components.** `MediaManagerInput` uploads directly and `MediaManagerPicker` browses and selects. Several pickers on one page can use separate collections. * **Extras.** RTL and multi-language support, dark mode, drag reordering, previews, responsive images and min/max selection validation. * **Free and open source.** ## Differences that matter | | Media Library Pro | tomatophp Media Manager | |---|---|---| | Filament | 4 and 5 | 4 and 5 | | Folders | Nested, drag to move, private folders | Folders and optional subfolders, password protection | | Private files | Per-user, with signed links and temporary S3 URLs | Password-protected folders and per-user folder access | | Tags, focal point, editor | Included | Not documented | | Usage tracking and delete guard | Included | Not documented | | Chunked uploads, duplicates, URL import | Included | Not documented | | Tenancy | Filament, stancl/tenancy or custom, with quotas | Not documented | | Table columns, infolist entries, rich editor | Included | Not documented | | Price | Commercial licence | No paid tier documented | Both are RTL-aware. This plugin ships English and Arabic and mirrors the whole layout, see [Translations, theming and RTL](/reference/translations). ## Migrating If your files are Spatie media rows, import them with the Spatie source: ```bash php artisan media-library:import spatie --model="App\Models\Post" --collection=gallery --dry-run php artisan media-library:import spatie --folder="Imported" --move ``` Each file gets a library record and an attachment that points back to its original model and collection, so existing usage is preserved. `--move` deletes the original Spatie media after copying. Folder structure is not carried over: files land in `--folder`, or in `Imported/` by default. For files that live in plain directories, use the directory source, which turns sub-folders into library folders. See [Importing existing files](/guide/importing). Swap your `MediaManagerInput` fields for [`MediaPicker`](/guide/picker-field) after the import. ## When to pick which Pick tomatophp's Media Manager when it is free, your files are already Spatie media, and folders with a password are the main thing you need. Pick Media Library Pro when you need tags, a focal point, an editor, usage tracking, large uploads, tenant isolation, or table and infolist components that go with the picker. See the [overview](/compare/). --- --- url: /legal/terms.md description: >- Media Library Pro terms and conditions: Paddle as merchant of record, one-time Single, Studio and Agency licenses, updates, support and liability. --- # Terms & Conditions *Last updated: October 10, 2026* These Terms & Conditions ("Terms") govern the purchase and use of **Media Library Pro**, a commercial software package for the Filament admin panel framework ("the Software"), sold by **Wiser Pocket** (owner: Hoceine EL IDRISSI), operating the Media Library Pro brand ("we", "us"). By purchasing or using the Software you agree to these Terms. ## 1. Merchant of record Orders are processed by our online reseller and merchant of record, **Paddle.com Market Ltd.** ("Paddle"). Paddle handles payment processing, billing inquiries, invoices, sales tax/VAT, and customer payment data. Paddle's [Terms of Use](https://www.paddle.com/legal/checkout-buyer-terms) apply to every purchase in addition to these Terms. ## 2. The product Media Library Pro is a developer tool distributed as a Composer package from our private package registry (`packages.hoceine.com`). After purchase you receive a **license key** by email, which authenticates package installation and updates. ## 3. Licenses All licenses are **one-time purchases** that grant a perpetual, non-exclusive, non-transferable right to use the version(s) of the Software released during your update window: * **Single project** — use of the Software in **one** production project (one root domain or application), plus unlimited local/staging environments for that project. Includes **12 months** of updates. * **Studio** — use of the Software in up to **five** production projects owned by you or built by you for your clients, plus unlimited local/staging environments for those projects. Includes **24 months** of updates. * **Agency** — use of the Software in **any number** of production projects owned by you or built by you for your clients. Includes **lifetime updates**: the update window never ends and there is nothing to renew. The update window runs from the date of purchase and covers updates and bug fixes. After it ends, the Software keeps working — the licence is perpetual, nothing is disabled and no attribution is added — you simply stop receiving versions released after that date until you renew. Renewal is optional and adds another window of the same length; it never changes your license key. You may **not**: redistribute, resell, sublicense, or publish the Software's source code; share your license key publicly; or use the Software to build a product whose primary purpose is to compete with Media Library Pro. ## 4. Delivery Delivery is electronic and immediate: your license key and installation instructions are emailed to the address used at checkout, normally within minutes of payment. No physical goods are shipped. ## 5. Refunds See our [Refund Policy](/legal/refund-policy). In short: 14 days, no questions asked, processed by Paddle. ## 6. Support We provide reasonable email support for installation and bug reports during your update window. Support does not include building your application for you, custom development, or consulting. ## 7. Disclaimer & limitation of liability The Software is provided **"as is"**, without warranty of any kind, express or implied, including merchantability or fitness for a particular purpose. To the maximum extent permitted by law, our total liability arising out of these Terms or your use of the Software shall not exceed the amount you paid for your license. We are not liable for indirect, incidental, or consequential damages, or for loss of data, profits, or revenue. ## 8. Changes We may update these Terms from time to time. The version published on this page at the time of your purchase applies to that purchase. ## 9. Contact Questions about these Terms: **hello@hoceine.com**. Billing questions can also be directed to Paddle via [paddle.net](https://paddle.net). --- --- url: /legal/privacy.md description: >- Media Library Pro privacy policy: what purchase and license data we collect via Paddle, registry logs, no telemetry or analytics, and your rights. --- # Privacy Policy *Last updated: October 10, 2026* This policy explains what data **Media Library Pro** (operated by **Wiser Pocket**, owner: Hoceine EL IDRISSI) collects and how it is used. ## What we collect **When you purchase a license**, our merchant of record **Paddle.com Market Ltd.** processes your payment. Paddle collects your name, email address, country, and payment details under its own [privacy policy](https://www.paddle.com/legal/privacy). We never see or store your card details. From Paddle we receive and store only what we need to deliver the product: * your **email address** and name, * the product/tier purchased and the transaction identifier, * the **license key** we generate for you. **When you install the package**, our registry (`packages.hoceine.com`) receives your license key and email (as Composer HTTP-basic credentials) plus standard web server logs (IP address, user agent, requested version). We use this solely to authenticate downloads and operate the service. **This documentation site** is a static site. It does not set cookies and does not run third-party analytics. **On the pricing page and in checkout**, we count anonymous funnel steps (page viewed, plan clicked, checkout opened, email entered, payment chosen, purchase completed or checkout closed) to see where buyers drop off. Each browser tab gets a random id that lives only for that tab; no cookies are set, and no IP address, email or name is stored with these counts, which are deleted after 180 days. If your browser sends a Global Privacy Control or Do Not Track signal, nothing is counted. ## What we don't do * We don't sell or rent your data to anyone. * We don't send marketing email — license and service emails only (e.g. your key, important security notices). * We don't collect telemetry from the Software itself: Media Library Pro runs entirely inside your own application and phones nothing home. ## Data retention & your rights We keep purchase and license records for as long as your license is active and as required for accounting. You can ask us at any time to access, correct, or delete the personal data we hold about you by emailing **hello@hoceine.com**. Deleting your data deactivates your license, since we can no longer authenticate it. ## Third parties we rely on * **Paddle** — payments, invoices, tax (merchant of record) * **Hetzner / Laravel Forge** — hosting of the package registry * **Cloudflare** — DNS and hosting of this website ## Contact Privacy questions: **hello@hoceine.com**. --- --- url: /legal/refund-policy.md description: >- Media Library Pro refund policy: a 14-day money-back guarantee, no questions asked. Request by email or Paddle; the license key is then deactivated. --- # Refund Policy *Last updated: October 10, 2026* We want you to be happy with Media Library Pro. ## 14-day money-back guarantee If Media Library Pro isn't a fit, contact us within **14 days of purchase** and we'll refund your order in full — no questions asked. ## How to request a refund Either of the following works: 1. Email **hello@hoceine.com** from the address you used at checkout, including your order number or license key; or 2. Contact **Paddle**, our merchant of record, directly via [paddle.net](https://paddle.net) using the receipt they emailed you. Refunds are processed by Paddle back to your original payment method, typically within 5–10 business days depending on your bank. ## After a refund When an order is refunded, the associated license key is deactivated and access to the package registry for that key ends. Software already installed must no longer be used in production. ## Late requests Requests after the 14-day window are handled case by case — write to us and we'll do our best to find a fair outcome. --- --- url: /guide/introduction.md description: >- Media Library Pro is a media library for Filament v4 and v5 panels: folders, tags, chunked uploads, an inspector and image editor, pickers, usage tracking and tenancy. --- # Introduction Media Library Pro adds a media library to Filament panels. Upload a file once, then reuse it in forms, tables, infolists, the rich editor and Blade views. It is built on [spatie/laravel-medialibrary](https://spatie.be/docs/laravel-medialibrary), so files, disks and conversions work the way you already know. ## Who it's for * **Content teams**: editors reuse the same images and documents across posts, pages and products instead of uploading copies. * **Agencies**: ship one library to every client panel, with permissions, private files and storage limits. * **SaaS apps**: each tenant gets its own library and its own storage quota. * **Sites with their own front end**: the picker and the library also run on your Livewire and Blade pages, outside any panel. ## What's included **Library page** * A folder tree with drag and drop, plus the smart views Recently added, Favorites, My uploads, Unused and Trash. * Search, type chips, a tag filter, six sort orders, grid and list layouts, and a thumbnail size slider. * Click, Shift and Cmd/Ctrl selection with a floating bulk bar. * Keyboard shortcuts and a full-screen preview. **Uploads** * Drop files or folders, paste from the clipboard, browse, or import from a URL. * Chunked uploads, several files at once, with a progress tray. * Duplicate detection by checksum, with a per-file choice. * Blocked executable extensions, content sniffing, SVG sanitising, a size limit and an optional storage quota. **Inspector** * Title, alt text, caption, description, tags, folder and visibility, plus your own metadata fields. * Focal point with crop previews, and an image editor for crop, rotate and flip. * Version history with restore, a "Used in" list and EXIF data. **In your app** * A `MediaPicker` form field, table columns, infolist entries and a rich editor plugin. * Blade components that render `srcset`, alt text and the focal point. * A `HasMediaLibrary` trait for attaching files to any model. **Safety and scale** * Files that are still in use cannot be deleted by accident. * Trash with restore, and a scheduled prune command. * "Only me" files and folders, served through short-lived signed links. * Filament panel tenancy, stancl/tenancy, or your own resolver. * Local, public, S3 and S3-compatible disks. * English and Arabic, with right-to-left layout, light and dark mode. ## Requirements | Package | Version | |---|---| | PHP | 8.2, 8.3, 8.4, 8.5 | | Laravel | 12, 13 | | Filament | 4.x, 5.x | | Livewire | 3.x, 4.x | | spatie/laravel-medialibrary | 11.13+ | | Database | SQLite, MySQL 8+, PostgreSQL | | Storage | Any Laravel disk, including S3 and S3-compatible services | Current Chrome, Edge and Firefox are supported. Safari 17+ supports every browser feature the library uses. Next: [Installation](/guide/installation). --- --- url: /guide/installation.md description: >- Install Media Library Pro from its private Composer repository with your licence key, run the installer, register the plugin and publish the assets. --- # Installation Media Library Pro is a paid package served from a private Composer repository. Your purchase email holds a licence key; your username is the email you bought with. ## Add the repository ```bash composer config repositories.media-library composer https://packages.hoceine.com ``` ## Add your licence ```bash composer config http-basic.packages.hoceine.com you@example.com YOUR-LICENCE-KEY ``` This writes to the project's `auth.json`. Keep that file out of git, or add `--global` to store the credentials once for every project on your machine. ## Require the package ```bash composer require hoceineel/filament-media-library-pro ``` ## Run the installer ```bash php artisan media-library:install ``` The installer does five things: 1. Publishes `config/filament-media-library.php`. 2. Publishes Spatie's `config/media-library.php` if you do not have one yet. 3. Publishes Spatie's `create_media_table` migration if your app has none yet. 4. Asks `Run the migrations now?` and runs `php artisan migrate` if you accept. The library's own tables load from the package, so they are migrated with the rest. 5. Runs `storage:link` when a `public` disk exists and `public/storage` does not. It finishes by printing the next steps. Pass `--force` to overwrite a config file you published earlier. In a script, add `--no-interaction` so the migration prompt takes its default and does not wait for input. ::: tip Custom key types If your users or tenants use UUID or ULID keys, set `key_types` in the published config **before** you migrate. Answer `no` to the migration prompt, edit the config, then run `php artisan migrate`. ::: ## Register the plugin Add the plugin to your panel provider: ```php use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; use Filament\Panel; public function panel(Panel $panel): Panel { return $panel ->plugin( FilamentMediaLibraryPlugin::make() ->navigationGroup('Content') ->navigationCountBadge() ); } ``` This adds the **Media library** page to the panel at `/media-library` under your panel path. If the panel uses Filament tenancy, the library scopes itself to the current tenant with no extra call. For stancl/tenancy or your own tenant lookup, add `->tenancy()` and `->resolveTenantUsing()`. See [Tenancy](/guide/tenancy) and the [plugin reference](/reference/plugin). ## Publish the assets ```bash php artisan filament:assets ``` Run this again after every update. ## Run a queue worker Thumbnails are generated while the upload request runs, so new files appear at once. Larger conversions, such as the `preview` size, are queued. Keep a worker running: ```bash php artisan queue:work ``` ## Schedule the cleanup In `routes/console.php`: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('media-library:prune')->daily(); ``` The command deletes trash older than 30 days and abandoned upload chunks. See [Commands](/reference/commands). ## Check it works 1. Open the panel and click **Media library** in the navigation. 2. Drop an image onto the page. It should upload and open in the details panel. 3. Run `php artisan route:list --name=media-library` and confirm the upload routes are listed. If a thumbnail stays blank, check that the queue worker is running and that `storage:link` has been run. For more, see [Troubleshooting](/reference/troubleshooting). To build your first form field, continue with the [Quick Start](/guide/quick-start). ## Deploying Servers and CI need the same credentials. Either commit nothing and set them per environment: ```bash composer config --global http-basic.packages.hoceine.com you@example.com YOUR-LICENCE-KEY ``` or provide them as JSON in the `COMPOSER_AUTH` environment variable: ```bash COMPOSER_AUTH='{"http-basic":{"packages.hoceine.com":{"username":"you@example.com","password":"YOUR-LICENCE-KEY"}}}' ``` On Laravel Forge, add them in the server's or site's Composer package authentication settings. After each deploy, run `php artisan migrate --force` and `php artisan filament:assets`, and restart your queue workers. ## Updates Your licence includes updates for its update window. Releases published after the window ends stay hidden until you renew; versions you already have keep installing. ## Publishing files ```bash php artisan vendor:publish --tag=filament-media-library-config php artisan vendor:publish --tag=filament-media-library-translations php artisan vendor:publish --tag=filament-media-library-views ``` English and Arabic ship in the box. See [Translations](/reference/translations). --- --- url: /guide/quick-start.md description: >- Go from a fresh install to a working picker: upload a file, add a MediaPicker to a resource form, then show the files in a table column and an infolist. --- # Quick Start This page takes a `Post` resource from nothing to a cover image and a gallery that editors pick from the library. It assumes you finished [Installation](/guide/installation). ## Upload a file Open **Media library** in the panel navigation and drop an image anywhere on the page. A progress tray appears, and when the upload finishes the file opens in the details panel. Give it alt text and click **Save changes**. You can also click **Upload**, paste an image from the clipboard, or use the arrow beside the button to import from a URL. See [Uploading](/guide/uploading). ## Add the database column A single file is stored as an id on your own table. Add a nullable column for it: ```php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::table('posts', function (Blueprint $table): void { $table->unsignedBigInteger('cover_id')->nullable(); }); } }; ``` A gallery needs no column. It is stored as attachments, which only needs a trait on the model: ```php use Hoceineel\FilamentMediaLibrary\Concerns\HasMediaLibrary; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasMediaLibrary; protected $fillable = ['title', 'cover_id']; } ``` ## Add the form field ```php use Filament\Forms\Components\TextInput; use Filament\Schemas\Schema; use Hoceineel\FilamentMediaLibrary\Filament\Forms\Components\MediaPicker; public static function form(Schema $schema): Schema { return $schema->components([ TextInput::make('title')->required(), MediaPicker::make('cover_id') ->image() ->aspectRatio('16:9'), MediaPicker::make('gallery') ->relationship() ->multiple() ->reorderable() ->maxItems(12), ]); } ``` `cover_id` runs in column mode: the field saves one id on the `posts` row. `gallery` runs in relationship mode: it saves ordered attachments in a collection named after the field. Both modes record where each file is used, so the library shows "Used in" and refuses to delete the file by accident. Open a post. The field opens the full library in a modal, where people can search, open folders, and upload new files that are selected automatically. Details are in [Picker field](/guide/picker-field). ## Show it in a table ```php use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Hoceineel\FilamentMediaLibrary\Filament\Tables\Columns\MediaColumn; use Hoceineel\FilamentMediaLibrary\Filament\Tables\Columns\MediaCountColumn; public static function table(Table $table): Table { return $table->columns([ MediaColumn::make('cover_id')->square(), TextColumn::make('title'), MediaCountColumn::make('photos')->collection('gallery'), ]); } ``` `MediaColumn` extends Filament's `ImageColumn`, so its options work. Each column loads the files for the whole page in one query. See [Table columns](/guide/table-columns). ## Show it in an infolist ```php use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Schema; use Hoceineel\FilamentMediaLibrary\Filament\Infolists\Components\MediaEntry; use Hoceineel\FilamentMediaLibrary\Filament\Infolists\Components\MediaGalleryEntry; public static function infolist(Schema $schema): Schema { return $schema->components([ TextEntry::make('title'), MediaEntry::make('cover_id')->conversion('preview'), MediaGalleryEntry::make('gallery') ->collection('gallery') ->gridColumns(4), ]); } ``` The gallery opens a full-screen preview on click. See [Infolist entries](/guide/infolist-entries). ## Use it in Blade ```blade ``` The component renders `srcset`, the alt text and the focal point. See [Models and Blade](/guide/models-and-blade). ## Where next * [The library page](/guide/library) for searching, selecting and bulk actions. * [Organizing](/guide/organizing) for folders, tags and trash. * [Rich editor](/guide/rich-editor) to insert library images into rich text. * [Outside panels](/guide/outside-panels) to use the picker on your own pages. --- --- url: /guide/library.md description: >- How the library page works: layouts, search, type chips, tags, sorting, selecting files, bulk actions, drag to folders, smart views and picker mode. --- # The library page The **Media library** page is where people browse and manage files. The same browser also opens inside the [picker field](/guide/picker-field) and on pages [outside panels](/guide/outside-panels). ## Layout and tile size The toolbar has a **Grid** and **List** switch. Grid shows thumbnails; list shows name, type, size, dimensions and age. A slider next to the switch changes the thumbnail size from 120 to 280 pixels in grid layout. The chosen layout and tile size are remembered per browser. Set the defaults for everyone on the plugin: ```php use Hoceineel\FilamentMediaLibrary\Enums\BrowserLayout; use Hoceineel\FilamentMediaLibrary\Enums\SortOrder; use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; FilamentMediaLibraryPlugin::make() ->defaultLayout(BrowserLayout::List) ->defaultSort(SortOrder::NameAsc) ->tileSize(220) ->perPage(24); ``` The library loads `perPage` files at a time (48 by default) and loads more as you scroll, or when you press **Load more**. ## Search Type in the search box, or press `/` to jump to it. The search matches, anywhere in the text: * the file's title, * its file name, * its alt text, * its caption, * the names of its tags. It does not search descriptions or custom metadata. Searching looks across every folder, not just the open one, and the results header replaces the folder path. Opening a folder clears the search. ## Type chips The chips under the toolbar filter by **Images**, **Videos**, **Audio**, **Documents**, **Archives** and **Other**. Select several to show any of them. In a picker that only accepts images, only the matching chips appear. **Clear filters** resets search, chips and tags together. ## Tags The tag cloud in the sidebar lists up to 30 tags with the number of files in each. Click a tag to filter. Select several tags to show files that have at least one of them. See [Organizing](/guide/organizing#tags). ## Sorting The sort button offers Newest first, Oldest first, Name A-Z, Name Z-A, Largest first and Smallest first. The sort order is not remembered between visits; it starts from the plugin's `defaultSort`. ## Selecting files | Action | Result | |---|---| | Click a file | Selects it and opens its details. | | Shift + click | Selects the range from the last file you clicked. | | Cmd/Ctrl + click | Adds or removes one file. | | Click the check circle on a tile | Adds or removes one file without a modifier key. | | Cmd/Ctrl + A | Selects every file currently loaded. | | Click empty space, or press Esc | Clears the selection. | | Double-click | Opens the full-screen preview. | Select all covers the files that are loaded, so scroll or press **Load more** first if you need more. ## Bulk actions With two or more files selected, a bar floats over the bottom of the grid. Selecting through the check circle shows it for a single file too. | Action | What it does | |---|---| | Move | Moves the files to a folder you choose. Needs the `Folders` feature. | | Tag | Adds tags to every selected file. Existing tags stay. Needs `Tags`. | | Download | Downloads one file as is, or up to 500 files as a ZIP. Needs `Download`. | | Visibility | Sets the files to **Shared** or **Only me**. See [Private files](/guide/private-files). | | Delete | Moves the files to the trash after you confirm. | In the Trash view the bar offers **Restore** and **Delete forever** instead. ## Drag to folders Drag one or more selected files onto a folder tile, a folder in the sidebar, a breadcrumb, or **All folders** to move them. Drag folders the same way to nest them. Dragging files from your computer onto a folder uploads straight into that folder. ## Smart views The sidebar lists views above the folder tree: | View | Shows | |---|---| | All media | Everything, by folder. | | Recently added | Files uploaded in the last 14 days. | | Favorites | Files you starred. Favorites are per user. | | My uploads | Files you uploaded. | | Unused | Files not attached anywhere. Safe to delete. Hidden in pickers. | | Trash | Deleted files, with restore. | Recently added, My uploads and Unused need the `SmartViews` feature. Favorites needs `Favorites`, Unused also needs `Usage`, and Trash needs `Trash`. Counts appear beside **All media** and **Trash**. See [Features](/reference/plugin). ## Empty states An empty library shows an upload button, a **New folder** button and a tip about dragging and pasting. An empty folder, an empty smart view and an empty trash each explain what belongs there. A search or filter with no match offers **Clear filters**. ## Picker mode and manage mode The page runs in **manage** mode. The picker modal runs in **pick** mode, and it changes some behaviour: * Clicking a file selects it and does not open details. Double-click, or **Enter**, inserts it. * A footer shows the selection and an **Insert** button, with a limit such as `3 / 12` when the field has `maxItems`. * Bulk actions, the Unused view and folder menus are hidden. * Only the accepted file types are listed. New uploads made in a picker are selected for you. Next: [Uploading](/guide/uploading). --- --- url: /guide/uploading.md description: >- Upload by drop, paste or browse, send files in chunks, handle duplicates, import from a URL, replace files, and keep storage under a quota. --- # Uploading Files upload from the library page, from the [picker field](/guide/picker-field) and from pickers [outside panels](/guide/outside-panels). All of them use the same upload routes, the same checks and the same progress tray. ## Ways to add files * **Drop** files or folders anywhere on the page. A drop overlay names the destination folder. Drop onto a folder tile or a sidebar folder to upload into it. * **Paste** an image from the clipboard while the library is visible and no text field has focus. * **Browse** with the **Upload** button. * **Upload a folder** from the arrow beside the button. Every file inside is uploaded to the folder you are in. Sub-folders are not recreated; use [Importing](/guide/importing) to bring in a directory tree. * **Import from URL** from the same menu. New files go into the folder that is open. A `.DS_Store` file is ignored. ## Chunked uploads Each file is cut into chunks and sent one chunk at a time, so large files do not hit PHP's `upload_max_filesize` or a proxy's body limit. Several files upload in parallel. The server checks the name, size and chunk count on the first chunk, rejects any upload that grows past the size it declared, and lets one person have 20 unfinished uploads at a time. If a transfer fails, the tray shows **Try again**, which starts that file over. The settings live under `upload` in `config/filament-media-library.php`: | Key | Default | Meaning | |---|---|---| | `max_file_size` | `512 * 1024` | Largest file, in kilobytes. | | `chunk_size` | `5 * 1024 * 1024` | Bytes per chunk. Keep it at 256 KB or more. | | `max_parallel_uploads` | `3` | Files sent at the same time. | | `accepted_mime_types` | images, video, audio, PDF, ZIP, text, Office | Allowed types. Wildcards such as `image/*` work. | | `chunk_directory` | `media-library-chunks` | Folder under `storage/app` for partial uploads. | Override size and types per panel on the plugin. The size is in kilobytes: ```php FilamentMediaLibraryPlugin::make() ->acceptedFileTypes(['image/*', 'application/pdf']) ->maxFileSize(20 * 1024); ``` The browser checks size and type before it sends anything, and the server checks again. A file that fails shows its reason in the tray. Make sure `chunk_size` is below your server's `post_max_size` and upload limits. Partial uploads that are never finished are removed by `media-library:prune`. See [Commands](/reference/commands). ## Progress tray The tray sits at the bottom corner. Its header reads `Uploading 2 of 5`, then `Uploads complete`, and it clears itself a few seconds after everything succeeds. Each row has a thumbnail, a bar, **Cancel** while it runs, and **Try again** on failure. When a file needs a decision, the header reads `Waiting for your decision`. The tray is announced to screen readers as a live region. ## Duplicates The library compares each new file's SHA-1 checksum with the files the person can see in the current library (and tenant). The `duplicates` option picks what happens on a match: | Strategy | Behaviour | |---|---| | `DuplicateStrategy::Ask` | Default. The tray shows "Already in the library." with **Use existing** and **Keep both**, per file. | | `DuplicateStrategy::UseExisting` | Never stores a second copy. The tray shows "Using the existing file". | | `DuplicateStrategy::Allow` | Skips the check and stores every file. | Set it on the plugin, or with `upload.duplicates` in the config: ```php use Hoceineel\FilamentMediaLibrary\Enums\DuplicateStrategy; use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; FilamentMediaLibraryPlugin::make() ->duplicates(DuplicateStrategy::UseExisting); ``` A file waiting for a decision is held for six hours. After that, upload it again. Replacing a file and importing from a URL skip the duplicate check. ## Import from URL Open the arrow beside **Upload** and choose **Import from URL**. Paste an `http` or `https` link. The file is downloaded by the server, checked like any upload, and stored in the open folder. The `UrlImport` feature turns it on and off, and `upload.url_import` is its older config switch. The import is guarded against server-side request forgery: * Only `http` and `https` links are accepted. * The host is resolved first, and every address must be public. Private, loopback, link-local, carrier-grade NAT and other reserved ranges are refused, including IPv6 forms of them. * The request connects to the address that was checked, so DNS cannot change between the check and the download. * Redirects are followed manually, up to three, and each one is checked again. * The download stops at `max_file_size` and times out after 30 seconds. A refused link shows "Enter a public http or https link." See [Security](/guide/security). ## Replace a file In the details panel, **Replace file** uploads a new file into the same item. The item keeps its id, metadata and every place it is used. The old file is kept as a version when `Versions` is on. See [Inspector](/guide/inspector#versions). ## Storage quota Set `quota` to a number of bytes, `null` for unlimited, or an invokable class that receives the tenant: ```php 'quota' => 5 * 1024 ** 3, ``` When a quota is set, the sidebar shows a meter with used and total storage. It switches to a warning style at 80% and a danger style at 95%. Used storage counts files in the trash and old versions, so empty the trash to free space. When a file is larger than the space left, the upload is refused with "Not enough storage left. Delete unused files or ask an admin for more space." Quotas are per tenant. See [Tenancy](/guide/tenancy). ## Blocked files These checks cannot be bypassed from the browser: * **Blocked extensions.** `php`, `phtml`, `phar`, `sh`, `exe`, `html`, `js`, `xml`, `svgz` and others in `blocked_extensions` are refused. So is a file with no extension, or an unusual one. * **Content sniffing.** The type is read from the file's bytes, not its name or its declared type, and is then matched against `accepted_mime_types`. * **SVG sanitising.** SVG files are cleaned of scripts and remote references. If a file cannot be made safe, it is refused. Turn this off with `sanitize_svg`. The tray shows "Files ending in .exe cannot be uploaded." or "This file type (application/x-foo) is not allowed here." Next: [Organizing](/guide/organizing). --- --- url: /guide/organizing.md description: >- Organize files with nested folders, tags and favorites, delete safely with the trash and the in-use guard, and schedule the prune command. --- # Organizing Folders, tags and favorites are three ways to find a file again. Each one can be switched off with a [feature flag](/reference/plugin). ## Folders Folders nest inside each other. The sidebar shows the tree, and the main area shows the sub-folders of the folder you are in, with item counts. **Create.** Click the **+** in the sidebar heading, choose **New folder** from the arrow beside **Upload**, or use the button in an empty library. The new folder is created inside the folder you are viewing. A folder has a name (up to 120 characters), an optional colour and, when private files are on, a visibility. Creating a folder whose name already exists in the same parent is refused. **Edit.** Each folder tile has a menu with **Edit folder**, **Move** and **Delete folder**. Editing changes the name, colour and visibility. **Move.** Drag a folder onto another folder or onto a sidebar entry, or use **Move** to pick a destination. Drop on **All folders** to return it to the top level. A folder cannot be moved into itself or into one of its own sub-folders. **Delete.** Deleting a folder deletes its sub-folders too. The files inside are not lost: they move to the trash and are listed there at the top level. The delete is refused when: * any file in the folder tree is still used somewhere, or * the tree holds private files or folders that belong to someone else. Move or detach those files first. If the `Trash` feature is off, the files are deleted for good. To move files, drag them onto a folder, use **Move** in the bulk bar, or change **Folder** in the details panel. See [The library page](/guide/library#drag-to-folders). ## Tags Tags are free-form labels. Add them in the details panel, where the field suggests existing tags, or to many files at once with **Tag** in the bulk bar. Typing a name that does not exist creates it. Bulk tagging adds tags and never removes any; to remove a tag, edit the file in the details panel. The sidebar tag cloud filters the grid, and the search box matches tag names. See [The library page](/guide/library#tags). ## Favorites Click the star on a tile, or the star in the details panel, to favorite a file. Favorites belong to each user, so your stars do not change what a colleague sees. The **Favorites** view lists them. ## Trash Deleting a file moves it to the trash. Open **Trash** in the sidebar to see it. * **Restore** puts files back in their original folder. If that folder was deleted, they return to the top level. * **Delete forever** removes the files and all their sizes. It cannot be undone. * **Empty trash** deletes every file in the view for good, after you confirm. * Trashed files cannot be edited, favorited or dragged, and they cannot be picked in a form field. Files stay in the trash for 30 days by default, then the prune command removes them. They still count toward your storage quota until then. ### Prune schedule Register the command once, in `routes/console.php`: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('media-library:prune')->daily(); ``` Change the retention in the config, or for one run with `--days`: ```php 'trash' => [ 'enabled' => true, 'prune_after_days' => 30, ], ``` ```bash php artisan media-library:prune --days=7 ``` The command also clears upload chunks left behind by interrupted uploads. See [Commands](/reference/commands). To skip the trash entirely, call `disableFeatures(Feature::Trash)` on the plugin. Deleting then removes files immediately. ## The in-use guard The library records where each file is used: every `MediaPicker`, every attachment made with the `HasMediaLibrary` trait, and every import that attaches files to a model. A tile shows a link badge with the count, the details panel lists the places under **Used in**, and the **Unused** view finds files that are safe to delete. When you delete files that are still used, the dialog says how many and refuses to continue until you turn on **Remove from those places and delete**. The server enforces the same rule, so a hidden button cannot bypass it. What the option does depends on the delete: * **Delete to trash.** The file moves to the trash and its usage links are kept. Those places show no file until someone picks a new one, and restoring the file puts it back where it was used. * **Delete forever, Empty trash and prune.** The usage links are removed with the file. Deleting a folder never offers this option. If a file inside it is in use, the folder delete is refused. Next: [Inspector](/guide/inspector). --- --- url: /guide/inspector.md description: >- The details panel: edit titles, alt text and tags, add your own metadata, set a focal point, crop and rotate images, restore versions and see where a file is used. --- # Inspector Click a file in the library to open the inspector beside the grid. It has the file's preview, a row of quick actions, a form, and read-only sections for details, usage, versions and camera data. Press **Esc** to close it. When the user cannot update the file, or the file is in the trash, the form is read-only. In the trash the panel offers **Restore** and **Delete forever** instead. ## Fields | Field | Notes | |---|---| | Title | Required, up to 255 characters. This is the file's display name. | | Alt text | Images only, up to 1,000 characters. Used by every component that renders the image. | | Caption | Up to 1,000 characters. Shown by `MediaGalleryEntry::captions()`. | | Description | Free text. | | Tags | Suggests existing tags; a new name creates a tag. | | Folder | Moves the file. Hidden when folders are off or the picker is locked to a folder. | | Visibility | **Shared** or **Only me**. See [Private files](/guide/private-files). | Click **Save changes** to store the form. With AI alt text configured, a **Write it for me** link appears beside the alt field. See [Alt text](/guide/alt-text). ### Your own metadata Add fields to the inspector with `metadataSchema()`. They are stored in the item's `custom_properties` column, under their own names. ```php use Filament\Forms\Components\TextInput; use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; FilamentMediaLibraryPlugin::make() ->metadataSchema(fn (): array => [ TextInput::make('credit')->label('Photo credit'), TextInput::make('license'), ]); ``` Read them from the model: ```php $credit = $item->custom_properties['credit'] ?? null; ``` ## Quick actions The icon row above the form has **Download**, **Copy link**, **Open original**, a star for favorites, **Crop and rotate** for editable images, **Replace file**, **Duplicate** and **Delete**. Each one honours its feature flag and the user's permissions. **Copy link** copies the absolute URL. A private file's link expires. **Duplicate** creates a copy named "Name (copy)" with the same file, metadata and tags, and opens it. ## Focal point Click anywhere on the preview image to set the focal point. It saves at once, and a marker shows where it is. Four crop previews (1:1, 16:9, 4:5 and 3:1) show how the image crops around that point. **Reset** clears it. The focal point drives: * every conversion with `'fit' => 'crop'`, such as the default `thumb`, which is cropped around the point instead of the centre; * the `object-position` of thumbnails in the library; * the `object-position` of `x-media-library-image`. Cropped conversions are rebuilt on the queue when the point changes, so keep a worker running. Conversions with `'fit' => 'contain'` are not cropped and are unaffected. See [Models and Blade](/guide/models-and-blade). ## Image editor **Crop and rotate** opens a full-screen editor for any image except SVG and GIF. * **Aspect ratio**: Free, 1:1, 4:3, 3:2, 16:9, 4:5 and 9:16, plus one chip for each cropped conversion in your config, such as Thumb. * **Tools**: rotate left and right by 90 degrees, flip horizontally and vertically, zoom in and out, and reset. * **Output**: JPEG, PNG or WebP, matching the original when it is one of those and JPEG otherwise. Output is capped at 8,192 pixels on a side. Choose how to save: | Button | Result | |---|---| | **Save** | Replaces the file. The original is kept as a version. | | **Save as copy** | Creates a new file named "Name (copy)" that inherits the folder, alt text, caption, description and visibility. | | **Cancel** | Discards the edit. | The editor reads pixels in the browser, so files on another domain, such as S3 or a CDN, need CORS rules that allow your panel origin. See [Storage](/guide/storage). Without them the editor shows "This image is served from another domain without CORS, so the edited version cannot be saved." ## Versions Replacing a file, including from the editor, keeps the previous file. The **Previous versions** section lists each one with its name, size and date. **Restore** asks you to confirm, then swaps the version in. The file that was current becomes a version, so you can switch back. The item keeps its id and every place it is used. The library keeps the 10 most recent versions. Change that with `versions.keep` in the config. Turn versions off with the `Versions` feature, and replacing a file then discards the old one. ## Used in The **Used in** section lists every record the file is attached to, with the record type and collection. The label comes from the record's `title`, `name`, `label`, `slug` or `email`. When a Filament resource exists for the record and the user may edit or view it, the label links there. An unused file reads "Not attached anywhere yet. Safe to delete." ## Camera data For JPEG and TIFF images, a collapsible **Camera data** section shows make, model, lens, exposure, aperture, ISO, focal length, date taken, software, artist and copyright. Location data is not read. The section only appears when the file has some. ## Details The read-only **Details** section shows type and MIME type, size, dimensions, file name, when it was added, who uploaded it and when it was last edited. --- --- url: /guide/private-files.md description: >- Mark files and folders Only me, decide who can see them with the viewPrivate ability, and understand signed links and the public-disk caveat. --- # Private files Every file and folder is either **Shared** or **Only me**. Shared is the default. A private record is visible only to the user who created it, and to users who pass the `viewPrivate` ability. ## Make something private * **A file**: change **Visibility** in the [inspector](/guide/inspector), or select files and use **Visibility** in the bulk bar. * **A folder**: choose **Visibility** when you create the folder, or in **Edit folder**. Uploads start as Shared. A private folder does not change the visibility of the files inside it; set those separately. Private files show a lock badge, and private folders are labelled Only me. ## Who sees what | Person | Private files and folders | |---|---| | The uploader (or folder creator) | Sees, edits, moves and deletes them. | | A user passing `viewPrivate` | Sees and manages every private record. | | Everyone else | Does not see them in the library, in pickers, in search, or in counts. | The same rule applies on the server. A form field rejects the id of a private file the user cannot see, downloads skip it, and the library actions refuse it. A folder that holds someone else's private content cannot be deleted by a user who cannot see it. ## Grant `viewPrivate` The bundled `MediaItemPolicy` returns `false` for `viewPrivate`. Extend it, return `true` for the people who need it, and register your policy: ```php namespace App\Policies; use Hoceineel\FilamentMediaLibrary\Policies\MediaItemPolicy; use Illuminate\Contracts\Auth\Authenticatable; class AppMediaItemPolicy extends MediaItemPolicy { public function viewPrivate(Authenticatable $user): bool { return $user->is_admin; } } ``` ```php use App\Policies\AppMediaItemPolicy; use Hoceineel\FilamentMediaLibrary\Models\MediaItem; use Illuminate\Support\Facades\Gate; public function boot(): void { Gate::policy(MediaItem::class, AppMediaItemPolicy::class); } ``` The library registers its own policies only when you have not. `update`, `delete`, `restore` and `forceDelete` follow `view`, so a user with `viewPrivate` can also change and delete other people's private files. Folders use `MediaFolderPolicy` with the same `viewPrivate` check. See [Authorization](/reference/authorization). ## Links to private files A private file never gets a permanent URL. Wherever the library needs a link, it creates a short-lived one: * On a **private disk** that supports temporary URLs, such as S3, it is a temporary URL. * On a disk set to `'visibility' => 'public'`, it is a signed link to the library's serve route, `/media-library/serve/{uuid}`. That route refuses a private file unless the link carries a valid signature, and it returns 404 for anything in the trash. Links last `temporary_url_minutes`, which is 30 by default. They are cached for half that time so a page of thumbnails does not sign every file again. A private file has no `srcset`, because responsive variants would each need a link. This has three consequences: * Do not use a private file on a public page. Its link stops working after it expires. * If you make a Shared file private later, permanent signed links already published for it stop working on a private disk. On a public disk the direct file URL keeps working, which is the caveat below. * **Copy link** in the inspector copies a link that expires. ## Rich text cannot embed private files The [rich editor](/guide/rich-editor) stores image URLs inside the saved content, so they must not expire. Private files have no permanent URL, so the plugin skips them when you insert. Make the file Shared first if it belongs in rich text. ## The public-disk caveat ::: warning Private files are only confidential on a private disk Files are stored under random UUID folders, so their URLs cannot be guessed or enumerated. On a disk with `'visibility' => 'public'`, though, the file still sits in a web-readable folder. Anyone who already has the exact URL can open it, and a copied path stays valid. ::: The library's signed links protect the page that lists the file. They do not move the file. When private files must stay confidential, point `disk` at a private disk, for example S3 with private visibility, or a `local` disk outside `public/`: ```php 'disk' => env('MEDIA_LIBRARY_DISK', 's3'), ``` Keep `conversions_disk` on a private disk too if you set it. See [Storage](/guide/storage). ## Turn it off Set `private_media` to `false`, or disable the feature on the plugin: ```php use Hoceineel\FilamentMediaLibrary\Enums\Feature; use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; FilamentMediaLibraryPlugin::make() ->disableFeatures(Feature::PrivateMedia); ``` The **Visibility** controls disappear. Files already marked **Only me** become visible to everyone who can open the library, so decide what to do with them before you switch it off. Next: [Keyboard](/guide/keyboard). --- --- url: /guide/keyboard.md description: >- Every keyboard shortcut in the library, how to switch shortcuts off, and what the library exposes to keyboards and screen readers. --- # Keyboard The library can be driven from the keyboard. Press `?` on the library page to open the shortcuts dialog. ## Shortcuts | Key | Action | |---|---| | `/` | Focus the search box. | | Arrow keys | Move between files. | | Shift + arrow keys | Extend the selection from the file you started on. | | Cmd/Ctrl + A | Select every loaded file. | | Space | Open the full-screen preview of the focused file. | | Enter | Open the details panel. In a picker, insert the selection. | | Delete, or Cmd/Ctrl + Backspace | Delete the selected files after you confirm. Library page only, not in a picker. | | Esc | Close the dialog, then clear the selection, then close the details panel. | | `?` | Show or hide the shortcuts dialog. | The dialog writes Select all as `⌘ A`. On Windows and Linux use Ctrl. Mouse equivalents: Shift + click selects a range, Cmd/Ctrl + click toggles one file, and double-click opens the preview (or inserts the file in a picker). ### How arrow keys move In grid layout, left and right move by one file and up and down move by one row. In list layout, up and down move by one row, and left and right also move by one. In right-to-left languages the horizontal arrows are reversed so that they match the visual direction. In manage mode, moving to a file also selects it. In a picker, moving only changes the focus ring, so the selection stays as you made it; press Enter to insert it. ### In the preview With the preview open, the left and right arrows step through the files, wrapping at the ends. Esc or Space closes it. ### In the Trash view Delete opens **Delete forever** instead of moving files to the trash. ### When shortcuts do not fire Shortcuts are ignored while: * the cursor is in a text field, select or editable area, * a modal dialog is open on the library page (in a picker, which is itself a modal, shortcuts keep working), * the library is not visible, for example behind another tab or a closed modal. Pressing Esc in the search box takes focus out of the box. Paste-to-upload is not a shortcut and keeps working when shortcuts are off. ## Turn shortcuts off Disable the `KeyboardShortcuts` feature on the plugin: ```php use Hoceineel\FilamentMediaLibrary\Enums\Feature; use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; FilamentMediaLibraryPlugin::make() ->disableFeatures(Feature::KeyboardShortcuts); ``` Or set `'keyboard_shortcuts' => false` under `features` in `config/filament-media-library.php`. Every shortcut stops working and the `/` badge disappears from the search box, except **Esc**, which still clears the selection and closes the details panel. ::: warning Arrow keys are the way into the grid Tiles are not tab stops, so the arrow keys are how a keyboard user reaches files. If you turn shortcuts off, the grid can no longer be navigated from the keyboard. Consider leaving them on. ::: ## Accessibility notes What the library exposes today: * The grid is a `listbox` with `aria-multiselectable`. Each file is an `option` with `aria-selected`, so selection changes are available to assistive technology. * Sidebar navigation, the bulk bar (`toolbar`), the type chips and the layout switch have labels. Chips and layout buttons report `aria-pressed`. * The upload tray is a polite live region, so progress and "Waiting for your decision" are announced. * The storage meter has `role="meter"` with its value. * The shortcuts dialog, the full-screen preview and the image editor are modal dialogs with labels. The preview is labelled with the file's name, and its previous and next buttons are labelled. * Images use the item's alt text. How focus works: * The library tracks the active tile itself and draws the focus ring on it. It does not move the browser's focus onto the tile, so a screen reader's cursor stays where it was. Selection state is what is exposed. * The three small controls on a tile (the check circle, the star and the details button) are mouse conveniences and are hidden from the accessibility tree. The keyboard routes are: Shift + arrows or Cmd/Ctrl + A to select, Enter then the star in the details panel to favorite, and Enter to open details. * There is no shortcut for toggling one file in a non-contiguous selection. Use Shift + arrows for ranges. * The toolbar, filters, sidebar, details form and dialogs are ordinary controls reached with Tab. Next: [Picker field](/guide/picker-field). --- --- url: /guide/picker-field.md description: >- The MediaPicker form field: single and multiple selection, file type filters, limits, relationship or column storage, folders, layouts and in-field uploads. --- # Picker Field `MediaPicker` is a Filament form field that stores a selection from the library. It opens the full library in a modal, and people can also drop, paste or upload files straight into the field. ```php use Hoceineel\FilamentMediaLibrary\Filament\Forms\Components\MediaPicker; MediaPicker::make('cover_id') ->image() ->aspectRatio('16:9'); ``` ## Single and multiple By default the field holds one file. Call `multiple()` for a list, and limit it with `minItems()` and `maxItems()`. ```php MediaPicker::make('cover_id')->image(); MediaPicker::make('gallery') ->multiple() ->image() ->minItems(1) ->maxItems(12) ->reorderable(); ``` * A single image shows as a large cover with Preview, Choose another, Upload and Remove actions. * Multiple selections show as cards or rows. Drag them to reorder, unless you call `reorderable(false)`. Reordering only applies to `multiple()` fields. * `minItems()` and `maxItems()` only apply to `multiple()` fields. A single field always allows exactly one file. ## Restrict file types `image()` is a shortcut for `acceptedFileTypes(['image/*'])`. Wildcards work. ```php MediaPicker::make('brochure') ->acceptedFileTypes(['application/pdf', 'application/zip']); ``` The type filter limits what the picker modal shows, what the field accepts when people upload, and what the server accepts when the form is saved. ## Where the selection is stored There are two storage modes. **Column mode** is the default. The field stores one id, or a JSON array of ids for `multiple()`, in the attribute with the field's name. Cast array columns on the model: ```php protected function casts(): array { return ['gallery_ids' => 'array']; } ``` ```php MediaPicker::make('cover_id')->image(); MediaPicker::make('gallery_ids')->multiple(); ``` In column mode the field also records where each file is used, so the library shows "Used in" and protects the file from deletion. The usage is saved under a collection named after the field. Turn it off with `trackUsage(false)`. **Relationship mode** syncs the `media_library_attachments` table instead and keeps the order. The model needs the `HasMediaLibrary` trait (see [Models & Blade](/guide/models-and-blade)). ```php MediaPicker::make('gallery') ->relationship() ->multiple() ->maxItems(12); MediaPicker::make('downloads') ->relationship('downloads') ->multiple() ->acceptedFileTypes(['application/pdf']); ``` Without an argument, `relationship()` uses the field name as the collection. Pass a name, or call `collection('downloads')`, to use another one. In relationship mode the field does not write to a column, and usage is always tracked. Read relationship selections with `$post->getMediaLibraryItems('gallery')`. Display columns and entries pick the same collection with `collection()`: see [Table Columns](/guide/table-columns) and [Infolist Entries](/guide/infolist-entries). ## Open in a folder ```php MediaPicker::make('downloads') ->folder('Downloads', lock: true); ``` `folder()` takes a folder id or the name of a root folder. A missing named folder is created when the user is allowed to create folders. New uploads from the field go into that folder. With `lock: true`, the picker stays inside the folder. ::: warning Locking is a convenience, not a permission boundary. Use [policies](/reference/authorization) to restrict access. ::: ## Layout and presentation Images show as a grid of cards. Other files show as a list with type, size and dimensions. The field picks a grid when every accepted type starts with `image/`, and a list otherwise. Override it: ```php MediaPicker::make('gallery')->multiple()->list(); MediaPicker::make('logos')->multiple()->grid(); MediaPicker::make('files')->multiple()->layout(PickerLayout::List); ``` `PickerLayout` lives in `Hoceineel\FilamentMediaLibrary\Enums`. The layout applies to `multiple()` fields. `aspectRatio()` frames a single selection at a fixed ratio. It accepts `16:9`, `16/9` or `1`. `conversion()` sets which size the cards show. It defaults to `thumb`. When `aspectRatio()` is set, a single cover uses the `preview` conversion so the larger frame stays sharp. ## Uploading inside the field With `uploadable()` on (the default), people can: * drop files onto the field, * paste an image from the clipboard, * press **Upload** to pick files from their device. Files upload in chunks with live progress and are added to the field when they finish. Uploads respect the field's accepted types, the maximum file size and, for `multiple()` fields, the remaining capacity from `maxItems()`. A file that does not match is rejected in the browser with a message. If the file already exists in the library (same checksum), the field uses the existing copy instead of storing a second one. It answers the duplicate prompt for the user. The exception is `upload.duplicates` set to `DuplicateStrategy::Allow`, which always stores a new copy. Uploading needs the `create` policy ability and an upload route for the current page. A disabled field is never uploadable. To only allow picking existing files, call `uploadable(false)`. ## Validation Validation always runs on the server. The field adds these rules: * Every id must exist in the library, belong to the current tenant, be visible to the user (other people's private files are rejected), and match the accepted file types. A failure uses Laravel's `validation.exists` message. * `minItems()` and `maxItems()` become `min` and `max` rules on `multiple()` fields. Use Filament's own `required()` to demand a selection. ## All options | Method | Default | Purpose | |---|---|---| | `multiple(bool)` | off | Hold a list of files instead of one. | | `minItems(int)` | none | Minimum files. `multiple()` only. | | `maxItems(int)` | none | Maximum files. `multiple()` only. | | `image()` | | Shortcut for `acceptedFileTypes(['image/*'])`. | | `acceptedFileTypes(array)` | all types | MIME filter. Wildcards such as `image/*` work. | | `reorderable(bool)` | on | Drag to reorder. `multiple()` only. | | `relationship(?string)` | off | Store selections as attachments. Needs `HasMediaLibrary`. | | `collection(string)` | field name | Attachment collection used for usage tracking and relationship mode. | | `trackUsage(bool)` | on | Record usage in column mode. | | `folder(id or name, lock: bool)` | none | Open the picker in a folder, optionally locked. | | `grid()`, `list()`, `layout(PickerLayout)` | by file types | Card grid or file list. | | `aspectRatio(string)` | none | Fixed ratio for a single selection. | | `conversion(string)` | `thumb` | Conversion shown on the cards. | | `uploadable(bool)` | on | Allow drop, paste and upload in the field. | | `previewable(bool)` | on | Show the full screen preview button. | | `showFileNames(bool)` | on | Show the file name under each card. | | `openOnMount(bool)` | off | Open the library as soon as the field renders. | | `submitOnPick(bool)` | off | Submit the surrounding action modal after files are inserted. | | `buttonLabel(string)` | "Browse library" | Text of the button that opens the library. | `openOnMount()` and `submitOnPick()` are meant for action modals. The [rich editor plugin](/guide/rich-editor) uses both. Most options also accept a closure, so you can compute them from the form state or the current record. --- --- url: /guide/table-columns.md description: >- Show library files in Filament tables with MediaColumn, MediaCountColumn and MediaFileColumn. Read ids from a column or attachments from a collection. --- # Table Columns Three columns show library files in a Filament table. They work in resource tables, relation managers and table widgets. ```php use Hoceineel\FilamentMediaLibrary\Filament\Tables\Columns\MediaColumn; use Hoceineel\FilamentMediaLibrary\Filament\Tables\Columns\MediaCountColumn; use Hoceineel\FilamentMediaLibrary\Filament\Tables\Columns\MediaFileColumn; public static function table(Table $table): Table { return $table->columns([ MediaColumn::make('cover_id')->square(), TextColumn::make('title'), MediaColumn::make('gallery')->collection('gallery')->stacked()->limit(3)->circular(), MediaCountColumn::make('photos')->collection('gallery'), MediaFileColumn::make('downloads')->collection('downloads')->downloadable()->visibleItems(1), ]); } ``` ## Where the files come from Every column reads files in one of two ways, matching how the [picker field](/guide/picker-field) stored them. | Call | Reads from | |---|---| | `MediaColumn::make('cover_id')` | The `cover_id` attribute: one id, or an array or JSON list of ids. | | `MediaColumn::make('gallery')->collection('gallery')` | The `gallery` attachment collection. The model needs the `HasMediaLibrary` trait. | With `collection()` on a model that does not use `HasMediaLibrary`, the column falls back to the attribute named in `make()`. Missing files are skipped. A row that points at a deleted id shows nothing instead of an error. Files in the trash and files from another tenant are never shown. Two options are shared by all three columns: | Method | Default | Purpose | |---|---|---| | `collection(string)` | none | Read attachments from this collection instead of an attribute. | | `conversion(string)` | `thumb` | Which image size to show. For images, falls back to the original if the conversion does not exist. | ## MediaColumn `MediaColumn` extends Filament's `ImageColumn`, so every image column option works: `square()`, `circular()`, `stacked()`, `limit()`, `limitedRemainingText()`, `height()`, `width()`, `ring()` and the rest. It shows one image per file. Files that are not images and have no thumbnail are left out. ```php MediaColumn::make('cover_id')->square()->size(48); MediaColumn::make('gallery') ->collection('gallery') ->stacked() ->limit(3) ->limitedRemainingText(); ``` ## MediaCountColumn `MediaCountColumn` extends `TextColumn`. It shows how many files are attached, as a centred numeric badge with a photo icon. The badge uses the primary colour for one or more files and gray for none. ```php MediaCountColumn::make('photos')->collection('gallery'); ``` Because it is a `TextColumn`, you can still change the colour, icon, label and other text options. The count is computed from the attachments, not read from a database column, so the column is not sortable or searchable. ## MediaFileColumn `MediaFileColumn` suits documents. Each file shows as a chip: a thumbnail for images or a type icon for other files, then the file name, and a line with type, size and dimensions. Extra files collapse into a "+2" badge. ```php MediaFileColumn::make('downloads') ->collection('downloads') ->visibleItems(2) ->showMeta(false) ->downloadable(); ``` | Method | Default | Purpose | |---|---|---| | `visibleItems(int)` | `1` | How many chips to show before the "+n" badge. | | `showMeta(bool)` | on | Show the type, size and dimensions line. | | `downloadable(bool)` | off | Make each chip a link that opens the original file in a new tab. | With `downloadable()`, clicking a chip opens the file and does not trigger the row action, so a row that links to its edit page still works. The chip is also reachable with the keyboard: focus it and press Enter. Private files use short-lived signed links. ## Loading and N+1 You do not need to eager load anything. Before the first row renders, each column loads the files for the whole page in one query: * In attribute mode, one query for every id on the page. * In `collection()` mode, one query for the attachments of every record on the page, with their media. Several columns reading the same ids share the lookup. Eager load yourself only when you read attachments elsewhere in the same table, for example in a `TextColumn::state()` closure that calls `getMediaLibraryItems()`: ```php $table->modifyQueryUsing(fn (Builder $query) => $query->with('mediaLibraryItems.media')); ``` Records you load this way are reused by the columns, so they do not query again. ::: tip Thumbnails come from the `thumb` conversion, which is generated when the file is uploaded. A table full of images stays fast because each cell only loads a small file. See [Models & Blade](/guide/models-and-blade) for the conversion presets. ::: --- --- url: /guide/infolist-entries.md description: >- Show library files on Filament view pages with MediaEntry, MediaGalleryEntry and MediaFileEntry: image, gallery grid with lightbox, and file list. --- # Infolist Entries Three entries show library files on a Filament view page or in any infolist. ```php use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Schema; use Hoceineel\FilamentMediaLibrary\Filament\Infolists\Components\MediaEntry; use Hoceineel\FilamentMediaLibrary\Filament\Infolists\Components\MediaFileEntry; use Hoceineel\FilamentMediaLibrary\Filament\Infolists\Components\MediaGalleryEntry; public static function infolist(Schema $schema): Schema { return $schema->components([ TextEntry::make('title'), MediaEntry::make('cover_id')->conversion('preview'), MediaGalleryEntry::make('gallery') ->collection('gallery') ->gridColumns(4) ->aspectRatio('4:3') ->captions(), MediaFileEntry::make('downloads')->collection('downloads'), ]); } ``` ## Where the files come from Entries read files the same way the [table columns](/guide/table-columns) do. Without `collection()`, the entry reads the attribute named in `make()`: one id, or an array or JSON list of ids. With `collection()`, it reads that attachment collection, and the model needs the `HasMediaLibrary` trait. All three entries share these options: | Method | Default | Purpose | |---|---|---| | `collection(string)` | none | Read attachments from this collection instead of an attribute. | | `conversion(string)` | see below | Which image size to show. | Missing files are skipped, and files in the trash or from another tenant are never shown. ## MediaEntry `MediaEntry` extends Filament's `ImageEntry`, so its image options work: `square()`, `circular()`, `stacked()`, `limit()`, `height()`, `width()` and the rest. It defaults to the `preview` conversion, which is larger than the table default. ```php MediaEntry::make('cover_id')->height(240); MediaEntry::make('gallery')->collection('gallery')->stacked()->limit(4); ``` ## MediaGalleryEntry `MediaGalleryEntry` shows a responsive grid of thumbnails. Clicking one opens a full screen lightbox. ```php MediaGalleryEntry::make('gallery') ->collection('gallery') ->gridColumns(3) ->aspectRatio('16:9') ->captions(false); ``` | Method | Default | Purpose | |---|---|---| | `gridColumns(int)` | `4` | Number of columns. The minimum is 1. | | `aspectRatio(string)` | `1:1` | Ratio of each tile. Accepts `4:3`, `4/3` or `1`. Pass `null` for no fixed ratio. | | `captions(bool)` | on | Show a caption under each tile. It uses the file's caption, or its name when the caption is empty. | | `lightbox(bool)` | on | Open files full screen on click. With it off, tiles are not clickable. | | `conversion(string)` | `thumb` | Image size used for the tiles. | Tiles use the alt text and focal point of each file, so cropped tiles stay centred on the subject. Files that are not images show a type icon with their extension. The lightbox shows images, plays video, and offers an **Open original** link for other files. Use the left and right arrow keys to move between files and Escape to close. The lightbox loads the `preview` conversion, not the tile size. The gallery needs no JavaScript from you. The same grid is available outside Filament as a Blade component: see [Models & Blade](/guide/models-and-blade). ## MediaFileEntry `MediaFileEntry` lists files as rows: thumbnail or type icon, file name, and a line with type, size and dimensions. ```php MediaFileEntry::make('downloads') ->collection('downloads') ->downloadable(); ``` | Method | Default | Purpose | |---|---|---| | `downloadable(bool)` | on | Show **Open original** and **Download** buttons on each row. Pass `false` for a read-only list. | | `conversion(string)` | `thumb` | Image size used for the row thumbnails. | Files on a private disk, or files marked **Only me**, use short-lived signed links, so the buttons keep working for people who can see the record. ::: tip Hide an entry when there is nothing to show with Filament's own `hidden()` and `visible()` methods. An empty entry otherwise renders a dash. ::: --- --- url: /guide/rich-editor.md description: >- Insert library images into Filament's RichEditor with MediaLibraryRichContentPlugin: options, the URLs it stores, and why private files cannot be embedded. --- # Rich Editor `MediaLibraryRichContentPlugin` adds an **Insert from library** button to Filament's `RichEditor`. It opens the library, and the images you choose are inserted into the content with their alt text. ```php use Filament\Forms\Components\RichEditor; use Hoceineel\FilamentMediaLibrary\Filament\Forms\RichEditor\MediaLibraryRichContentPlugin; RichEditor::make('body') ->plugins([ MediaLibraryRichContentPlugin::make()->conversion('preview'), ]); ``` The plugin works with Filament 4 and 5. It needs no panel setup beyond the media library plugin itself. ## How it works 1. The toolbar shows a photo button labelled **Insert from library**. 2. Pressing it opens a modal titled **Insert images**, with the picker already open and limited to images. 3. Choosing files inserts them and closes the modal. You do not need to press **Insert** when you pick from the library. 4. Images are inserted at the cursor, in the order you picked them, each with its alt text. If a file has no alt text, its name is used. The modal uses the [`MediaPicker`](/guide/picker-field) field with `image()`, `reorderable()`, `openOnMount()` and `submitOnPick()`. Drop, paste and upload work in it as they do anywhere else. The button is added to the toolbar automatically. Remove it on one editor with Filament's own method: ```php RichEditor::make('body') ->plugins([MediaLibraryRichContentPlugin::make()]) ->disableToolbarButtons(['mediaLibrary']); ``` ## Options | Method | Default | Purpose | |---|---|---| | `conversion(string)` | `preview` | Which image size to insert. Falls back to the original when the file has no such conversion. | | `multiple(bool)` | on | Allow several images in one go. Pass `false` to insert one at a time. | | `folder(id, name or Closure)` | none | Open the picker in this folder. A missing named folder is created if the user may create folders. | ```php MediaLibraryRichContentPlugin::make() ->conversion('preview') ->multiple(false) ->folder('Blog images'); ``` ## What gets stored The editor saves HTML, so the plugin inserts an ordinary image with a `src` and an `alt`. The `src` depends on where the file lives. | File | Inserted URL | |---|---| | On a disk whose `visibility` is `public` | The direct file URL. | | On any other disk | A signed link to the plugin's serve route. It does not expire. | | Marked **Only me** | Nothing. The file is skipped. | Rich content is stored outside the library and read by anyone who can see the page, so it cannot use the short-lived links that private files get elsewhere. Non-expiring signed links are the compromise for files on a private disk. Files marked **Only me** are never given a permanent link, so they cannot be embedded. The serve route also refuses them. Files the user cannot see, and files from another tenant, are never inserted either. The server checks this when the modal is submitted, not only in the picker. ::: warning Embedded images are not tracked as usage. The library does not know a post uses the file, so it can be deleted. Deleting moves the file to the trash, and on a non-public disk the serve route stops serving trashed files. Keep important images out of the trash, or also attach them to the record with a [picker field](/guide/picker-field). ::: Two more things to know: * The URL is written into your content when you insert the image. If you move to another disk or domain later, existing content keeps the old URL. * Signed links depend on your `APP_KEY`. Rotating the key invalidates links already stored in rich content on private disks. ## Displaying the content Render the saved HTML as you would for any rich editor. The images load from the stored URLs, so no extra Blade component is needed. To control sizes and focal points yourself, use `` instead (see [Models & Blade](/guide/models-and-blade)). --- --- url: /guide/models-and-blade.md description: >- Attach library files to Eloquent models with HasMediaLibrary, read URLs and responsive images, define conversion presets, and render with Blade components. --- # Models & Blade Use the `HasMediaLibrary` trait to attach library files to any Eloquent model, and the Blade components to show them on your site. ## The HasMediaLibrary trait ```php use Hoceineel\FilamentMediaLibrary\Concerns\HasMediaLibrary; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasMediaLibrary; } ``` Files are grouped in named collections. A collection is just a string, such as `gallery` or `downloads`. The default collection is `default`. | Method | Returns | |---|---| | `mediaLibraryItems(?string $collection = null)` | A `MorphToMany` relation, ordered. With no collection, every attached file. | | `getMediaLibraryItems(string $collection = 'default')` | A collection of `MediaItem`. | | `getFirstMediaLibraryItem(string $collection = 'default')` | The first `MediaItem`, or `null`. | | `getFirstMediaLibraryUrl(string $collection = 'default', string $conversion = '')` | The first file's URL, or `null`. | | `syncMediaLibraryItems($items, string $collection = 'default')` | Replaces the collection with the given ids or items, in that order. | | `attachMediaLibraryItem($item, string $collection = 'default')` | Adds one file at the end of the collection. | | `mediaAttachments()` | The raw `MorphMany` of attachment rows. | ```php $post->syncMediaLibraryItems([4, 8, 15], 'gallery'); $post->attachMediaLibraryItem($item, 'gallery'); $post->getMediaLibraryItems('gallery'); $post->getFirstMediaLibraryItem('cover'); $post->getFirstMediaLibraryUrl('gallery', 'thumb'); $post->mediaLibraryItems('gallery')->get(); ``` Sync only accepts ids that exist for the current tenant. Anything else is dropped without an error. Attachments are deleted with the model, except when the model uses soft deletes and is only soft deleted. The [picker field](/guide/picker-field) in relationship mode calls `syncMediaLibraryItems()` for you, using the field name as the collection unless you set another. ### Avoid N+1 queries `getMediaLibraryItems()` uses the loaded relation when it is available, so eager load it for lists: ```php $posts = Post::query()->with('mediaLibraryItems.media')->get(); foreach ($posts as $post) { $post->getMediaLibraryItems('gallery'); } ``` ### Storing ids in a column You do not need the trait to store a selection. A picker in column mode saves ids in an attribute, and you read it with the Blade components below or with the model: ```php use Hoceineel\FilamentMediaLibrary\Models\MediaItem; $cover = MediaItem::query()->find($post->cover_id); ``` ## Reading a file Every `MediaItem` has these methods: | Method | Returns | |---|---| | `url(string $conversion = '')` | URL of the original, or of a conversion. | | `thumbnailUrl()` | The `thumb` conversion, or the original for images without one. | | `previewUrl()` | The `preview` conversion, or the original for images without one. | | `conversionUrlOrOriginal(string $conversion)` | The conversion if it exists. For images, the original if not. For other files, `null`. | | `srcset(string $conversion = 'preview')` | A `srcset` string of responsive sizes, or an empty string. | | `focalPosition()` | A CSS `object-position` value such as `32% 18%`. Defaults to `50% 50%`. | ```php $item->url(); $item->url('hero'); $item->conversionUrlOrOriginal('hero'); $item->srcset('preview'); $item->focalPosition(); ``` Public files on a public disk get direct URLs. Files on a private disk, and files marked **Only me**, get short-lived signed or temporary URLs (`temporary_url_minutes`, 30 by default). `srcset()` returns an empty string for those files, and for conversions without responsive images. See [Storage](/guide/storage). Other useful properties on the item are `name`, `alt`, `caption`, `description`, `width`, `height`, `mime_type`, `extension` and `size`. ## Conversions Conversions are presets in `config/filament-media-library.php`. Two ship by default: ```php 'conversions' => [ 'thumb' => ['width' => 480, 'height' => 480, 'fit' => 'crop', 'format' => 'webp', 'queued' => false], 'preview' => ['width' => 1600, 'height' => 1600, 'fit' => 'contain', 'format' => 'webp', 'queued' => true, 'responsive' => true, 'optimize' => true], ], ``` Each preset accepts these keys: | Key | Default | Meaning | |---|---|---| | `width`, `height` | none | Target size in pixels. A missing side defaults to 4096. | | `fit` | `contain` | `crop` fills exactly `width` by `height` and needs both. Anything else fits inside the box without cropping. | | `format` | `webp` | Output format. | | `queued` | `false` | Generate on the queue instead of during the upload. | | `responsive` | `false` | Also build the smaller sizes used by `srcset`. | | `optimize` | `false` | Run Spatie's image optimizers. Slow, best queued. | Add your own preset by adding a key: ```php 'hero' => ['width' => 2400, 'height' => 1000, 'fit' => 'crop', 'queued' => true], ``` New uploads get the conversion. Build it for existing files with: ```bash php artisan media-library:regenerate --only=hero --missing ``` Cropped conversions centre on the focal point set in the inspector. When a focal point changes, the file's conversions are rebuilt on the queue, so keep a worker running. See [Commands](/reference/commands). Conversions are built for images. A non-image file normally has none, and then `thumbnailUrl()` and `conversionUrlOrOriginal()` return `null`. ## Blade components Both components are registered by the package. Outside Filament panels, enable standalone mode and load Filament's assets first: see [Outside Panels](/guide/outside-panels). ### x-media-library-image ```blade ``` | Attribute | Default | Meaning | |---|---|---| | `item` | none | A `MediaItem`, an id, or `null`. With `null` or a missing file, nothing is rendered. | | `conversion` | `preview` | Which conversion to use. Uses the original if it does not exist. | | `sizes` | `100vw` | The `sizes` attribute. Only output when there is a `srcset`. | | `lazy` | `true` | Adds `loading="lazy"` and `decoding="async"`. Pass `:lazy="false"` for images above the fold. | Any other attribute, such as `class` or `id`, is passed to the ``. The component also sets: * `alt` from the file's alt text, or an empty string. * `width` and `height` from the original file. Add `object-cover` and a fixed ratio class when you use a cropped conversion. * `srcset` from the responsive sizes, when the conversion has them. * `style="object-position: ..."` from the focal point, so a cropped image keeps the subject in frame. Passing an item you already loaded avoids a query: `:item="$post->getFirstMediaLibraryItem('cover')"`. ### x-media-library-gallery ```blade ``` | Attribute | Default | Meaning | |---|---|---| | `record` | none | A model with `HasMediaLibrary`. Use with `collection`, or with `attribute` for a column. | | `collection` | none | Attachment collection to show. | | `attribute` | none | Column on `record` holding the ids. Use it instead of `collection`. | | `items` | none | A list of ids, a single id, or an Eloquent collection of items. | | `conversion` | `thumb` | Conversion used for the tiles. | | `columns` | `4` | Grid columns. | | `aspect-ratio` | `1:1` | Ratio of each tile. | | `captions` | `true` | Show the caption, or the name, under each tile. | | `lightbox` | `true` | Open files full screen on click. | Give it either `items`, or `record` with `collection` or `attribute`. A `record` with neither shows nothing. The `items` form only shows files the current user can see, so other people's private files are left out. The gallery uses Alpine for the lightbox. Pages built with `@filamentScripts` already have it. It is the same grid as the [infolist entry](/guide/infolist-entries). --- --- url: /guide/outside-panels.md description: >- Use the media picker and library on your own Livewire and Blade pages, with standalone mode, assets, policies, plain HTML forms, routes and tenancy. --- # Outside Panels The picker, the full library and the display components also work on your own pages: a customer portal, a front-end editor or a plain HTML form. No Filament panel is needed. ## 1. Turn it on Standalone mode is off by default. Until you enable it, the components refuse to load outside a panel (they return a 403) and the upload routes return 404. In `config/filament-media-library.php`: ```php 'standalone' => [ 'enabled' => true, 'middleware' => ['web', 'auth'], ], ``` ## 2. Check who can manage files ::: warning Inside a panel, Filament's `canAccessPanel()` decides who gets in. Outside a panel, only your policy does. The bundled `MediaItemPolicy` lets every signed-in user browse and upload shared files. If customers can sign in to your site, register your own policy before you enable standalone mode. ::: ```php use Hoceineel\FilamentMediaLibrary\Models\MediaItem; use Hoceineel\FilamentMediaLibrary\Policies\MediaItemPolicy; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Facades\Gate; class AppMediaPolicy extends MediaItemPolicy { public function viewAny(Authenticatable $user): bool { return $user->is_staff; } public function create(Authenticatable $user): bool { return $user->is_staff; } } // AppServiceProvider::boot() Gate::policy(MediaItem::class, AppMediaPolicy::class); ``` `viewAny` gates the library and the picker modal. `create` gates uploads. `update`, `delete`, `restore` and `forceDelete` defer to `view`, so override `view` too if you need to restrict those separately. See [Authorization](/reference/authorization) for every ability. ## 3. Load the assets The page needs Filament's styles and scripts, as in Filament's own guide for using components outside panels. In `resources/css/app.css`: ```css @import 'tailwindcss'; @import '../../vendor/filament/support/resources/css/index.css'; @import '../../vendor/filament/actions/resources/css/index.css'; @import '../../vendor/filament/forms/resources/css/index.css'; @import '../../vendor/filament/infolists/resources/css/index.css'; @import '../../vendor/filament/notifications/resources/css/index.css'; @import '../../vendor/filament/schemas/resources/css/index.css'; @variant dark (&:where(.dark, .dark *)); ``` In your layout: ```blade @filamentStyles @vite('resources/css/app.css') {{ $slot }} @livewire('notifications') @filamentScripts ``` The library's own CSS and JavaScript load on demand. After an update, run `php artisan filament:assets` again. ## 4. Pick files in a Livewire component ```blade ``` ```php use Hoceineel\FilamentMediaLibrary\Models\MediaItem; use Hoceineel\FilamentMediaLibrary\Rules\MediaLibraryItems; use Livewire\Component; class EditPost extends Component { public Post $post; public ?int $coverId = null; public array $gallery = []; public function mount(): void { $this->coverId = $this->post->cover_id; $this->gallery = $this->post->getMediaLibraryItems('gallery')->modelKeys(); } public function save(): void { $this->validate([ 'coverId' => ['nullable', MediaLibraryItems::image()], 'gallery' => ['array', 'max:12', MediaLibraryItems::image()], ]); $this->post->update(['cover_id' => $this->coverId]); $this->post->syncMediaLibraryItems($this->gallery, 'gallery'); } public function render() { return view('livewire.edit-post'); } } ``` The picker has the same features as the [form field](/guide/picker-field): drop, paste and upload, chunked uploads, reorder, preview, and the full library in a modal. | Attribute | Default | Meaning | |---|---|---| | `wire:model` or `value` | `null` | Selected id, or a list of ids when `multiple`. An Eloquent collection works too. | | `name` | `null` | Renders hidden inputs, for a plain `
`. | | `multiple` | `false` | Allow several files. | | `max-items` | `null` | Limit for `multiple`. | | `image` | `false` | Shortcut for `accept="image/*"`. | | `accept` | all types | Comma-separated MIME types. Wildcards work. | | `folder` | `null` | Folder id or name to open. A name is created when missing and the user may create folders. | | `lock-folder` | `false` | Keep the picker inside that folder. | | `layout` | auto | `grid` or `list`. A grid when only images are accepted. | | `aspect-ratio` | `null` | Frame for a single image, such as `16:9`. | | `conversion` | `thumb` | Conversion used for previews. | | `uploadable`, `previewable`, `reorderable`, `show-file-names` | `true` | Switch off with `:uploadable="false"`. | | `disabled` | `false` | Read-only. | | `open-on-mount` | `false` | Open the library as soon as the picker renders. | | `button-label` | "Browse library" | Text of the button that opens the library. | These options are locked on the server, so a visitor cannot change them from the browser. ::: warning Always validate submitted ids with `MediaLibraryItems`. It checks that each id exists, that the current user can see it, and that it has an accepted type. Without it, a visitor can submit any id. ::: ## 5. Or in a plain HTML form ```blade @csrf @method('PUT') ``` ```php $data = $request->validate([ 'cover_id' => ['nullable', MediaLibraryItems::image()], 'gallery' => ['nullable', 'array', MediaLibraryItems::image()], ]); ``` With `name`, the picker writes hidden inputs. A single picker posts `cover_id`, and a `multiple` picker posts `gallery[]`. An empty selection posts an empty value, so use `nullable`. After a failed validation, the picker restores the previous selection from `old()`. ## 6. The full library on any page ```blade ``` It needs the `viewAny` ability, and the user can upload, organise and delete files according to your policy. ## 7. Show files ```blade ``` The display components need no sign-in and no standalone switch, because they only read files. The gallery lightbox still needs the Alpine that `@filamentScripts` loads. See [Models & Blade](/guide/models-and-blade) for every attribute. ## Routes and access | Route | Purpose | |---|---| | `POST /media-library/upload` | Chunked uploads. | | `POST /media-library/upload/resolve` | Answers the duplicate prompt. | | `GET /media-library/download` | Downloads one or more files. | | `GET /media-library/serve/{media}/{conversion?}` | Serves files from private disks. Signed links only. | The first three use `standalone.middleware` (`['web', 'auth']` by default), are throttled to 600 requests a minute, and exist only while `standalone.enabled` is on. The serve route is always available, but only answers valid signed links. Change the `media-library` prefix with `route_prefix`. Inside a panel, the picker uses the panel's own routes and guard, whatever the standalone setting says. ## What the plugin options do not cover The plugin's fluent settings (`features()`, `defaultLayout()`, `maxFileSize()`, `acceptedFileTypes()`, `duplicates()`, `canAccess()` and the others) apply only to their panel. Outside panels, set the same options in `config/filament-media-library.php`: the `features`, `browser` and `upload` arrays. Gate access with policies, not `canAccess()`. ## Tenancy A panel's tenant does not exist outside the panel. To scope front-end pages, turn tenancy on in the config and give it a resolver: ```php 'tenancy' => [ 'enabled' => true, 'resolver' => App\Support\CurrentTeamResolver::class, 'strict' => true, ], ``` ```php namespace App\Support; use Hoceineel\FilamentMediaLibrary\Tenancy\TenantResolver; use Illuminate\Database\Eloquent\Model; class CurrentTeamResolver implements TenantResolver { public function resolve(): ?Model { return auth()->user()?->currentTeam; } } ``` The default resolver reads the Filament panel tenant, which is empty outside a panel. In strict mode that hides every file, so always set your own resolver. Panels with tenancy keep using their own tenant, whatever this config says. See [Tenancy](/guide/tenancy). --- --- url: /guide/tenancy.md description: >- Scope the media library per tenant with Filament tenancy, stancl/tenancy or a custom resolver. Covers strict mode, quotas and standalone pages. --- # Tenancy The library can keep every tenant's files apart. Folders, files and tags carry a tenant, uploads are stamped with it, and every query is filtered by it. Tenancy is off until a panel with tenancy, or your config, turns it on. ## Filament panel tenancy Nothing to configure. When the panel uses `->tenant(Team::class)`, the plugin turns tenancy on and resolves the tenant with `Filament::getTenant()`. ```php use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; public function panel(Panel $panel): Panel { return $panel ->tenant(Team::class) ->plugin(FilamentMediaLibraryPlugin::make()); } ``` Everything is scoped to the current tenant: | What | How it is scoped | |---|---| | Files, folders and tags | A `tenant_type` and `tenant_id` column on each table, filtered by a global scope. | | New records | Stamped with the current tenant when they are created. | | Upload and download routes | Registered as tenant routes, so the tenant is in the URL, for example `/studio/media-library/upload`. | | Picker validation | The [picker field](/guide/picker-field) rejects ids that belong to another tenant. | | Attachments | `syncMediaLibraryItems()` drops ids from another tenant. | | Rich editor | The [rich editor plugin](/guide/rich-editor) never inserts another tenant's files. | | Quota | Counted per tenant. See below. | In strict mode, which is on for Filament tenancy, a request with no resolved tenant sees nothing at all. ## stancl/tenancy, single database Store every tenant in one database with a `tenant_id` column, and resolve the tenant from stancl: ```php use Hoceineel\FilamentMediaLibrary\Tenancy\StanclTenantResolver; FilamentMediaLibraryPlugin::make() ->tenancy() ->resolveTenantUsing(StanclTenantResolver::class); ``` `StanclTenantResolver` calls stancl's `tenant()` helper. It returns the tenant model, or `null` when stancl has not been initialised for the request. ::: warning On a panel without Filament tenancy, the plugin's `strict` argument has no effect: the library does not hide files when no tenant resolves. Make sure stancl's tenancy middleware runs on the panel's routes. For pages outside panels, use the `tenancy.strict` config key instead. ::: ## stancl/tenancy, database per tenant The library tables live in each tenant's database, so the database already isolates tenants. 1. Add the package migration to your tenant migrations. 2. Keep `tenancy.enabled` set to `false`. 3. Enable stancl's `FilesystemTenancyBootstrapper`, so each tenant writes to its own disk path. ## Custom resolver Pass a closure, a class name or a `TenantResolver` instance to `resolveTenantUsing()`: ```php FilamentMediaLibraryPlugin::make() ->tenancy() ->resolveTenantUsing(fn () => auth()->user()?->organization); ``` A resolver class implements one method and returns an Eloquent model, or `null`: ```php use Hoceineel\FilamentMediaLibrary\Tenancy\TenantResolver; use Illuminate\Database\Eloquent\Model; class OrganizationResolver implements TenantResolver { public function resolve(): ?Model { return auth()->user()?->organization; } } ``` The tenant is stored as a morph (`tenant_type` and `tenant_id`), so it can be any model. The `tenant_id` column type follows `key_types.tenant` in the config. Set it to `uuid`, `ulid` or `string` before you run the migration if your tenants do not use integer keys. ## Strict mode | Strict | When no tenant resolves | |---|---| | on | Every query returns nothing. Safe by default. | | off | The tenant filter is skipped, so the library shows every record. | Leave strict mode on unless you know why you need otherwise. A missing tenant then shows an empty library, not another tenant's files. ## Standalone pages Pages outside panels have no panel tenant. Turn tenancy on in `config/filament-media-library.php` and give it a resolver: ```php 'tenancy' => [ 'enabled' => true, 'resolver' => App\Support\CurrentTeamResolver::class, 'strict' => true, ], ``` | Key | Default | Meaning | |---|---|---| | `enabled` | `false` | Scope the library to a tenant outside panels. | | `resolver` | `FilamentTenantResolver` | A class implementing `TenantResolver`. | | `strict` | `true` | Hide everything when no tenant resolves. | The default resolver reads the Filament panel tenant, which does not exist outside a panel, so always set your own. Panels with tenancy keep using their own tenant, whatever this config says. See [Outside Panels](/guide/outside-panels). ## Quotas Limit storage per tenant, or for the whole app when tenancy is off: ```php 'quota' => 5 * 1024 ** 3, 'quota' => PlanQuota::class, ``` The value is a number of bytes, `null` for unlimited, or an invokable class that receives the current tenant and returns a byte limit, or `null`. ```php use Illuminate\Database\Eloquent\Model; class PlanQuota { public function __invoke(?Model $tenant): ?int { return $tenant?->plan === 'pro' ? 50 * 1024 ** 3 : 5 * 1024 ** 3; } } ``` Used space counts every file the tenant has, including files in the trash and previous versions. An upload that does not fit is refused. For chunked uploads the check runs when the first chunk arrives, before the file is stored. The library sidebar shows the storage used. ## Private files Tenancy separates tenants. Private files separate people inside a tenant. A user can mark a file or folder as **Only me**. Private records are visible only to their uploader, and to users who pass the `viewPrivate` policy ability, for example admins. Turn the feature off with `private_media => false`. See [Private Files](/guide/private-files) and [Authorization](/reference/authorization). --- --- url: /guide/storage.md description: >- Choose a disk for Media Library Pro: public and private disks, signed URLs, S3 and S3-compatible storage, CORS, file paths, queues and cleanup. --- # Storage Media Library Pro stores files on any Laravel filesystem disk. This page covers choosing a disk, how URLs are made, S3 setup, and the jobs that keep storage tidy. ## Choose a disk Set the disk in `.env`: ```bash MEDIA_LIBRARY_DISK=s3 ``` The config reads `MEDIA_LIBRARY_DISK`, then `MEDIA_DISK`, then falls back to `public`. ```php 'disk' => env('MEDIA_LIBRARY_DISK', env('MEDIA_DISK', 'public')), 'conversions_disk' => null, ``` Set `conversions_disk` to keep thumbnails and other conversions on a different disk from the originals. Leave it `null` to store them together. Each file remembers the disk it was stored on. Changing `disk` later affects new uploads only. Existing files keep working from where they are. ## Public and private disks The library treats a disk as public when its entry in `config/filesystems.php` has `'visibility' => 'public'`. Laravel's default `public` disk has it. The default `local` and `s3` disks do not. | Disk | Files shown to people | |---|---| | Visibility is `public`, file is shared | A direct URL from the disk. | | Visibility is `public`, file is **Only me** | A signed link to the library's serve route. It expires. | | Any other disk | A temporary URL from the disk (S3 and compatible), or a signed link to the serve route when the disk cannot create temporary URLs. It expires. | Links expire after `temporary_url_minutes`, which defaults to 30 and cannot go below 2. The library caches each link for half that time, so a page does not sign the same file again and again. ```php 'temporary_url_minutes' => 30, ``` The serve route is `GET /media-library/serve/{media}/{conversion?}`. It only answers valid signed links, refuses trashed files, and sends the file with `X-Content-Type-Options: nosniff` and a locked-down `Content-Security-Policy`. ::: warning On a public disk, a file marked **Only me** is hidden in the library and given only expiring links. The file itself still sits in a web-readable folder, and anyone with its exact URL can open it. When files must stay confidential, use a private disk: S3 with private visibility, or a `local` disk outside `public/`. See [Private Files](/guide/private-files). ::: Rich text cannot use expiring links. For files on a non-public disk, the [rich editor plugin](/guide/rich-editor) inserts signed links that never expire. ## S3 and S3-compatible storage Configure the disk in `config/filesystems.php` as usual. For MinIO, Cloudflare R2 or DigitalOcean Spaces, set an endpoint: ```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'), 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => false, ], ``` Keep the bucket private and the disk without a `visibility` setting. The library then serves every file through temporary URLs, and nothing in the bucket is public. To serve public files straight from a bucket or CDN, set `'visibility' => 'public'` and a `url` on the disk. Laravel then applies public visibility when it writes files, so your bucket must allow it. ### CORS for the image editor The image editor reads the original pixels in the browser. When files live on another domain, such as S3 or a CDN, the bucket must allow your panel origin. Without it, the editor shows an error instead of opening the image. ```json [{ "AllowedOrigins": ["https://admin.example.com"], "AllowedMethods": ["GET"], "AllowedHeaders": ["*"] }] ``` Replace the origin with the address of your panel. Browsing, uploads and the picker do not need this rule. Only the editor does. ## File paths Spatie stores files under the media's numeric id by default. The library stores them under a random UUID folder instead, so URLs cannot be enumerated from sequential ids: ```text 5b0e3f0c-.../photo.webp 5b0e3f0c-.../conversions/photo-thumb.webp ``` The library registers its path generator for the item model in Spatie's `media-library.custom_path_generators`. If you map the item model there yourself, the library leaves your entry alone. The `prefix` from Spatie's `media-library` config is honoured. File names are slugged by default. Set `upload.preserve_file_names` to `true` to keep letters, numbers, spaces and dashes from the original name. ## Conversions and the queue Thumbnails are generated while the upload finishes, so new files appear at once. Larger conversions are queued. The default `preview` conversion runs on the queue, and so does rebuilding conversions after a focal point changes. Keep a worker running: ```bash php artisan queue:work ``` Without a worker, the original still shows, but queued conversions never appear. Presets and their `queued` flag are covered in [Models & Blade](/guide/models-and-blade). To rebuild existing conversions, see [Commands](/reference/commands). ## Upload limits and chunks Uploads arrive in chunks of `upload.chunk_size` bytes, 5 MB by default. Values below 256 KB are raised to 256 KB. Keep your PHP `upload_max_filesize` and `post_max_size` above the chunk size, not above your largest file. `upload.max_file_size` is in kilobytes. The default, `512 * 1024`, is 512 MB. Set a [quota](/guide/tenancy) to cap total storage. Chunks are stored under `storage/app/media-library-chunks` on the application server, not on the media disk. Change the folder name with `upload.chunk_directory`. If you run several servers, send all chunks of one upload to the same server, or share that directory. ## Scheduled cleanup Schedule the prune command so old trash and abandoned chunks are removed: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('media-library:prune')->daily(); ``` It does two things: * Permanently deletes files that have been in the trash longer than `trash.prune_after_days` (30 by default). Pass `--days=` to override once. * Deletes upload chunk folders untouched for 24 hours. ```bash php artisan media-library:prune --days=7 ``` Trash stays on disk until it is pruned. See [Commands](/reference/commands) for the full list. --- --- url: /guide/importing.md description: >- Move existing files in with media-library:import. Covers Spatie media, Curator and plain directories, options, dry runs and migration checklists. --- # Importing `media-library:import` copies existing files into the library. It reads three kinds of source: media managed by Spatie's laravel-medialibrary, rows from Awcodes Curator, and any folder on a disk. ```bash php artisan media-library:import spatie --model="App\Models\Product" --collection=images --folder=Products php artisan media-library:import curator --update=posts.featured_image_id --update=pages.hero_id --map=storage/curator-map.json php artisan media-library:import directory uploads/legacy --disk=public --folder=Legacy ``` Every file goes through the same pipeline as a normal upload: type and size checks, SVG sanitising, conversions, the `MediaItemUploaded` event and, if enabled, queued alt text. Files that fail a check are skipped with a warning, and the rest continue. ## Options | Option | Applies to | Meaning | |---|---|---| | `source` | all | `spatie`, `curator` or `directory`. | | `path` | `directory` | Folder to import, relative to the disk root. | | `--disk=` | `directory` | Disk to read from. Defaults to the library disk. | | `--model=*` | `spatie` | Only media attached to these model classes. Repeat for several. | | `--collection=*` | `spatie` | Only these Spatie collections. Repeat for several. | | `--move` | `spatie` | Delete the original Spatie media after it is copied. | | `--table=` | `curator` | Table holding the Curator rows. Defaults to `curator`. | | `--update=*` | `curator` | A `table.column` whose values are rewritten to the new ids. Repeat for several. | | `--folder=` | all | Put everything into this folder, created when missing. | | `--map=` | all | Write a JSON map of old id to new library id to this file. | | `--tenant=` | all | Tenant key to assign to the imported records. | | `--tenant-model=` | all | Tenant model class. Required with `--tenant`. | | `--dry-run` | all | Count what would be imported and write nothing. | ## Spatie media ```bash php artisan media-library:import spatie ``` This copies every Spatie media row that does not already belong to the library, then attaches the new library item back to the model that owned it, in the same collection. Usage tracking works straight away, and `$product->getMediaLibraryItems('images')` returns the files once the model uses [`HasMediaLibrary`](/guide/models-and-blade). * The item name comes from the media name. `alt` and `caption` are read from the media's custom properties. * Without `--folder`, files go into `Imported/`, for example `Imported/Product`. * The originals stay in place unless you pass `--move`, which deletes each Spatie media row and its file after it was copied. ## Curator ```bash php artisan media-library:import curator --update=posts.featured_image_id ``` This reads the `curator` table (change it with `--table`) and copies each file from its `disk` and `path`. Title, alt text, caption and description come across when the row has them. Without `--folder`, files go into `Imported/Curator`. `--update=table.column` rewrites every value in that column from the old Curator id to the new library id, in one query per 500 ids. Pass it once for each column that stores a Curator id. A column that does not exist is skipped with a warning. ::: warning `--update` replaces values by looking up old ids. If you run it a second time, values that were already rewritten can match an old id and be changed again. Back up the database first and run the rewrite once. Rows whose file failed to import keep their old id. ::: ## Directory ```bash php artisan media-library:import directory uploads/legacy --disk=public --folder=Legacy ``` This imports every file under the path. Sub directories become folders, nested under `--folder` when you give one. Files starting with a dot are ignored. Files are copied, so the originals are left alone. ## Dry runs ```bash php artisan media-library:import spatie --dry-run -v ``` A dry run creates no files, no folders and no map file. It prints `Would import N files, skipped M.` Add `-v` to list each file. Already imported files count as skipped. ## Safe to run again Each imported item records where it came from in its custom properties (`imported_from`, such as `spatie:12`, `curator:100` or `directory:public:uploads/legacy/a.png`). A second run skips files it has already imported, even if they are in the trash, so an interrupted import can continue where it stopped. ## What to know * Imported files are shared and have no uploader, because the command runs without a signed-in user. They appear under **All media**, not under **My uploads**. * Thumbnails are built during the import. Larger conversions are queued, so run a worker: see [Storage](/guide/storage). * Duplicate detection is not applied. Two copies of the same file in the source become two items. * With tenancy on, pass `--tenant` and `--tenant-model` so the records land in the right tenant. See [Tenancy](/guide/tenancy). ## Checklist: from Curator 1. Back up the database and the storage disk. 2. Install the package and run its migrations: see [Installation](/guide/installation). 3. List every column that holds a Curator id, for example `posts.featured_image_id`. 4. Run `media-library:import curator --dry-run -v` and check the count. 5. Run the import with one `--update` for each column, and `--map=storage/curator-map.json`. 6. Read the warnings. Fix anything that was skipped, then handle those ids by hand with the map. 7. Replace Curator fields in your resources with [`MediaPicker`](/guide/picker-field), using the same column names, and Curator display columns with [`MediaColumn`](/guide/table-columns). 8. For columns that store several ids as JSON, rewrite them yourself using the map file. `--update` only handles one id per cell. 9. Start a queue worker, open the library and spot check files and alt text. 10. Remove Curator once nothing references it. ## Checklist: from a plain Spatie setup 1. Back up the database and the storage disk. 2. Install the package and run its migrations. 3. Add `HasMediaLibrary` to the models whose media you are importing. 4. Run `media-library:import spatie --dry-run -v`, narrowing with `--model` and `--collection` if needed. 5. Run the import without `--move`, so your original Spatie media stays until you have checked the result. 6. Switch reads from `$model->getMedia('images')` to `$model->getMediaLibraryItems('images')`, and forms to [`MediaPicker`](/guide/picker-field) with `relationship('images')`. 7. Compare counts per model and collection, then check the pages that show the images. 8. Remove the old Spatie media yourself when you are happy. `--move` only deletes originals as they are copied, so a second run with `--move` does not delete files that an earlier run already imported. --- --- url: /guide/alt-text.md description: >- Write alt text with AI. Set up the Anthropic generator, turn on auto-generation, write your own AltTextGenerator, and understand what image data is sent. --- # AI Alt Text Media Library Pro can write alt text for you. It adds a **Write it for me** action to the inspector and can describe new images on the queue. Generation is off until you pick a generator. ## Set up the Anthropic generator Install the SDK: ```bash composer require anthropic-ai/sdk ``` Add your key to `.env`: ```bash ANTHROPIC_API_KEY=your-key ``` Point the config at the generator: ```php 'alt_text' => [ 'generator' => \Hoceineel\FilamentMediaLibrary\AltText\AnthropicAltTextGenerator::class, 'auto_generate' => false, 'anthropic' => [ 'api_key' => env('ANTHROPIC_API_KEY'), 'model' => env('MEDIA_LIBRARY_ALT_TEXT_MODEL', 'claude-opus-5-5'), ], ], ``` The generator is available when the SDK is installed and an API key is set. Until then the inspector action stays hidden. Change the model with `MEDIA_LIBRARY_ALT_TEXT_MODEL`. ## In the inspector With a generator available, the alt text field in the [inspector](/guide/inspector) shows **Write it for me**. It appears for images only. Press it and the generated text fills the field. Nothing is saved until you save the inspector, so you can edit the text first. If the provider returns nothing, you see a warning and the field keeps its current text. The request waits for the provider's answer. Turn the action off for a panel with `->disableFeatures(Feature::AltText)`. ## Automatic alt text ```php 'auto_generate' => true, ``` With this on, every new image that has no alt text is described by a queued job. Keep a queue worker running. The job: * Skips files that already have alt text, so text you wrote is never replaced. * Retries once if it fails. * Saves the text without firing model events. * Writes in the application locale of the queue worker, since the job does not know which language the uploader used. Files that arrive through [URL import](/guide/uploading) and the [import command](/guide/importing) go through the same path, so they are described too. ## How the Anthropic generator works * It only handles images. Other files return nothing. * JPEG, PNG, GIF and WebP files up to 4 MB are sent as they are. Larger files, or other image types, are sent as their `preview` conversion if one exists. If neither applies, there is no alt text. * It asks for one sentence under 125 characters, in the language of the current locale, with no "Image of" opening. * Text is trimmed and capped at 250 characters. A refusal from the model returns nothing. ## Write your own generator Implement `AltTextGenerator`. It has two methods: | Method | Purpose | |---|---| | `isAvailable(): bool` | Return `false` to hide the inspector action and skip generation. | | `generate(MediaItem $item, ?string $locale = null): ?string` | Return the alt text, or `null` for nothing. | ```php namespace App\Support; use Hoceineel\FilamentMediaLibrary\AltText\AltTextGenerator; use Hoceineel\FilamentMediaLibrary\Models\MediaItem; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; class CaptionServiceAltTextGenerator implements AltTextGenerator { public function isAvailable(): bool { return filled(config('services.captioner.url')); } public function generate(MediaItem $item, ?string $locale = null): ?string { $media = $item->file(); if (! $media || ! $item->isImage()) { return null; } $response = Http::withToken(config('services.captioner.key')) ->timeout(30) ->attach('image', Storage::disk($media->disk)->get($media->getPathRelativeToRoot()), $media->file_name) ->post(config('services.captioner.url'), ['language' => $locale ?? app()->getLocale()]); return $response->successful() ? $response->json('caption') : null; } } ``` Register it: ```php 'alt_text' => [ 'generator' => App\Support\CaptionServiceAltTextGenerator::class, 'auto_generate' => true, ], ``` The container builds your class, so constructor injection works. The default is `NullAltTextGenerator`, which is never available and always returns `null`. ## Privacy ::: warning To describe an image, the generator sends the image itself to the provider you configure. With the Anthropic generator, that is Anthropic's API. Check the provider's data terms before you turn this on. ::: * `auto_generate` describes every new image, including files uploaded as **Only me**. The generator does not check visibility. If private images must not leave your server, leave `auto_generate` off, or write a generator that skips them with `$item->isPrivate()`. * With the default `NullAltTextGenerator`, nothing is sent anywhere. * Review generated text before publishing. It describes what the model sees and does not know your context. --- --- url: /guide/security.md description: >- How Media Library Pro protects uploads, URL imports, private files and Livewire state, what it leaves to you, and how to report a vulnerability. --- # Security This page lists what Media Library Pro checks, so you can judge what to add on top. Each statement matches the shipped code and is covered by the package's test suite. ## Authorization Every action goes through Laravel policies. `MediaItemPolicy` and `MediaFolderPolicy` are registered unless your app defines its own for the models. * Shared files are open to every user who can reach the library. Files marked **Only me** are limited to their uploader and to users who pass the `viewPrivate` ability. * Uploads need `create`. Replacing a file needs `update` on that file. * The library page and the picker modal need `viewAny`. The panel plugin's `canAccess()` also gates the page and the upload routes. * Deleting a folder that holds someone else's private content, or moving a folder into one the user cannot see, is refused. * Folders given to the picker with `folder()` are only resolved when the user can see them. See [Authorization](/reference/authorization) for every ability and how to override it. ## Uploads Uploads arrive in chunks, and the server checks each stage. | Check | What it does | |---|---| | Extension | Before any data is stored, the file name must have a plain extension that is not on `upload.blocked_extensions`. Trailing dots and spaces are trimmed, so `shell.php.` is still blocked. Files with no extension are refused. | | Content sniffing | After assembly, the file's real MIME type is detected from its bytes with `finfo`. It must match `upload.accepted_mime_types`, so a renamed executable does not pass as `photo.jpg`. | | Size | The declared and assembled size must be within `upload.max_file_size`. | | Quota | The upload must fit the remaining [quota](/guide/tenancy). Previous versions count towards it. | | File names | Names are slugged by default. Inner extensions are never kept, so `shell.php.png` is stored as `shell-php.png`. | | SVG | With `upload.sanitize_svg` on (the default), scripts, event handlers and remote references are removed. An SVG that cannot be cleaned is refused. | The default `blocked_extensions` list covers server scripts (`php`, `phtml`, `phar`, `py`, `rb`, `jsp`, `asp`), executables and shell files (`exe`, `sh`, `bat`, `ps1`, `jar`), and browser-renderable files (`html`, `htm`, `xhtml`, `js`, `mjs`, `xml`, `xsl`, `svgz`, `swf`). Edit it in `config/filament-media-library.php`. ### Chunk handling * The first chunk must come first. A chunk that skips it is refused and nothing is written. * Every later chunk must match the file name, total size and chunk count declared by the first. * A chunk may not be larger than the configured chunk size, and the parts together may not exceed the declared size. * Each person can have at most 20 unfinished uploads. Finished uploads and duplicates do not count. * Chunks are kept in a folder per user. Folders untouched for 24 hours are removed by `media-library:prune`. * The upload routes require an authenticated user and are throttled to 600 requests a minute. ## URL import Import from URL downloads a file from a link a user types, so it is guarded against server-side request forgery. * Only `http` and `https` links are accepted. * The host is resolved first. Every address it resolves to must be public. Loopback, private ranges, link-local addresses (including cloud metadata addresses), carrier-grade NAT, IPv4-mapped and NAT64 IPv6 addresses, and documentation ranges are refused. * The request connects to the address that was checked (DNS pinning), so a second DNS answer cannot redirect it. * Redirects are followed manually, up to three times, and every hop is checked again. * The download has a 30 second timeout and stops as soon as it passes `upload.max_file_size`. * The downloaded file then goes through the same checks as any upload. ## Links and storage paths * Files are stored under a random UUID folder, so URLs cannot be guessed from sequential ids. * Private files are never given a permanent link. They get short-lived signed links, and the serve route refuses a signed link without an expiry for a private file. * The serve route refuses trashed files and invalid signatures, and sends `X-Content-Type-Options: nosniff` and a restrictive `Content-Security-Policy`. * The image editor receives a fresh link, and signed links are never altered after signing. * Rich text embeds use non-expiring signed links for non-public disks, and never for private files. See [Rich Editor](/guide/rich-editor). Signed links depend on your `APP_KEY`. Keep it secret. ## Tenants and private files * Folders, files and tags are filtered by tenant on every query when tenancy is on. Picker validation, attachments and the rich editor reject items from another tenant. See [Tenancy](/guide/tenancy). * Duplicate detection ignores files from other tenants. * Private files are filtered out of picker validation for users who cannot see them. See [Private Files](/guide/private-files). ## Livewire state The library and the picker lock their configuration with Livewire's `#[Locked]` attribute: the folder, page size, active file, accepted types, selection limits, layout and the rest. A visitor who edits that state in the browser gets an exception. Sort order goes through a validated method, and tag names typed by users are treated as names, never as ids. ## Standalone mode Using the library outside Filament panels is off by default. Until you enable `standalone.enabled`, the components return 403 and the upload and download routes return 404. See [Outside Panels](/guide/outside-panels). Before you enable it, register a policy that fits your users. ## What we do not do * **No virus scanning.** Files are checked by type and content sniffing, not scanned for malware. If you need it, scan chunks or stored files with your own pipeline, for example on the `MediaItemUploaded` event. * **No EXIF stripping.** Photos keep the metadata they were uploaded with, including location data. The inspector shows a short list of camera fields, but the file on disk is unchanged. * **No protection for public disks.** On a public disk the file sits in a web-readable folder. Anyone with the exact URL can open it, even if the file is marked **Only me**. Use a private disk for confidential files. See [Storage](/guide/storage). * **No content moderation.** The library does not look at what an image shows. * **No policy for you.** The default policy is permissive for signed-in users. Write your own for any site where users are not all trusted. ## Reporting a vulnerability Email `security@hoceine.com` with a description, the version you tested and steps to reproduce. Please do not open a public issue or post details until a fix is available. --- --- url: /reference/configuration.md description: >- Every key in config/filament-media-library.php with its type, default and what it does, grouped like the file. --- # Configuration Publish the file with `php artisan vendor:publish --tag=filament-media-library-config`, or run `php artisan media-library:install`. The config lives at `config/filament-media-library.php`. Inside a Filament panel, the fluent options on the [plugin](/reference/plugin) override `features`, `browser`, `upload.accepted_mime_types`, `upload.max_file_size`, `upload.duplicates` and tenancy. Outside panels, this file is the only place to set them. ## Models and tables | Key | Type | Default | What it does | |---|---|---|---| | `models.item` | class-string | `MediaItem::class` | The model for library files. Swap it to extend the built-in model. | | `models.folder` | class-string | `MediaFolder::class` | The folder model. | | `models.tag` | class-string | `MediaTag::class` | The tag model. | | `models.attachment` | class-string | `MediaAttachment::class` | The pivot model that records where a file is used. | | `table_prefix` | string | `'media_library_'` | Prefix for the library tables. | | `user_model` | class-string or `null` | `null` | The model for uploaders and owners. `null` uses your auth provider's model. | ## Key types Set these before you run the migration. | Key | Type | Default | What it does | |---|---|---|---| | `key_types.user` | `int`, `uuid`, `ulid` or `string` | `'int'` | Column type for `uploaded_by` and `created_by`. | | `key_types.tenant` | same | `'int'` | Column type for the tenant foreign key. See [Multi-tenancy](/guide/tenancy). | | `key_types.attachable` | same | `'int'` | Column type for the model that uses a file. | ## Disk | Key | Type | Default | What it does | |---|---|---|---| | `disk` | string | `env('MEDIA_LIBRARY_DISK', env('MEDIA_DISK', 'public'))` | The disk that holds originals. | | `conversions_disk` | string or `null` | `null` | A separate disk for thumbnails and conversions. `null` keeps them with the original. | | `temporary_url_minutes` | int | `30` | Lifetime of temporary URLs (S3) and signed streaming links (local) for files on a private disk. | | `route_prefix` | string | `'media-library'` | URL prefix for the upload, download and serve routes. | See [Storage & S3](/guide/storage). ## Upload | Key | Type | Default | What it does | |---|---|---|---| | `upload.max_file_size` | int (kilobytes) | `512 * 1024` | Largest file, 512 MB by default. `0` removes the limit. | | `upload.chunk_size` | int (bytes) | `5 * 1024 * 1024` | Size of each chunk the browser sends. The server never accepts a value under 256 KB. | | `upload.max_parallel_uploads` | int | `3` | Files the browser uploads at the same time. | | `upload.accepted_mime_types` | array | images, video, audio, PDF, ZIP, text, CSV, Office documents | MIME patterns the library accepts. `image/*` style wildcards work. | | `upload.blocked_extensions` | array | scripts, executables, `html`, `xml`, `js` and similar | Extensions that are always refused, whatever the MIME type says. | | `upload.sanitize_svg` | bool | `true` | Strip scripts and unsafe markup from SVG uploads. | | `upload.preserve_file_names` | bool | `false` | Keep the original file name on disk instead of a generated one. | | `upload.duplicates` | `DuplicateStrategy` | `DuplicateStrategy::Ask` | What happens when a file matches an existing one by hash. `Ask`, `Allow` or `UseExisting`. | | `upload.url_import` | bool | `true` | Allow importing files from a URL. The `url_import` feature overrides it. | | `upload.chunk_directory` | string | `'media-library-chunks'` | Folder under `storage/app` for unfinished chunks. | See [Uploading](/guide/uploading) and [Security](/guide/security). ## Conversions `conversions` is an array keyed by conversion name. The defaults: ```php 'conversions' => [ 'thumb' => ['width' => 480, 'height' => 480, 'fit' => 'crop', 'format' => 'webp', 'queued' => false], 'preview' => ['width' => 1600, 'height' => 1600, 'fit' => 'contain', 'format' => 'webp', 'queued' => true, 'responsive' => true, 'optimize' => true], ], ``` | Option | Type | What it does | |---|---|---| | `width`, `height` | int | Target size in pixels. | | `fit` | string | `crop`, `contain` and the other Spatie fit modes. Cropped conversions follow the focal point. | | `format` | string | Output format, for example `webp`. | | `queued` | bool | Generate on the queue. `thumb` is synchronous so new uploads appear at once. | | `responsive` | bool | Generate responsive image variants. | | `optimize` | bool | Run Spatie's image optimizers. Slow, so queue it. | Add your own names to use them with `` and the table columns. ## Versions | Key | Type | Default | What it does | |---|---|---|---| | `versions.enabled` | bool | `true` | Keep the previous file when you replace or save an edit as a new version. | | `versions.keep` | int | `10` | Versions kept per file. Older ones are pruned. | ## Tenancy | Key | Type | Default | What it does | |---|---|---|---| | `tenancy.enabled` | bool | `false` | Scope the library to a tenant outside panels. Panels with tenancy do this on their own. | | `tenancy.resolver` | class-string | `FilamentTenantResolver::class` | The class that returns the current tenant. `StanclTenantResolver` ships with the package. | | `tenancy.strict` | bool | `true` | Show nothing when no tenant resolves. | See [Multi-tenancy](/guide/tenancy). ## Private media | Key | Type | Default | What it does | |---|---|---|---| | `private_media` | bool | `true` | Let uploaders mark files and folders private. Private records are visible to their owner and to users who pass `viewPrivate`. | See [Private files](/guide/private-files) and [Authorization](/reference/authorization). ## Quota | Key | Type | Default | What it does | |---|---|---|---| | `quota` | int, `null` or invokable class-string | `null` | Maximum bytes per tenant, or for the whole app without tenancy. An invokable class receives the current tenant and returns bytes or `null`. | ## Trash | Key | Type | Default | What it does | |---|---|---|---| | `trash.enabled` | bool | `true` | Move deleted files to the trash. When off, deleting removes the file for good. | | `trash.prune_after_days` | int | `30` | Age at which `media-library:prune` removes trashed files. | ## Alt text | Key | Type | Default | What it does | |---|---|---|---| | `alt_text.generator` | class-string | `NullAltTextGenerator::class` | Class that writes alt text. Use `AnthropicAltTextGenerator::class` for Claude, or your own `AltTextGenerator`. | | `alt_text.auto_generate` | bool | `false` | Describe every new image on the queue. | | `alt_text.anthropic.api_key` | string | `env('ANTHROPIC_API_KEY')` | API key for `AnthropicAltTextGenerator`. | | `alt_text.anthropic.model` | string | `env('MEDIA_LIBRARY_ALT_TEXT_MODEL', 'claude-opus-5-5')` | Model used for descriptions. | See [AI alt text](/guide/alt-text). ## Browser | Key | Type | Default | What it does | |---|---|---|---| | `browser.per_page` | int | `48` | Items loaded per page. | | `browser.default_layout` | `'grid'` or `'list'` | `'grid'` | Starting layout. | | `browser.default_sort` | string | `'newest'` | Starting sort: `newest`, `oldest`, `name_asc`, `name_desc`, `largest`, `smallest`. | | `browser.tile_size` | int | `180` | Grid tile size in pixels. The plugin clamps it between 120 and 280. | ## Features Each key switches one part of the library on or off. `null` keeps the default, which is on. The four keys that default to `null` also honour their older config key (`trash.enabled`, `private_media`, `versions.enabled`, `upload.url_import`). | Key | Default | |---|---| | `features.folders`, `tags`, `favorites` | `true` | | `features.trash` | `null` | | `features.private_media` | `null` | | `features.versions` | `null` | | `features.image_editor`, `focal_point` | `true` | | `features.url_import` | `null` | | `features.folder_upload`, `alt_text`, `duplicate`, `download`, `exif`, `usage`, `smart_views`, `keyboard_shortcuts` | `true` | The [plugin](/reference/plugin#features) lists what each flag turns off. ## Standalone | Key | Type | Default | What it does | |---|---|---|---| | `standalone.enabled` | bool | `false` | Allow the picker and library on your own Livewire and Blade pages. When off, the standalone routes return 404. | | `standalone.middleware` | array | `['web', 'auth']` | Middleware for the standalone upload and download routes. | Read [Outside Filament panels](/guide/outside-panels) before you turn this on. The default policy lets every signed-in user manage shared media, so register your own policy first on a site with customer accounts. --- --- url: /reference/plugin.md description: >- Every fluent method on FilamentMediaLibraryPlugin: navigation, tenancy, metadata schema, access, the 17 feature flags and the default layout, sort and upload options. --- # Plugin options Register the plugin on each panel that should have a library. Every method returns the plugin, so you can chain them. ```php use Hoceineel\FilamentMediaLibrary\Enums\BrowserLayout; use Hoceineel\FilamentMediaLibrary\Enums\DuplicateStrategy; use Hoceineel\FilamentMediaLibrary\Enums\Feature; use Hoceineel\FilamentMediaLibrary\Enums\SortOrder; use Hoceineel\FilamentMediaLibrary\FilamentMediaLibraryPlugin; use Filament\Forms\Components\TextInput; public function panel(Panel $panel): Panel { return $panel ->id('admin') ->tenant(Team::class) ->plugin( FilamentMediaLibraryPlugin::make() ->navigationGroup('Content') ->navigationLabel('Assets') ->navigationIcon('heroicon-o-photo') ->navigationSort(20) ->navigationCountBadge() ->slug('assets') ->canAccess(fn (): bool => auth()->user()->isStaff()) ->metadataSchema(fn (): array => [ TextInput::make('credit')->label('Photo credit'), TextInput::make('license'), ]) ->disableFeatures(Feature::UrlImport, Feature::AltText) ->defaultLayout(BrowserLayout::List) ->defaultSort(SortOrder::Newest) ->perPage(60) ->tileSize(200) ->acceptedFileTypes(['image/*', 'application/pdf']) ->maxFileSize(20 * 1024) ->duplicates(DuplicateStrategy::UseExisting), ); } ``` Fluent options apply to the panel they are registered on. They are copied into `config('filament-media-library.*')` when the panel boots, so they win over the config file. Outside panels no plugin runs: set the same values in [the config file](/reference/configuration). ## Static helpers | Method | Returns | |---|---| | `FilamentMediaLibraryPlugin::make()` | A new plugin instance. | | `FilamentMediaLibraryPlugin::get()` | The plugin registered on the current panel. | | `FilamentMediaLibraryPlugin::isRegistered()` | `true` when the current panel has the plugin. | ## Page and navigation | Method | Purpose | |---|---| | `page(string $page)` | Use your own page class. It must extend `MediaLibraryPage`. | | `withoutPage(bool\|Closure $condition = true)` | Do not register the library page. Use the picker only. | | `registerNavigation(bool\|Closure $condition = true)` | Show or hide the navigation item. | | `navigationGroup(string\|Closure\|null $group)` | Navigation group. | | `navigationLabel(string\|Closure\|null $label)` | Label. Defaults to the `navigation_label` translation. | | `navigationIcon(string\|BackedEnum\|Closure\|null $icon)` | Icon. | | `navigationSort(int\|Closure\|null $sort)` | Sort position. | | `navigationCountBadge(bool\|Closure $condition = true)` | Show the file count as a badge. | | `slug(?string $slug)` | Page URL slug. Defaults to `media-library`. | ## Tenancy | Method | Purpose | |---|---| | `tenancy(bool $condition = true, bool $strict = true)` | Scope the library per tenant. On by default when the panel uses tenancy. With `strict`, a request without a tenant sees nothing. | | `resolveTenantUsing(TenantResolver\|Closure\|string $resolver)` | Resolve the tenant yourself. Panels with tenancy use `FilamentTenantResolver` unless you set one. | See [Multi-tenancy](/guide/tenancy). ## Inspector metadata `metadataSchema(?Closure $schema)` adds fields to the inspector. Return an array of Filament form components. Values are stored in the item's `custom_properties`. ```php ->metadataSchema(fn (): array => [ TextInput::make('credit'), ]) ``` ## Access `canAccess(bool|Closure $condition)` hides the page and its navigation item and returns 403 when the condition is false. It does not replace the policies. See [Authorization](/reference/authorization). ## Features Three methods control the `Feature` flags: | Method | Behaviour | |---|---| | `features(array $features)` | Keep only the listed features and switch every other one off. | | `enableFeatures(Feature ...$features)` | Switch the listed features on. | | `disableFeatures(Feature ...$features)` | Switch the listed features off. | ```php FilamentMediaLibraryPlugin::make()->features([Feature::Folders, Feature::Tags, Feature::Download]); ``` The `Hoceineel\FilamentMediaLibrary\Enums\Feature` enum has 17 cases: | Case | Value | Switching it off removes | |---|---|---| | `Feature::Folders` | `folders` | The folder tree, folder breadcrumbs, new, rename, move and delete folder actions, and the folder field in the inspector. Everything lists at one level. | | `Feature::Tags` | `tags` | The tag filter, tag field in the inspector and the bulk tag action. | | `Feature::Favorites` | `favorites` | The favorite toggle and the Favorites view. | | `Feature::Trash` | `trash` | The trash. Deleting removes files for good. | | `Feature::PrivateMedia` | `private_media` | The private visibility option for files and folders. | | `Feature::Versions` | `versions` | Version history and the restore action. | | `Feature::ImageEditor` | `image_editor` | The crop, rotate and flip editor. | | `Feature::FocalPoint` | `focal_point` | Setting the focal point. | | `Feature::UrlImport` | `url_import` | Import from URL. | | `Feature::FolderUpload` | `folder_upload` | Uploading a whole folder. | | `Feature::AltText` | `alt_text` | The "Write it for me" alt text action. | | `Feature::Duplicate` | `duplicate` | The duplicate action. | | `Feature::Download` | `download` | Download buttons, including the bulk ZIP download. | | `Feature::Exif` | `exif` | The camera data section in the inspector. | | `Feature::Usage` | `usage` | The "Used in" section and the Unused view. | | `Feature::SmartViews` | `smart_views` | The Recent, Favorites, Mine and Unused views. All files and Trash stay. | | `Feature::KeyboardShortcuts` | `keyboard_shortcuts` | Keyboard shortcuts. See [Keyboard](/guide/keyboard). | ## Defaults | Method | Config key | Notes | |---|---|---| | `defaultLayout(BrowserLayout $layout)` | `browser.default_layout` | `BrowserLayout::Grid` or `BrowserLayout::List`. | | `defaultSort(SortOrder $sort)` | `browser.default_sort` | `Newest`, `Oldest`, `NameAsc`, `NameDesc`, `Largest`, `Smallest`. | | `perPage(int $perPage)` | `browser.per_page` | Items per page. | | `tileSize(int $pixels)` | `browser.tile_size` | Clamped between 120 and 280. | | `acceptedFileTypes(array $mimeTypes)` | `upload.accepted_mime_types` | Wildcards such as `image/*` work. | | `maxFileSize(int $kilobytes)` | `upload.max_file_size` | In kilobytes. | | `duplicates(DuplicateStrategy $strategy)` | `upload.duplicates` | `Ask`, `Allow` or `UseExisting`. | A [`MediaPicker`](/guide/picker-field) field has its own options for one field. --- --- url: /reference/commands.md description: >- The four media-library Artisan commands: install, import, regenerate and prune, with every option and scheduling advice. --- # Commands ## media-library:install ```bash php artisan media-library:install ``` | Option | Purpose | |---|---| | `--force` | Overwrite the published config file. | Publishes `config/filament-media-library.php`, publishes Spatie's `media-library` config and `create_media_table` migration when they are missing, offers to run `migrate`, and links `storage` for the `public` disk. It ends with a list of next steps. See [Installation](/guide/installation). ## media-library:import ```bash php artisan media-library:import spatie --model="App\Models\Post" --collection=gallery --folder="Imported" --dry-run php artisan media-library:import curator --update=posts.cover_id --map=curator-map.json php artisan media-library:import directory uploads/2024 --disk=s3 ``` | Argument or option | Purpose | |---|---| | `source` | `spatie`, `curator` or `directory`. | | `path` | Directory source only. The directory to import, relative to the disk root. | | `--disk=` | Directory source only. Disk to read from. Defaults to the library disk. | | `--model=*` | Spatie only. Import media attached to these model classes. Repeatable. | | `--collection=*` | Spatie only. Import these collections. Repeatable. | | `--move` | Spatie only. Delete the original Spatie media after copying it. | | `--table=curator` | Curator only. The table that holds the media rows. | | `--update=*` | Curator only. Foreign key columns to rewrite to the new ids, as `table.column`. Repeatable. | | `--folder=` | Put everything in this folder. It is created when missing. | | `--map=` | Write a JSON map of old id to new library id to this file. | | `--tenant=` | Tenant key to assign to imported records. | | `--tenant-model=` | Tenant model class. Required with `--tenant`. | | `--dry-run` | Count what would be imported and write nothing. Add `-v` to list each file. | Imports are safe to repeat. Each record stores where it came from in `custom_properties.imported_from`, and files already imported are skipped. A file that fails is skipped with a warning and the run continues. The full walk-through is in [Importing existing files](/guide/importing). ## media-library:regenerate ```bash php artisan media-library:regenerate --only=thumb --missing ``` | Option | Purpose | |---|---| | `--only=*` | Rebuild only these conversion names. Repeatable. | | `--missing` | Build only conversions that do not exist yet. | Rebuilds conversions and responsive images for every file, with a progress bar. Run it after you change the `conversions` array in [the config](/reference/configuration#conversions), or after you move files to a new disk. Conversions marked `queued` still go through your queue, so keep a worker running. ## media-library:prune ```bash php artisan media-library:prune --days=14 ``` | Option | Purpose | |---|---| | `--days=` | Delete trashed files older than this many days. Defaults to `trash.prune_after_days` (30). | Permanently deletes trashed files past the age limit, together with their attachments, and removes unfinished upload chunks older than 24 hours. It covers every tenant. ## Scheduling Schedule `prune` daily. Without it, abandoned chunks and old trash stay on disk. ```php // routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('media-library:prune')->daily(); ``` Keep `regenerate` out of the schedule. Run it by hand when conversions change. ## Assets and translations ```bash php artisan filament:assets php artisan vendor:publish --tag=filament-media-library-translations ``` --- --- url: /reference/events.md description: >- The three events Media Library Pro dispatches: MediaItemUploaded, MediaItemDeleted and MediaItemReplaced, with payload and listener examples. --- # Events All events live in `Hoceineel\FilamentMediaLibrary\Events` and carry one public property, `$item`, the `MediaItem` model. They use `Dispatchable` and `SerializesModels`, so a queued listener works. | Event | Fired when | Payload | |---|---|---| | `MediaItemUploaded` | A file is stored in the library, by upload, URL import, the picker or `media-library:import`. | `public MediaItem $item` | | `MediaItemDeleted` | Files are deleted from the browser, including a move to the trash. It fires once per item. | `public MediaItem $item` | | `MediaItemReplaced` | A file's content is swapped: replace file, save an edit over the original, or restore a version. | `public MediaItem $item` | `MediaItemDeleted` also fires for a soft delete, so check `$event->item->trashed()` to tell trash from permanent removal. `media-library:prune` removes files directly and does not fire it. ## Listening Laravel discovers listeners by their type-hinted `handle` method: ```php namespace App\Listeners; use Hoceineel\FilamentMediaLibrary\Events\MediaItemUploaded; use Illuminate\Contracts\Queue\ShouldQueue; class NotifyAssetTeam implements ShouldQueue { public function handle(MediaItemUploaded $event): void { logger()->info('New asset', [ 'name' => $event->item->name, 'size' => $event->item->humanSize(), 'uploaded_by' => $event->item->uploaded_by, ]); } } ``` Or register one explicitly: ```php use Hoceineel\FilamentMediaLibrary\Events\MediaItemReplaced; use Illuminate\Support\Facades\Event; Event::listen(MediaItemReplaced::class, function (MediaItemReplaced $event): void { Cache::forget("asset-{$event->item->getKey()}"); }); ``` Use `MediaItemReplaced` to clear CDN or page caches, because the file URL stays the same when the content changes. --- --- url: /reference/authorization.md description: >- How MediaItemPolicy and MediaFolderPolicy decide access, how to replace them, and examples for staff-only and per-team rules. --- # Authorization The library asks Laravel's Gate for every action. Two policies ship with the package and are registered for your configured item and folder models, unless you already registered your own. ## Default abilities `MediaItemPolicy` (`Hoceineel\FilamentMediaLibrary\Policies\MediaItemPolicy`): | Ability | Default | |---|---| | `viewAny` | Allowed for any signed-in user. | | `view` | Allowed for the uploader, for any non-private file, and for users who pass `viewPrivate`. | | `create` | Allowed for any signed-in user. | | `update` | Same as `view`. | | `delete` | Same as `view`. | | `restore` | Same as `view`. | | `forceDelete` | Same as `view`. | | `viewPrivate` | Denied for everyone. Grant it to let staff see other people's private files. | `MediaFolderPolicy` has `viewAny`, `view`, `create`, `update` and `delete`. `view` follows the same rule as items: public folders are open, private folders belong to their creator, and `viewPrivate` on the item model opens them to staff. `update` and `delete` follow `view`. It has no `restore` or `forceDelete`. In short, shared media is open to every panel user and private media belongs to its uploader. If that is too open for you, replace the policy. ## Use your own policy Extend the shipped policy and override what you need, then register it with the Gate: ```php namespace App\Policies; use Hoceineel\FilamentMediaLibrary\Models\MediaItem; use Hoceineel\FilamentMediaLibrary\Policies\MediaItemPolicy as BasePolicy; use Illuminate\Contracts\Auth\Authenticatable; class MediaItemPolicy extends BasePolicy { public function delete(Authenticatable $user, MediaItem $item): bool { return $user->isEditor() && parent::delete($user, $item); } public function viewPrivate(Authenticatable $user): bool { return $user->isAdmin(); } } ``` ```php // AppServiceProvider::boot() use Hoceineel\FilamentMediaLibrary\Models\MediaItem; use Illuminate\Support\Facades\Gate; Gate::policy(MediaItem::class, \App\Policies\MediaItemPolicy::class); ``` The package only registers its policy when none exists for the model, so yours wins. If you swapped the model in [the config](/reference/configuration#models-and-tables), register the policy for your model class. Do the same for `MediaFolder` and `MediaFolderPolicy`. ## The canAccess plugin option `canAccess()` gates the library page and its navigation item. It does not touch the picker or the policies: ```php FilamentMediaLibraryPlugin::make()->canAccess(fn (): bool => auth()->user()->can('manage-media')); ``` A user who fails `canAccess()` or `viewAny` gets a 403 on the page. See [Plugin options](/reference/plugin#access). ## Examples ### Staff only Only staff can upload, and only admins see private files: ```php public function create(Authenticatable $user): bool { return $user->is_staff; } public function viewPrivate(Authenticatable $user): bool { return $user->is_admin; } ``` The same `create` check protects the chunked upload endpoint and URL import. ### Per-team rules Inside a tenant panel, [tenancy](/guide/tenancy) already limits the query to the current team. Add a policy rule for who in the team may change files: ```php public function update(Authenticatable $user, MediaItem $item): bool { return parent::update($user, $item) && $user->teamRole($item->tenant_id) !== 'viewer'; } ``` `tenant_id` is the tenant foreign key from [the config](/reference/configuration#key-types). ### Deny everything outside a permission ```php public function viewAny(Authenticatable $user): bool { return $user->can('media.view'); } ``` ## Outside panels On your own pages the same policies apply, and the default is open to every signed-in user. Register a policy before you enable [`standalone`](/guide/outside-panels). See also [Security](/guide/security). --- --- url: /reference/translations.md description: >- Publish and extend the English and Arabic translations, how RTL works, and the --fml CSS tokens you can override in a Filament theme. --- # Translations, theming and RTL ## Translations English (`en`) and Arabic (`ar`) ship with the package. Each is one file, `media-library.php`, in the `filament-media-library` namespace. Publish them to edit the wording: ```bash php artisan vendor:publish --tag=filament-media-library-translations ``` The files land in `lang/vendor/filament-media-library/{locale}/media-library.php`. Laravel merges your copy over the package, so you can keep only the keys you change. ### Add a language Copy the English file and translate the values. Keep the keys, the `:placeholders` and the pluralisation syntax. ```bash mkdir -p lang/vendor/filament-media-library/fr cp lang/vendor/filament-media-library/en/media-library.php lang/vendor/filament-media-library/fr/media-library.php ``` Panel users see the library in the locale your app uses for that request. Plural strings use Laravel's `{1} … |[2,*] …` form, for example `item_count`. Upload and validation messages live under `errors` and `client`, so translators see them in the same file. ## Right-to-left Arabic switches the library to a right-to-left layout with no setup. The layout follows the document direction that Filament sets for the locale, so any RTL locale you add works the same way. The sidebar, breadcrumbs, folder carets, bulk bar and lightbox arrows mirror. File sizes, dimensions and numbers stay left-to-right so they read correctly inside Arabic text. ## Theming The library ships its own compiled CSS, loaded only on pages that use it. You do not add a Tailwind `@source` line or a `@import` to your Filament theme. It reads Filament's color tokens, so it inherits your panel's primary, danger, success and warning colors, your fonts and your dark mode. Primary buttons take their classes from Filament's button color system, so the text color follows Filament's contrast logic for your primary color, in light and dark mode. If you change `->colors([...])` on the panel, the library follows. ### Tokens All tokens start with `--fml-`. They are declared on `.fml`, `.fml-field`, `.fml-cell-files`, `.fml-file-list`, `.fml-gallery` and `.fml-field-preview`, with dark values under `.dark`. | Token | Use | |---|---| | `--fml-surface`, `--fml-surface-raised` | Main and raised backgrounds. | | `--fml-rail`, `--fml-sunken` | Sidebar and recessed areas. | | `--fml-line`, `--fml-line-strong` | Borders and dividers. | | `--fml-text`, `--fml-text-2`, `--fml-text-3` | Primary, secondary and muted text. | | `--fml-hover`, `--fml-press` | Hover and pressed backgrounds. | | `--fml-accent`, `--fml-accent-text` | Accent fill and accent text. Derived from `--primary-*`. | | `--fml-accent-soft`, `--fml-accent-line` | Selected backgrounds and outlines. | | `--fml-focus` | Focus ring color. | | `--fml-danger`, `--fml-success`, `--fml-warning` | Status colors. | | `--fml-shadow-sm`, `--fml-shadow-lg` | Elevation. | | `--fml-radius`, `--fml-radius-sm` | Corner radii. | | `--fml-tile-size` | Grid tile size. Set by the `tileSize` option. | | `--fml-ease` | Easing curve for transitions. | | `--fml-checker-a`, `--fml-checker-b` | The transparency checkerboard behind images. | | `--fml-type-color` | Per-file-type accent for badges. | ### Override tokens Add your overrides to your Filament theme CSS. Match the selector so yours wins, and set dark values separately: ```css .fml, .fml-field, .fml-gallery { --fml-radius: 0.375rem; --fml-surface: #fafaf9; } .dark .fml, .dark .fml-field, .dark .fml-gallery { --fml-surface: #0c0a09; } ``` To change the accent, change the panel's primary color rather than `--fml-accent`, so buttons, focus rings and the rest of Filament stay consistent. --- --- url: /reference/testing.md description: >- Test your app with Media Library Pro: factories, Storage::fake, Livewire tests for the picker and browser, and how to run the package's own suite. --- # Testing ## Factories `MediaItem::factory()` and `MediaFolder::factory()` are available once the package is installed. | Factory | State | What it does | |---|---|---| | `MediaItem` | default | A public JPEG record (1600 x 1067) with no file behind it. Enough for lists, pickers and attachments. | | `MediaItem` | `withImage($width = 640, $height = 480)` | Also writes a real PNG through Spatie, so URLs and conversions work. | | `MediaItem` | `document()` | A PDF record: `application/pdf`, type `Document`, no dimensions. | | `MediaItem` | `private($ownerId = null)` | Private visibility. Pass the uploader's id so they can see it. | | `MediaFolder` | default | A public folder with a random name. | | `MediaFolder` | `private($ownerId = null)` | A private folder owned by that user. | ```php use Hoceineel\FilamentMediaLibrary\Models\MediaFolder; use Hoceineel\FilamentMediaLibrary\Models\MediaItem; $folder = MediaFolder::factory()->create(['name' => 'Campaign']); $photo = MediaItem::factory()->withImage()->create(['folder_id' => $folder->id]); $secret = MediaItem::factory()->private($user->id)->create(); $brochure = MediaItem::factory()->document()->create(['name' => 'Brochure']); ``` ## Disks Fake the library disk so tests never touch real storage. `withImage()` and uploads write to it. ```php use Illuminate\Support\Facades\Storage; beforeEach(function () { Storage::fake('public'); }); ``` Use the disk name from `filament-media-library.disk`. The package's own tests also set `media-library.queue_conversions_by_default` to `false` so conversions run inline. ## Authenticating Sign in a user who can access your panel, and for a tenant panel, set the tenant: ```php use Filament\Facades\Filament; $this->actingAs($user); Filament::setCurrentPanel(Filament::getPanel('admin')); Filament::setTenant($team); Filament::bootCurrentPanel(); ``` Call `bootCurrentPanel()` after `setTenant()` so the plugin turns on tenant scoping. The default policies allow any signed-in user, so register your own policy in the test or use `Gate::before` to test denials: ```php Gate::before(fn ($user, string $ability) => $ability === 'viewAny' ? false : null); $this->get(MediaLibraryPage::getUrl(['tenant' => $team]))->assertForbidden(); ``` ## Testing a form with the picker A form that uses `MediaPicker` is tested like any Filament form. Set the field to an item id: ```php use Livewire\Livewire; $item = MediaItem::factory()->create(); Livewire::test(CreatePost::class) ->fillForm(['title' => 'Launch', 'cover_id' => $item->id]) ->call('create') ->assertHasNoFormErrors(); expect(Post::query()->sole()->cover_id)->toBe($item->id); ``` For a relationship field, pass an array of ids, and read back with `getMediaLibraryItems('gallery')`. The order you send is the order stored. ## Testing the browser `MediaBrowser` is a Livewire component: ```php use Hoceineel\FilamentMediaLibrary\Livewire\MediaBrowser; MediaItem::factory()->create(['name' => 'Root photo']); Livewire::test(MediaBrowser::class)->assertSee('Root photo'); ``` Test the page itself with a request: ```php $this->get(MediaLibraryPage::getUrl(['tenant' => $team]))->assertOk(); ``` ## Outside panels Turn on the standalone mode for the test: ```php config()->set('filament-media-library.standalone.enabled', true); Livewire::test(MediaPickerInput::class, ['name' => 'cover_id', 'image' => true]) ->set('value', (string) $photo->id); ``` The picker's configuration properties are locked, so a test that tries to `set()` one throws Livewire's `CannotUpdateLockedPropertyException`. That is the behavior you want. ## Running the package's suite From the package root: ```bash composer test composer test-parallel ``` `composer test` runs `vendor/bin/pest`. `composer test-parallel` runs `vendor/bin/pest --parallel`, which is faster; chunk directories and fake storage are scoped per worker. Other scripts: `composer format` (Pint) and `composer analyse` (PHPStan). The suite uses Orchestra Testbench with SQLite in memory. To run it on MySQL or PostgreSQL, set `DB_CONNECTION` and the usual connection variables before you run Pest: ```bash DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=media_library_test DB_USERNAME=root DB_PASSWORD= vendor/bin/pest ``` ```bash DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=media_library_test DB_USERNAME=postgres DB_PASSWORD=secret vendor/bin/pest ``` `DB_CONNECTION` selects the connection name in `database.default`. Create the empty database first. --- --- url: /reference/troubleshooting.md description: >- Fixes for the problems you are most likely to hit: 404 images, missing conversions, S3 CORS, upload limits, standalone 403, empty tenants and assets. --- # Troubleshooting ## Images return 404 after upload The files are on the `public` disk but the web server cannot reach them. 1. Run `php artisan storage:link`. `media-library:install` does this when it can. 2. Check `APP_URL`. The `public` disk builds URLs from it, so `http://localhost` in production produces broken links. 3. Check `MEDIA_LIBRARY_DISK` (or `disk` in [the config](/reference/configuration#disk)) points at the disk you expect. ## Thumbnails work, larger sizes are missing The `thumb` conversion runs during the upload. `preview` and any conversion with `'queued' => true` run on the queue. Start a worker: ```bash php artisan queue:work ``` With `QUEUE_CONNECTION=sync` they run inline. To rebuild after a fix, run `php artisan media-library:regenerate --missing`. See [Commands](/reference/commands#media-libraryregenerate). ## The image editor will not open on S3 The editor reads the original pixels in the browser, so the bucket must allow your panel's origin. Add a CORS rule: ```json [{ "AllowedOrigins": ["https://admin.example.com"], "AllowedMethods": ["GET"], "AllowedHeaders": ["*"] }] ``` The editor shows a CORS message when the browser blocks the request. The same applies to a CDN in front of the bucket. See [Storage & S3](/guide/storage). ## Private or S3 links return 403 Temporary URLs and signed links expire after `temporary_url_minutes` (30 by default), and the library caches each URL for half of that. A 403 on a page that has been open for a long time means the link expired: reload. If every link fails straight away on S3, check the bucket credentials and region in `config/filesystems.php`, and that the server clock is correct, because S3 signatures include a timestamp. On a local private disk, links go through a signed route, so a changed `APP_KEY` or `APP_URL` invalidates them. ## Uploads fail on large files Files are sent in chunks of `upload.chunk_size` (5 MB by default), so a 500 MB file is many small requests. Three limits matter: | Limit | Where | Effect | |---|---|---| | `upload.max_file_size` | [Config](/reference/configuration#upload) or `maxFileSize()` | The whole file, in kilobytes. Over it, the upload stops with "This file is larger than the limit". | | `post_max_size` | `php.ini` | Must be larger than `chunk_size` plus a little overhead. | | `upload_max_filesize` | `php.ini` | Each chunk is a multipart file upload, so this must also be larger than `chunk_size`. PHP's default is 2 MB. | If you cannot raise the PHP limits, lower `upload.chunk_size`. The minimum the server accepts is 256 KB. A reverse proxy in front of PHP also needs room: nginx's `client_max_body_size` must exceed the chunk size. An error saying "The upload was interrupted" means a chunk was rejected or out of order. One person can have at most 20 unfinished uploads. Run `media-library:prune` to clear abandoned ones. ## "Not enough storage left" A [`quota`](/reference/configuration#quota) is set and the tenant, or the whole app, is full. Raise the quota or delete unused files. Trashed files still count until they are pruned. ## 403 or 404 on the library outside a panel Pages outside a Filament panel are off by default. Set this in `config/filament-media-library.php`: ```php 'standalone' => ['enabled' => true], ``` While it is off, the standalone upload and download routes return 404, and `` and `` abort with 403. Before you turn it on, register a policy that fits your site. See [Outside Filament panels](/guide/outside-panels). ## The library is empty for a tenant With tenancy on, strict mode shows nothing when no tenant resolves. That is by design, so one tenant never sees another's files. * In a panel with `->tenant(...)`, make sure you opened the library under a tenant URL. * Outside panels, `tenancy.enabled` needs a working `tenancy.resolver`. Check that it returns the current tenant in the request that renders the component. * Files uploaded before you turned tenancy on have no tenant and do not match the scope. * Set `tenancy.strict` to `false` only if you want unresolved requests to see everything. See [Multi-tenancy](/guide/tenancy). ## Livewire "Cannot update locked property" The picker and browser lock their configuration (`multiple`, `maxItems`, `acceptedFileTypes`, `folderId`, `layout` and more) so a visitor cannot change them from the browser. The error means something tried to change one after render: a `wire:model` bound to one of those properties, or a `$wire.set()` call. Pass options as component attributes when you render it instead: ```blade ``` Bind only to `value`, which is what `wire:model` does by default. Clear your browser cache after an upgrade so old JavaScript does not send stale state. ## Styles or scripts do not load Run: ```bash php artisan filament:assets ``` Do it on every deploy, because the assets are copied to `public/`. The library's CSS loads on demand on pages that use it, and you do not need to edit your Filament theme. If you cache views or assets behind a CDN, clear them after upgrading the package. ## Still stuck Search the error text in this page first. If you open a support request, include the package version, Filament and Livewire versions, the disk driver and the browser console output. --- --- url: /changelog.md description: >- Media Library Pro changelog: release notes for every version, listing added features, changes and fixes. --- # Changelog All notable changes to `hoceineel/filament-media-library-pro` are documented here. ## 1.0.1 - 2026-10-10 * Image conversions fall back to JPG when the server's GD or Imagick build cannot write WebP or AVIF, so uploads no longer fail on those servers. ## 1.0.0 - 2026-10-10 First release. * Media library page: folders, tags, favorites, smart views, trash, private files, search, grid and list layouts, keyboard shortcuts. * Inspector with alt text (optionally written by Claude), captions, focal point, image editor, versions, EXIF and usage tracking. * Chunked, parallel uploads with duplicate detection, URL import with SSRF protection, folder upload and storage quotas. * `MediaPicker` form field with drop, paste and upload, reorder, preview and a full library modal. * Table columns (`MediaColumn`, `MediaFileColumn`, `MediaCountColumn`), infolist entries (`MediaEntry`, `MediaGalleryEntry`, `MediaFileEntry`) and a rich editor plugin. * Works outside panels: ``, ``, ``, `` and the `MediaLibraryItems` validation rule. * Multi-tenancy for Filament panels, stancl/tenancy or a custom resolver. * Feature flags and plugin-level defaults. * Import from curator, Spatie media and folders on disk. * English and Arabic translations. * Laravel 12 and 13, Filament 4 and 5, PHP 8.2 to 8.5; SQLite, MySQL and PostgreSQL; local and S3 storage. * Security: bounded chunked uploads, extension hardening, SSRF guard, unguessable storage paths, signed links for private files, locked Livewire state. * Accessibility: WCAG 2.1 AA in automated audits. --- --- url: /pricing.md description: >- Media Library Pro pricing. One-time purchase from $49, perpetual use. Single project, Studio and Agency licences with a 14-day money-back guarantee. --- # Media Library Pro pricing One-time payment, perpetual use. Every tier is the same software with no feature gates; tiers differ only in how many production projects the licence covers and how long new versions can be downloaded. Prices are in USD and exclude sales tax or VAT. | Plan | Price | Production projects | Updates | Renewal | | --- | --- | --- | --- | --- | | Single project | $49 | 1 | 12 months | Optional, $29/yr | | Studio | $129 | Up to 5 (yours or your clients') | 24 months | Optional, $69 | | Agency | $299 | Unlimited, including every tenant of a SaaS | Lifetime | Never | ## FAQ * **What happens when the update window ends?** Nothing breaks. The last version you downloaded keeps running forever. Renewing only buys another window of new releases. * **How are project counts enforced?** They are licence terms, not a runtime limit. The package never contacts a server. * **Is the source included?** Yes. Your key authenticates `composer require hoceineel/filament-media-library-pro` from the private registry and the full source lands in `vendor/`. * **Which versions are supported?** Filament 4 and 5, Livewire 3 or 4, Laravel 12 or 13, PHP 8.2+. * **Refunds?** 14-day money-back guarantee. See the [Refund Policy](/legal/refund-policy).