Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Attachment Library Laravel Package

van-ons/laravel-attachment-library

A Laravel library for attaching files to Eloquent models. Install via Composer, run the install command to add migrations and assets, then use the HasAttachments trait on any model to link and retrieve attachments through a simple relationship.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish. Two commands, nothing exotic:

composer require van-ons/laravel-attachment-library
php artisan attachment-library:install
php artisan migrate

The install command publishes the migrations and assets (config, Glide-related files, possibly views). Commit the published files — they are now yours to maintain across upgrades.

2. Add the trait to the models that own files. Not to Attachment itself, and not globally:

use VanOns\LaravelAttachmentLibrary\Concerns\HasAttachments;

class Post extends Model
{
    use HasAttachments;
}

3. Where to look first, in this order:

  • docs/README.md in the repo — the README is a stub; the real API surface (upload, collections, ordering, focal point) lives there.
  • config/attachment-library.php — disk name, path prefix, image presets, Glide settings. Nearly every "why is my file here / why is my URL wrong" question is answered in this file.
  • The published migrations — they tell you the exact columns (alt, title, description, caption, owner, file size, focal point, ordering, polymorphic pivot). Faster than reading source.
  • VanOns\LaravelAttachmentLibrary\Models\Attachment — the @property docblock is your field list (note it was stale until v1.6.1, so fix it yourself if you're on an older pin).
  • AttachmentManager — where the actual upload/resize/replace logic lives, per the changelog.

4. First use case: a Post with one hero image. The README stops at linking an existing attachment, which is the unusual part — attachments are standalone records you create first, then link:

$attachment = $post->attachments()->create([...]); // or via AttachmentManager, see docs
$post->attachments()->attach($attachment);

// Read it back
$hero = $post->attachments()->first();

Get this path working end-to-end (upload → row in attachments → row in the pivot → URL that renders) before touching collections, presets, or focal points. If the URL 404s, the problem is almost always the disk config or the Glide route, not your model.

Implementation Patterns

Treat attachments as a shared media library, not per-model uploads. The polymorphic many-to-many pivot means one Attachment row can back many models. That is a feature (reuse a logo across pages) and a liability (you now own orphan cleanup). Pick a posture early:

  • Shared library: attachments are created once and attached many times. Add a usage_count/last_used_at view or a pruning command.
  • Owned files: create and attach in the same transaction, and detach + delete when the owner goes away.

Own the lifecycle explicitly. There is no documented cascade. Wire it yourself:

static::deleting(function (Post $post) {
    $post->attachments()->each(function (Attachment $a) {
        if ($a->posts()->count() <= 1) { // last owner — verify the inverse relation name in docs
            $a->delete();
        }
    });
});

Or safer: never delete inline. Detach on model delete, and run a scheduled command that deletes attachments with zero pivot rows older than N days. That survives soft deletes and failed transactions.

Wrap uploads in a small service. Don't let AttachmentManager calls sprawl through controllers. One AttachFile action that takes an UploadedFile, a disk, and an optional collection, returns an Attachment. Your validation, virus scanning, and mime whitelist live there too — the package blocks executable extensions (since v0.0.1), but that is not a substitute for mimes:/max: rules on your FormRequest.

Use collections instead of inventing your own grouping. Since v1.0.0 you can attach into named collections on a model — gallery, documents, downloads. This is the cleanest way to model "a post has many images but exactly one hero," and it beats a type column on your own pivot extension.

Ordering is a first-class concern (v1.2.4). Read the docs for the exact API, then expose a drag-and-drop reorder endpoint that writes the ordering column. Eager-load in the right order so you don't sort in Blade on every request.

Render through the package's route, not Storage::url(). S3 and remote disks are supported (v1.0.0, v1.2.9) precisely because URLs are generated by the library, and images are served/resized by a Glide-backed route. Presets (v1.2.2) give you named sizes:

<img src="{{ $attachment->url('card') }}"
     alt="{{ $attachment->alt ?? '' }}"
     loading="{{ $attachment->loading ?? 'lazy' }}">

Define presets in config so marketing can ask for a new crop without a deploy.

Focal point (v1.2.0) + fit (v0.0.7) are your crop-quality tools. Store the focal point from your admin crop UI, and the server-side resize respects it. If faces are getting cut off in thumbnails, the fix is a focal point, not a new preset.

File replacement (v1.1.0) for "swap the PDF" flows. Replace in place so existing pivots and URLs keep working. Caveat in the next section.

Backfill with the sync functionality. v1.2.1 added inserting missing files, and v1.2.5 improved auto-sync performance — so there's a command that scans a disk directory and creates Attachment rows for files not yet in the DB. That is how you migrate an existing storage/app/public/uploads directory into the library without writing a custom importer.

Test with fakes. Storage::fake('attachments') plus UploadedFile::fake()->image('hero.jpg') gets you 90% of the coverage. Assert the pivot row, the disk path, and that the served URL returns 200.

Gotchas and Tips

Published migrations are a permanent upgrade obligation. The install command copies files into your app; new versions can ship new migrations or config keys that won't appear automatically. Add an upgrade step to your routine: on every bump, diff the package's database/migrations and config against yours. Pin an exact version in composer.json — this is a 3-star, zero-dependent package, so treat it as vendored code you may have to fork. MIT license makes that cheap.

Orphan attachments are the default outcome. Nothing in the docs describes cleanup when the last owner is deleted. Files will accumulate on disk and rows in attachments will point at nothing. Build the pruning command on day one, not month six.

Missing files must be handled in the view layer. v0.0.5 made Glide return 404 for missing files and v1.3.1 stopped the crash when a file is gone from disk — but that means an <img> tag silently 404s in production. Guard it:

@if ($attachment->existsOnDisk()) {{-- verify method name in docs --}}
    <img src="{{ $attachment->url('thumb') }}" alt="...">
@endif

Replacing a file may serve stale resized images. Glide caches derivatives keyed by path. If replace() keeps the same filename, previously cached sizes can be served until the cache is cleared. Verify this on your setup immediately after implementing a replace flow; if confirmed, bust the cache (or change the filename) as part of replacement.

The Glide route is a public HTTP surface. Check the published route file and config for middleware. Any attachment URL is guessable if paths are sequential or filename-derived. If attachments include contracts, invoices, or personal documents, put the route behind auth (or a signed-URL scheme) and don't reuse the default image-serving route for sensitive files. There was also a route-matching fix in v1.2.8 for long paths — relevant if you register your own catch-all image routes alongside it; register the package's route first.

Remote disks break naive assumptions. With S3, there is no local path — anything you write that calls storage_path() or file_get_contents() on an attachment will fail. Go through the model/manager. Local dev on public and production on S3 is the classic config drift; test the URL path on both.

Ordering and eager loading. $post->attachments inside a loop is N+1. Eager-load with the ordering applied, and if you render several collections, load them scoped rather than filtering in Blade.

Files without extensions exist. v0.0.6, v1.2.3 and v1.2.7 all touch the missing/unreadable-extension cases, which tells you it's a rough edge. If your users upload generated files (exports, signed PDFs without suffix), test that path explicitly — resize and size-detection may return null.

Stale @property blocks on the Attachment model were only corrected in v1.6.1. If you're below that, PHPStan/IDE autocomplete will lie to you about columns. Either upgrade or add your own docblock.

Extension points are limited, by design. There's no documented repository/contract layer — it's a trait, a model, and a manager. If you need custom behaviour (per-tenant disks, custom naming, watermarking), subclass Attachment, override the model binding in config if the package allows it, and keep your overrides in one directory so a fork stays trivial. Don't scatter trait assumptions across fifty models; apply HasAttachments only where files genuinely live.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource