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

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.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

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:

  • Three narrow action contracts (UploadActionInterface, DownloadActionInterface, DeleteActionInterface) over a single ClientInterface — good interface segregation, trivially mockable, easy to wrap in a Laravel Storage-style facade.
  • A mock client shipped as a first-class provider rather than a test-only afterthought.
  • 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:

  1. Do not take the dependency. Reimplement the action-interface pattern (~1 day) over Flysystem and keep Storage::fake() for tests. Captures 100% of the value with 0% of the compatibility risk.
  2. Fork and harvest. Vendor the 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.
  3. Boot the bundle. Only defensible if the organisation already runs Symfony components inside Laravel for other reasons. Expect 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:

  • Compatibility drift. Release notes show a v2→v3 major (contracts format changed) and a v3.0.1 whose sole content is "Upgrade to Symfony 8.0." That is a maintenance posture tracking Symfony majors aggressively with breaking changes at the contract layer. Every Symfony major becomes a forced migration for us, on their schedule.
  • Adoption signal. Zero stars, zero dependents, no disclosed CI or coverage status. There is no population of production users to absorb bugs before we do.
  • Functional gaps as silent failures. No presigned URLs means private-object access either fails or gets worked around with public buckets — a security regression that surfaces in review, not in tests. No multipart means large uploads hit request-size ceilings at runtime.
  • Bus factor. Single-author, single-organisation (1:N Labs). MIT license means we can fork; it does not mean we can staff a fork.

Key questions

  1. What is the actual business driver — multi-cloud portability, or S3/R2 with a CDN? If the latter, Laravel's native disk config already answers it and this evaluation ends here.
  2. Do we need private-object access via presigned/temporary URLs? If yes, the bundle does not cover it and we are writing that layer ourselves regardless.
  3. What is the largest object we must accept (single-shot vs. multipart)? Who owns the answer, and has it been validated against the 2026 roadmap?
  4. Is the CDN hostname for each bucket known and stable, or does it vary per tenant/environment? custom_url is a global scalar in the bundle config — per-tenant CDN domains would need a fork.
  5. Who owns patching this if the upstream goes quiet for two quarters — and is that person on the team today?
  6. Does our test strategy require the mock client to fail the way S3 fails (throttling, 403 on bad credentials, partial writes)? The documented mock appears to be a happy-path in-memory stub, which validates wiring but not failure handling.

Integration Approach

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:

  • Week 1 — feasibility. Branch install of 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.
  • Week 2 — value test. If week 1 succeeds, add the mock client into the existing test suite and confirm it can be configured to fail (bad credentials, timeout). Confirm behaviour against a real R2 bucket using AWS_ENDPOINT. Simultaneously, one engineer builds the Flysystem-based equivalent of the action interfaces as a control.
  • Decision gate. Unless week 1 produced near-zero friction, adopt the control implementation and archive the spike.

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.

Operational Impact

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:

  • No multipart/chunked path means large uploads are bounded by PHP 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.
  • No presigned URLs means private delivery either proxies bytes through our application (bandwidth and PHP worker cost) or uses public buckets (unacceptable).
  • custom_url is a single global scalar, so multi-tenant or multi-region CDN hostnames require a fork.

Failure modes

  • CDN/object-key divergence. 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.
  • Silent test/production skew. A happy-path mock means the CI suite proves the wiring, not the storage contract. Credentials, IAM policy, bucket policy, and content-type handling are all unvalidated until the first real upload.
  • Credential handling split. Symfony secrets vs. Laravel's encrypted env / Vault / Vapor means two rotation procedures for one credential set. Rotation procedures that exist in two places get executed in one.
  • No retry/backoff policy in configuration. Transient SDK failures propagate to the caller; each call site will invent its own handling or, more likely, won't.
  • No observability hooks. No events, no logging integration into Laravel's channels. Every failure investigation starts from an application stack trace rather than a storage-layer log line.

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.

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.
thomas-0816/doctrine-dbal-duckdb
moyisoft/laravel-admin
oxygen/mod-pages
oxygen/mod-import-export
oxygen/mod-media
ksoft/laravel-contentbuilderjs
ourgoldpackages/exchange
mikkimike/laravel-exchange1c
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler
artisanbarista/laravel-shield
instance-code/repository
flooris/prestashop-models