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

Getting Started

First, the reality check. 1tomany/storage-bundle is a Symfony bundle, not a Laravel package. composer require 1tomany/storage-bundle will install the classes, but nothing will auto-register: there is no StorageServiceProvider, no facade, no php artisan vendor:publish, and no config/ merge. All of the wiring the bundle gets for free from Symfony's Flex/compiler-pass system you will be doing by hand in a service provider.

Where to look first. Ignore the README's YAML and look at the interfaces — that is the entire usable API surface:

  • OneToMany\StorageBundle\Contract\Action\UploadActionInterface
  • OneToMany\StorageBundle\Contract\Action\DownloadActionInterface
  • OneToMany\StorageBundle\Contract\Action\DeleteActionInterface
  • OneToMany\StorageBundle\Contract\Client\ClientInterface
  • OneToMany\StorageBundle\Request\UploadRequest and Response\UploadResponse

The bundle's own docs are explicit that you should inject the action interfaces, not ClientInterface. The action layer exists so that pre/post-processing (validation, key normalisation, response shaping) lives in one place instead of being duplicated in each client. That is the one genuinely portable idea here, and it survives translation into Laravel cleanly.

Translate the config, don't ship the YAML. The config/packages/onetomany_storage.yaml file is a Symfony config dump — Laravel will never read it. Move the values into config/services.php (or a dedicated config/storage_bundle.php) and read them with config('services.storage.client') etc. Use Laravel's own .env + secrets handling for AWS_KEY/AWS_SECRET; the bundle's advice about Symfony secrets does not apply.

First use case. The canonical day-one job is "accept an upload, push it to S3 (or R2), return a CDN-friendly URL." Concretely:

  1. Install aws/aws-sdk-php (the plain SDK — not aws/aws-sdk-php-symfony, see Gotchas).
  2. Build an Aws\S3\S3Client from your config/services.php credentials.
  3. Construct the bundle's Amazon client against that S3 client.
  4. Bind UploadActionInterface to a singleton that wraps it.
  5. In a controller or job: $this->uploadAction->act(new UploadRequest($path, $format, $key)), then read the URL off the UploadResponse.

Implementation Patterns

Bind the three action interfaces in a single provider. A small StorageServiceProvider is all the integration you need:

$this->app->singleton(S3Client::class, fn () => new S3Client([
    'version' => 'latest',
    'region'  => config('services.storage.region'),
    'endpoint'=> config('services.storage.endpoint'), // R2 / MinIO
    'credentials' => [
        'key'    => config('services.storage.key'),
        'secret' => config('services.storage.secret'),
    ],
]));

$this->app->singleton(UploadActionInterface::class, UploadAction::class);
$this->app->singleton(DownloadActionInterface::class, DownloadAction::class);
$this->app->singleton(DeleteActionInterface::class, DeleteAction::class);
$this->app->singleton(ClientInterface::class, fn ($app) =>
    new AmazonClient($app->make(S3Client::class), config('services.storage.bucket'))
);

Wrap the action in an application-level service, not the controller. The action classes are thin. Put your business rules — key naming (users/{id}/files/{uuid}.{ext}), MIME/extension decisions, CDN host selection — in an App\Files\Storage class that depends on UploadActionInterface. The controller talks to your class. This is what makes it survivable if you later drop the bundle and go back to Storage::disk().

Bridge UploadedFile to UploadRequest. Laravel hands you Illuminate\Http\UploadedFile; the bundle wants a path and a format. Store the upload to a temp path first ($file->storeAs('tmp', ...) or getRealPath()), then build the request. Don't try to make the bundle's request objects carry Laravel's file object — that coupling will bite you.

Use the action interfaces for queueing. Uploads are I/O-bound and belong in a job. A ProcessUploadJob that injects UploadActionInterface gives you Laravel's retry semantics ($tries, backoff, retryUntil) for free. This is the single biggest day-to-day win of the action pattern — you never pass an SDK client through serialization.

Swap in the mock client in tests by rebinding, not by config. The bundle's mock_client is selected by a config key at container boot. In Laravel you get the same effect more predictably by rebinding in your test case:

$this->app->bind(UploadActionInterface::class, MockUploadAction::class);

Then assert on the UploadResponse your service returns. This gives you Storage::fake()-style confidence without pretending the bundle has a test harness (it doesn't).

Custom providers go through ClientInterface, not a Symfony tag. The README says to tag your client with onetomany.storage.client and a key. In Laravel that machinery doesn't exist — just $this->app->singleton(ClientInterface::class, YourClient::class) and pick your implementation in the service provider based on a config value. The compiler-pass indirection adds nothing you can't do with when() or a match expression.

Delete and download are fire-and-forget — treat the responses as best-effort. DeleteActionInterface was only added in v3.0.0, so the surface is young. Wrap deletes in a job, log failures, and design for idempotency (deleting a key that's already gone should not be an error in your application).

Gotchas and Tips

Do not install aws/aws-sdk-php-symfony. The README tells you to, but that package is a Symfony bundle. In Laravel it will fight with your existing aws/aws-sdk-php dependency and you'll end up with two S3 configurations. Use the plain SDK and instantiate S3Client yourself.

Two sources of truth for S3. If you're also using Laravel's s3 disk for other work, you now have config/filesystems.php and your bundle config pointing at the same bucket with different credentials rotation. Pick one and disable the other for the same bucket.

custom_url is not signing. It's a static prefix substitution (https://files.app-cdn.com/{key}) — useful for a public CloudFront or R2 public domain, useless for private objects. There is no temporaryUrl() equivalent anywhere in the bundle. For presigned URLs you'll be back in S3Client::createPresignedRequest(...) directly.

format on UploadRequest needs verification. The README example passes "format" as a string alongside path and key without defining whether it's a MIME type or an extension. Check the class before you build a convention on top of it — this is exactly the kind of ambiguity a 0-star, 0-dependent package leaves unresolved.

Version and PHP constraints are a real hazard. v3.0.1 "Upgrades to Symfony 8.0" and v3.0.0 migrated contracts. Laravel 10/11 still pin Symfony 6/7 components. Run composer why-not 1tomany/storage-bundle before committing to this; the dependency graph is the most likely reason to abandon the install.

Zero stars, zero dependents, single maintainer. Treat it accordingly: pin to exact versions, don't rely on it for anything security-sensitive, and consider vendoring the four or five classes you actually use. The ActionInterface pattern is ~50 lines; you can reimplement it in an afternoon and own the maintenance.

The mock client is a config switch, not a test double. It cannot assert on uploaded content or key correctness. Don't mistake it for Storage::fake() — you still need your own assertions.

No logging, no retries, no timeouts in the action layer. Everything you'd expect from a production storage adapter is on you: log at the boundary, wrap in queued jobs with retries, configure the S3Client with retries and http timeouts explicitly.

Watch the transitive churn. Earlier releases show repeated fixes to SmartFile / 1tomany/data-uri interplay (v2.1.5, v2.1.6). If you find yourself on v2.x, expect surprises from that dependency. On v3.x stay close to the latest patch.

Extension point worth noting. If you need multi-cloud (R2 + Backblaze + GCS behind one interface), implementing ClientInterface per provider and selecting it by config is the bundle's intended design and it works reasonably well if you've already committed to the action pattern. If you haven't, Laravel's Storage disks with a custom driver is the lower-friction path and the one I'd default to.

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