---
title: "Adding media — Media Library for Laravel | Roundly"
description: "Add files from uploads, requests, URLs, disks, strings, base64 or streams through MediaLibrary::for() and the PendingFileAdd builder."
url: https://roundly-consulting.com/open-source/docs/media-library-for-laravel/adding-media
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Media Library for Laravel](https://roundly-consulting.com/open-source/docs/media-library-for-laravel.md)

# 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:

```php
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:

```php
$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:

```php
$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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/media-library-for-laravel.md)
