NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

MediaLibrary::for($model) scopes adds to one owner. Every add* method returns a PendingFileAdd builder; the terminal toBucket() writes the file and returns the Media. Sources cover every shape of input:

use RoundlyConsulting\MediaLibrary\Facades\MediaLibrary;

$media = MediaLibrary::for($user);

$media->add($request->file('avatar'))->toBucket('avatar');            // UploadedFile or local path
$media->addFromRequest('avatar')->toBucket('avatar');                 // request upload by key
$media->addFromUrl('https://example.com/poster.png')->toBucket('gallery');
$media->addFromDisk('incoming/doc.pdf', 's3')->toBucket('gallery');
$media->addFromString($bytes)->usingFileName('note.txt')->toBucket('gallery');
$media->addFromBase64($base64)->toBucket('gallery');
$media->addFromStream($resource)->usingFileName('upload.bin')->toBucket('gallery');

The trait shorthand

The InteractsWithMedia trait offers the same adds on the model itself — they go through the facade's manager, so MediaLibrary::fake() sees them too:

$user->addMedia($request->file('avatar'))->toMediaBucket('avatar');
$user->addMediaFromRequest('avatar')->toMediaBucket('avatar');
$user->addMultipleMediaFromRequest(['a', 'b']);                       // PendingFileAdd[] (keys without a file skipped)
$user->addMediaFromUrl($url);  $user->addMediaFromDisk($path, 's3');
$user->addMediaFromString($bytes);  $user->addMediaFromBase64($b64);  $user->addMediaFromStream($resource);

The PendingFileAdd builder

Chain the builder to set a name, custom properties, disks, visibility and more before the terminal call:

$media = MediaLibrary::for($user)->add($request->file('avatar'))
    ->usingName('Profile photo')                 // display name
    ->usingFileName('avatar.jpg')                // stored filename (made safe, see Upload safety)
    ->withCustomProperties(['alt' => 'Jane'])    // arbitrary metadata
    ->withProperty('source', 'signup')           // one property at a time
    ->useDisk('cold')                            // override the bucket/config original disk
    ->storingVariantsOnDisk('hot')               // override the bucket/config variants disk
    ->withVisibility('private')                  // override the bucket default
    ->onQueue('media')                           // queue any queued variants on this queue
    ->asDraft()                                  // store as an unbound draft
    ->toBucket('avatar');                        // terminal: returns Media (toMediaBucket() is the same)

The terminal call validates the file before anything is written — the bucket’s mime allowlist, its size cap (maxFileSize(), else media.max_file_size) and, for images, its minDimensions() / maxDimensions() — and throws a typed exception on failure: FileUnacceptableForBucket, FileDoesNotExist, DiskDoesNotExist, or RemoteFileRejected (addFromUrl() with a non-http(s) URL or a body over the size cap). These are the same limits MediaLibrary::rulesFor() derives, so a FormRequest and the package never disagree.

A source is only ever read: a local path you add is never moved or deleted — the package always stores a copy.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.