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

Storage Bundle Laravel Package

opensoft/storage-bundle

OpenSoft Storage Bundle is a PHP/Symfony bundle for integrating storage backends into applications. It provides bundle configuration and services to manage file storage workflows, helping developers wire adapters and handle stored files with reusable, framework-friendly components.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package — the first thing to internalise. Composer will pull it in, but there is no service provider, no auto-discovery, and no config/storage.php. If you're working in a Laravel codebase you are effectively reading it as a reference implementation for a StoredFile model plus a storage-manager service, and porting the parts you need. If you're in Symfony, register Opensoft\StorageBundle\OpensoftStorageBundle in bundles.php and import the bundle's routing/services config manually — it predates Symfony Flex recipes.

Read these three things before writing any code, in this order:

  1. Resources/config/services.yml (or services.xml) — tells you the real service IDs and what the bundle expects to be injected. Everything else is guesswork until you've seen this.
  2. The StoredFile entity and its Doctrine mapping — the schema is the contract. Fields are typically storage/adapter name, path/key, original filename, mime type, byte size, and a hash. Copy this schema even if you rewrite the code.
  3. The manager/service class (usually StorageManager, FileManager, or similar) — the public methods there (store, remove, getUrl, getPath) are your entire API surface.

Minimal first use case — get one file round-tripping:

// 1. Persist after a successful write to disk/bucket
$file = new StoredFile();
$file->setStorage('local');          // adapter name from bundle config
$file->setPath('uploads/2024/09/uuid.pdf');
$file->setOriginalName($upload->getClientOriginalName());
$file->setMimeType($upload->getMimeType());
$file->setSize($upload->getSize());
$file->setHash(hash_file('sha256', $upload->getRealPath()));
$em->persist($file);
$em->flush();

// 2. Resolve later without ever rebuilding the path by hand
$url = $storageManager->getUrl($file);   // or getPath() for local

That's the whole value proposition: the database becomes the source of truth for where a file lives, so moving from local disk to S3 is a config change plus a backfill, not a grep across your codebase.

Implementation Patterns

Write file first, row second — always in that order. The database transaction and the object-store write are not atomic and never will be. The safe sequence is: write to storage under a generated key → persist the row → commit. If the row insert fails you have an orphan blob you can garbage-collect by hash. Do it the other way round and you have a row pointing at nothing, which surfaces as a 500 on a user's avatar two weeks later.

Never expose raw paths to your application code. Route every consumer through the manager's getUrl()/getPath(). The moment a controller does '/uploads/' . $file->getPath(), the storage name column is dead weight and you can't migrate adapters.

Deduplicate by hash before you write. The bundle records a hash; use it. Check for an existing row with the same hash and size before uploading, and if found, point the new record (or a join row) at the same storage key. This is the single highest-leverage pattern for user-avatar and attachment workloads, and most implementations of this bundle skip it.

Soft-delete the row, hard-delete the blob asynchronously. Mark the record deleted in the same request, then let a queued job call the manager's remove method. A failed S3 delete becomes a retryable job instead of a lost pointer. The bundle's remove call typically takes the entity, so keep the entity around long enough to act on it.

Multi-storage is the reason to use this at all. Configure two adapters (local, s3) and write the adapter name per row from day one, even if you only ever use one. When a customer asks for their data export on cold storage, or you need thumbnails local and originals remote, the column already exists.

For Laravel specifically, the cleanest port is: an Eloquent model mirroring the bundle's schema (storage, path, original_name, mime_type, size, hash, created_at), a StorageManager bound as a singleton in a service provider, and a FilesystemManager-backed adapter map keyed by the same storage names. Keeping the schema identical means you can migrate to the real bundle later if you ever move to Symfony, and it means the mental model transfers directly.

Wrap the manager in your own thin facade. The bundle is unmaintained (last release 2019, 4 stars, zero dependents). Any code that touches StorageManager directly is code you'll rewrite when you fork it. One interface with store, url, delete methods keeps the blast radius to a single class.

Gotchas and Tips

Assume you will fork it. Zero dependents, four stars, last release mid-2019 — this is a reference implementation, not a maintained dependency. Vendor the source into your own app/Support/Storage rather than depending on Packagist. You'll need PHP 8 compatibility fixes (nullable return types, Symfony\Component\... class moves) within the first hour.

Symfony 2/3-era config assumptions. Expect %kernel.root_dir%, doctrine.orm.entity_manager aliases, and YAML service definitions that assume auto-wiring is off. If you're on Symfony 5+, you'll be rewriting the service file, not editing it. Check whether the entity mapping is XML/YAML — you may need to switch to attributes/annotations and adjust the doctrine.orm.mappings config.

The DB/file consistency gap is the bug you will actually hit. Anything that deletes — user deletion, cleanup cron, admin panel — must go through the soft-delete-then-job pattern. Add a scheduled reconciliation command that lists storage keys and diffs them against rows; run it weekly in staging. This catches both orphan blobs and dangling rows, and it's the only defence you have.

Signed URLs expire; file IDs don't. If you store a URL anywhere (cache, email, search index), you've stored something with a TTL. Cache the StoredFile ID and resolve the URL at render time. Same rule applies if you're on S3 with presigned GETs.

Path traversal and filename collisions. Store under a generated UUID or content hash, keep the user's original filename only in the metadata column. If the bundle's store() accepts a client-supplied name, sanitise before it reaches the filesystem adapter — this is the classic 2019-era bundle oversight.

MIME detection is unreliable for office formats. The bundle records whatever you pass it; don't trust getMimeType() from the upload alone for anything that gates access. Re-detect server-side (finfo) and store both the sniffed type and the client-claimed type if you need to debug.

Debugging tip: when a file 404s but the row exists, log the resolved adapter name and the resolved path together. Nine times out of ten the adapter config key doesn't match the string stored in the storage column, and the manager silently falls back to the default adapter.

Extension points worth knowing: the manager is usually constructor-injected with the adapter registry and the entity manager, so subclassing or decorating it is straightforward. If you need per-tenant buckets, that's the seam — decorate the adapter lookup rather than editing the bundle. If the bundle builds URLs internally, override that method first; it's the one most likely to be wrong for your CDN setup.

Score context: an "opportunity" score near 10 alongside four stars means the feature gap is real and unfilled, not that the package is ready. Budget a day for the port and treat it as a starting schema, not a dependency.

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