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

Attachment Laravel Package

agrodata/attachment

A Laravel/PHP package for managing file attachments. Upload, associate files with models, and retrieve attachments through a clean API, simplifying polymorphic file handling in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin client for the Agrodata attachment service: it wraps the remote file API so you don't hand-roll HTTP calls. Treat it as infrastructure, not a domain library — it moves bytes and returns identifiers/URLs; your app still owns the relationship between an attachment and its parent record.

Install and wire it up

composer require agrodata/attachment
php artisan vendor:publish --tag=attachment-config   # check the tag in the README/src

Configure credentials in .env (names will match config/attachment.php — confirm before guessing):

ATTACHMENT_BASE_URI=https://attachments.agrodata.example/api/v1
ATTACHMENT_TOKEN=your-service-token
ATTACHMENT_TIMEOUT=30

Then php artisan config:clear and hit the service once from tinker to prove connectivity before writing any feature code.

Where to look first

  1. config/attachment.php — base URI, auth, timeout, disk defaults.
  2. src/*ServiceProvider.php — what's bound in the container, whether a facade alias exists, and which config keys are actually read.
  3. src/Contracts/ — the interface you should type-hint against rather than the concrete client.
  4. tests/ — the fastest source of truth for request shape and response payloads, since docs for low-adoption packages are usually thin.

First use case: upload a file and store the handle

use Agrodata\Attachment\Contracts\AttachmentClient;

class StoreContractDocument
{
    public function __construct(private AttachmentClient $attachments) {}

    public function handle(UploadedFile $file, Contract $contract): Attachment
    {
        $result = $this->attachments->upload($file, [
            'context' => 'contract',
            'reference' => (string) $contract->id,
        ]);

        return $contract->attachments()->create([
            'external_id' => $result['id'],
            'url'         => $result['url'] ?? null,
            'filename'    => $file->getClientOriginalName(),
        ]);
    }
}

That's the whole loop — upload, persist the remote id, render the URL later. Everything else is lifecycle management around that.

Implementation Patterns

Inject the contract, not the facade. Type-hint AttachmentClient (or whatever the interface is called) in constructors. Facades are convenient in controllers and tinker but make it painful to swap in a fake during tests, and they hide the dependency from static analysis.

Always keep a local record of the external id. The service is the blob store; your database is the index. A small attachments table with external_id, disk, path, mime, size, uploaded_by, plus a polymorphic attachable relation gives you listing, authorization, and orphan cleanup without round-tripping to the API for every page render.

Separate "upload" from "attach". Uploading is an HTTP concern that can fail; attaching is a database transaction. Do the upload first, then create the row, and delete the remote file in a catch/finally if the DB insert fails. The reverse order leaves you with rows pointing at nothing.

Stream large files; don't buffer. For PDFs, spreadsheets, and soil-sample images, pass the file handle or UploadedFile straight through rather than file_get_contents(). PHP's memory limit is the usual ceiling here.

$stream = Storage::disk('s3')->readStream($path);
$result = $this->attachments->upload($stream, ['filename' => basename($path)]);

Push multi-file uploads to a queue. A request that uploads twelve photos will time out before the client does. Dispatch a job per file, record a pending status, and update on completion. This also gives you free retries.

Prefer redirects or signed URLs over proxying. If the package returns a public/signed URL, return redirect($attachment->url) from the controller. Proxying bytes through your PHP workers doubles egress and memory for no benefit. Only proxy when you need to enforce authorization on a private bucket — and then stream with response()->streamDownload().

Delete in the same place you create. A deleting model observer that calls $client->delete($externalId) keeps the remote store from growing unboundedly. Guard it with is_null($externalId) so failed uploads don't throw during cleanup.

Test with Http::fake(). Because this is an HTTP client, you get full coverage without a live endpoint:

Http::fake([
    'attachments.agrodata.example/*' => Http::response(['id' => 'abc123', 'url' => 'https://cdn/x.pdf'], 201),
]);

$this->post('/contracts/1/documents', ['file' => UploadedFile::fake()->create('a.pdf', 100)])
     ->assertRedirect();

Http::assertSent(fn ($req) => $req->hasFile('file'));

Add one live smoke test behind a @group external annotation so contract drift from the real API is caught before release, not during it.

Wrap it in a small domain service. One class — DocumentService or similar — that owns upload, attach, and delete. Your controllers and jobs call that, never the package directly. When the API changes or you outgrow the package, you rewrite one file.

Gotchas and Tips

Check the base URI carefully. https://host/api/v1 and https://host/api/v1/ behave differently when the client concatenates paths — you'll get //attachments or a missing segment. Verify with Http::assertSent in a test rather than eyeballing a 404 in staging.

HTTP failures may not throw. Many small clients return the response object and let you decide. Always check ->successful() (or whatever the package's result object exposes) before reading ['id'], or you'll get "undefined index" errors masking a 401.

Auth tokens and clock skew. If the token is a bearer JWT with an expiry, cache it and refresh on 401 with a single retry. Never log the token; redact it in the exception handler if the package includes request context in errors.

Don't blindly retry uploads. Laravel's Http::retry() will happily POST twice on a timeout that actually succeeded server-side, leaving duplicate remote files. Retry only GET/DELETE, or use an idempotency key if the API supports one. Upload retries with orphan risk are better handled by a reconciliation job.

Raise the timeout, and set a separate connect timeout. Default 30s is generous for connect and stingy for a 40 MB upload over a slow link. If the package exposes both, set connect to 5s and total to 120s for upload paths.

Filename encoding is a quiet killer. Accented Portuguese filenames (laudo-análise.pdf) round-trip badly through multipart headers. Normalize to ASCII slug + preserve the original in your own column for display.

Config caching bites in production. php artisan config:cache freezes .env reads. Every config value the package uses must go through config('attachment.*'), not env(). If you find env() calls in src/, that's a bug — patch locally or set the values in config/attachment.php directly.

Local vs remote disk mismatch. Files staged on local won't survive a container restart mid-upload. Stage in S3/TOS and stream from there if your infra is ephemeral.

Version pinning matters more than usual here. Zero stars, zero dependents, no tagged releases visible from the outside. Pin an exact version ("agrodata/attachment": "1.2.3") or a commit hash in composer.json, commit the lock file, and vendor the source into app/Support if the Bitbucket repo ever becomes unreachable. Assume you may be maintaining this yourself in a year.

Extension points to look for: a container binding for the HTTP client (so you can inject a configured Guzzle instance with middleware for logging and correlation IDs), a config-driven disk or storage key, and any events dispatched after upload. If the client is final or news up its own Guzzle, you'll need a decorator around the interface instead — decide that before you've written fifty call sites.

Log correlation ids on every call. Attach a request id to the client via middleware, and include the returned attachment id in your own logs. When a user reports a missing document, matching your app log to the vendor's support request is the difference between a five-minute and a two-day debug.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager