Adding media
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, MediaOwnerNotSaved (an owner that was never saved — drafts excepted), InvalidVisibility (withVisibility() given anything but exactly public or private, Private included), FileCannotBeWritten (the disk refused the write; no row is saved) or RemoteFileRejected (a URL addFromUrl() refuses — see Upload safety). 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. Uploads, streams and disk files are copied in chunks, never read into memory whole, and the display name is cut to the 255 characters the name column holds.
Inside your own transaction
An add writes its files before your own DB::transaction() is decided. Should that transaction roll the new row back, the add deletes the files it wrote — the original and the variants rendered during the add, on whichever disks they went to — so no file is left without a row:
- A deduplicated original shared with an existing media is kept, and a file some media row still stores as its original is never deleted.
- A nested DB::transaction() that rolls back deletes only the files of the adds inside it; one that committed keeps them until the outer transaction ends, and a commit of the outermost one keeps every file.
- Queued variants are dispatched only once the outermost transaction commits, so a rollback never leaves one behind.
- A delete that fails during the rollback is reported to your exception handler and never replaces the exception that caused it; that file stays on the disk.
- Laravel’s RefreshDatabase and DatabaseTransactions test traits roll back their wrapping transaction with events off, so that rollback never cleans up a test’s adds.
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 cryptoBy 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.