1tomany/storage-bundle
A Symfony bundle for uploading files to remote storage services like Amazon S3, Cloudflare R2, Google Cloud Storage, and Azure Blob Storage, plus a mock client for testing integrations without a network connection.
Architecture fit
1tomany/storage-bundle is a Symfony bundle in the strict sense: it ships a bundle class, a DependencyInjection\Extension that parses config/packages/onetomany_storage.yaml, compiler-pass-driven service tagging (onetomany.storage.client), and a factory that resolves ClientInterface from the container based on a config key. None of that scaffolding maps onto Laravel. Laravel's container is PSR-11 only — it has no parameter bag, no bundle registration, no YAML extension loading, and no compiler passes. The only way to use the bundle as designed is to boot a Symfony KernelInterface inside a Laravel process, which means pulling symfony/framework-bundle, symfony/http-kernel, symfony/dependency-injection, and symfony/config into an application that already pins its own Symfony component versions.
What is portable is the inner design, and it is genuinely clean:
UploadActionInterface, DownloadActionInterface, DeleteActionInterface) over a single ClientInterface — good interface segregation, trivially mockable, easy to wrap in a Laravel Storage-style facade.custom_url as a CDN-host override is the right primitive and is a one-line concat to reimplement.The strategic problem is that almost every capability described in the README already exists natively in Laravel via Flysystem v3: S3, Cloudflare R2, GCS, and Azure adapters are first-party or first-class documented, Storage::fake() covers the mock-client use case, Storage::url() covers custom domains, and Storage::temporaryUrl() covers presigned access. The bundle's functional surface — upload by path/format/key, download, delete — is a strict subset of what Flysystem exposes. There is no listing, no exists, no copy/move, no presigned URL generation, no streaming or multipart/chunked upload path documented. For a file-storage layer, presigned URLs and multipart uploads are not nice-to-haves; they are the difference between a demo and a production system.
Integration feasibility
Low-to-moderate, and the effort is almost entirely framework tax rather than storage work. Realistic paths, in descending order of preference:
Storage::fake() for tests. Captures 100% of the value with 0% of the compatibility risk.Contract/ and Client/ namespaces, discard the bundle class, DI extension, and YAML, and write a ~50-line Laravel service provider that binds UploadActionInterface etc. to singletons. You inherit the maintainer's S3 client logic and keep your own container.symfony/http-kernel version conflict resolution against Laravel's pinned symfony/http-foundation, symfony/console, and symfony/routing.The aws/aws-sdk-php-symfony dependency compounds this: it is also a Symfony bundle registering its own extension and .env conventions (AWS_MERGE_CONFIG), which is orthogonal to Laravel's config/services.php + config/filesystems.php idiom. You would end up maintaining credentials in two configuration systems.
Technical risk
High, concentrated in four areas:
Key questions
custom_url is a global scalar in the bundle config — per-tenant CDN domains would need a fork.Stack fit
Poor at the framework layer, good at the abstraction layer. The bundle assumes a Symfony application kernel; we have a Laravel application with a Flysystem-backed Storage facade and a FilesystemManager that already resolves disks lazily, supports Storage::fake(), and exposes the full Flysystem operation set. Adopting the bundle means running two storage abstractions side by side — the highest-cost steady state we could choose, because every future file feature has to be implemented twice or arbitrated case by case.
Capability comparison, which is where the decision actually gets made:
| Capability | Bundle | Laravel native |
|---|---|---|
| S3 / R2 / GCS / Azure | Yes (S3 family via AWS SDK; GCS/Azure claimed in README but config only documents S3 + mock) | Yes, documented first-party |
| Upload / download / delete | Yes | Yes |
| List / exists / copy / move | Not documented | Yes |
| Presigned / temporary URLs | Not documented | Yes (temporaryUrl) |
| Multipart / streaming | Not documented | Yes |
| Test double | Mock client (happy path) | Storage::fake() + assertExists |
| CDN host override | custom_url config |
url in disk config |
| Action-class ergonomics | Yes — the one genuine differentiator | Build it, ~1 day |
If the decision is nonetheless to proceed, restrict scope to the S3 and mock clients and treat GCS/Azure as unproven until spiked.
Migration path
There is no migration to perform, because we are not currently on this bundle. Frame the work as a two-week timeboxed spike with a hard go/no-go at the end:
1tomany/storage-bundle + aws/aws-sdk-php-symfony alongside a standard Laravel 11/12 app. Objective: get UploadActionInterface injected and a file on S3. Deliverable: a written list of every Symfony component that had to be pinned or patched. This is the number that decides the programme — if it exceeds three pins, stop.AWS_ENDPOINT. Simultaneously, one engineer builds the Flysystem-based equivalent of the action interfaces as a control.Compatibility and sequencing
Pin the bundle at an exact version, not a caret range — the v2→v3 contract change is precedent enough. Add a composer require --dev constraint check to CI so a transitive Symfony bump cannot silently change behaviour. Sequence any adoption behind an internal StorageAdapter interface we own, so a future rip-out is a one-file swap rather than a codebase-wide refactor. Do not migrate existing Storage:: call sites until the new path has run in production for at least one release cycle on a single non-critical upload flow.
Maintenance
The recurring cost is the real objection. Every Symfony major release forces a decision: upgrade, fork, or pin and accumulate CVE exposure across the symfony/* tree we pulled in. Laravel's own Symfony dependencies move on Laravel's release cadence; the bundle moves on its author's. These will diverge, and the divergence will land as an unplanned maintenance item mid-quarter. Assign ownership explicitly before adoption — an unowned storage layer becomes an unowned incident.
Support
No vendor, no SLA, no security policy, no disclosed response time. Community support is a single GitHub issue tracker with zero dependents, which means we are likely the only organisation filing issues. Budget for the team owning the code, not for asking upstream. MIT license gives us the legal right to fork; it does not give us the capacity.
Scaling
Throughput and durability scale with S3/R2, not with this bundle — it is a thin request/response wrapper. The scaling risks are architectural:
upload_max_filesize, post_max_size, and the web server body limit, and must route through the application process rather than direct-to-bucket. For media at any real volume this is the binding constraint.custom_url is a single global scalar, so multi-tenant or multi-region CDN hostnames require a fork.Failure modes
custom_url is string concatenation with no verification that the CDN actually fronts the bucket. Misconfiguration produces URLs that 403 or, worse, 200 with stale content from a previous key owner. Nothing in the documented surface detects this.Ramp-up
Any Laravel developer can read this code — it's small and well-named. The friction is environmental: bundle registration, YAML extension config, tagged services, and Symfony secret conventions are unfamiliar to a Laravel-majority team. Estimate two to three days for a senior engineer to reach a working local upload, and one to two weeks to reach something production-grade with tests and error handling. Compare against roughly one day to build the same action interfaces over Flysystem, using APIs the team already knows.
TPM recommendation: do not adopt 1tomany/storage-bundle as a dependency. The abstraction pattern is worth copying — narrow action interfaces over a client interface, with a mock provider — and can be reproduced over Laravel's native Flysystem disks in a day, with presigned URLs, multipart support, listing, and Storage::fake() arriving for free. Revisit only if the bundle grows meaningful adoption and adds temporary-URL and multipart support, or if a genuine multi-provider requirement emerges that Laravel's disk configuration cannot express.
How can I help you explore Laravel packages today?