1tomany/storage-bundle
Symfony bundle for uploading files to remote storage (Amazon S3/R2, GCS, Azure) with a simple client-based config. Includes an Amazon S3-compatible client plus a mock client for fast, offline testing, and optional custom URLs for CDN/public buckets.
Install the Bundle:
composer require 1tomany/storage-bundle aws/aws-sdk-php-symfony
For Laravel, manually create config/packages/onetomany_storage.yaml (Symfony’s autoloader will handle the rest).
Configure .env:
# AWS S3/R2
AWS_KEY=your_key
AWS_SECRET=your_secret
AWS_ENDPOINT=https://your-account.r2.cloudflarestorage.com # For R2
AWS_MERGE_CONFIG=true
AWS_REGION=auto
Bind Services to Laravel Container (in AppServiceProvider):
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
use OneToMany\StorageBundle\Contract\Client\ClientInterface;
public function register()
{
$this->app->bind(ClientInterface::class, function ($app) {
return $app->make('onetomany.storage.client');
});
$this->app->bind(UploadActionInterface::class, function ($app) {
return $app->make('onetomany.storage.upload_action');
});
}
First Upload:
use OneToMany\StorageBundle\Request\UploadRequest;
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
public function uploadAvatar(UploadActionInterface $uploadAction, $filePath, $userId)
{
$response = $uploadAction->act(
new UploadRequest(
$filePath,
'png', // Format (e.g., 'jpg', 'pdf')
"users/{$userId}/avatar.png" // S3 key
)
);
return $response->getUrl(); // Returns custom URL if configured
}
Use the action interfaces (UploadActionInterface, DownloadActionInterface, DeleteActionInterface) to encapsulate storage logic. Example:
// In a Laravel job or service
public function handle(UploadedFile $file, string $storageKey)
{
$uploadAction = app(UploadActionInterface::class);
$response = $uploadAction->act(
UploadRequest::fromFile($file, $storageKey)
);
// Process response (e.g., save URL to DB)
}
Extend Laravel’s Filesystem to delegate to the bundle:
use Illuminate\Contracts\Filesystem\Filesystem;
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
class StorageFilesystem implements Filesystem
{
public function __construct(private UploadActionInterface $uploadAction) {}
public function put($path, $contents, $options = [])
{
$response = $this->uploadAction->act(
new UploadRequest(
$path,
pathinfo($path, PATHINFO_EXTENSION),
$path // Use $path as S3 key
)
);
return $response->getUrl();
}
// Implement other Filesystem methods...
}
Use the mock client in phpunit.xml:
config/packages/onetomany_storage.yaml:
onetomany_storage:
client: "mock"
Test uploads without network calls:
public function testUpload()
{
$uploadAction = $this->app->make(UploadActionInterface::class);
$response = $uploadAction->act(
new UploadRequest(__DIR__.'/test.png', 'png', 'test.png')
);
$this->assertEquals('https://mock.app-cdn.com/test.png', $response->getUrl());
}
Configure custom_url in onetomany_storage.yaml:
onetomany_storage:
custom_url: "https://cdn.yourdomain.com"
Now all file URLs will use your CDN:
$response = $uploadAction->act(...);
echo $response->getUrl(); // Outputs: https://cdn.yourdomain.com/users/1/avatar.png
Wrap actions in try-catch:
try {
$response = $uploadAction->act($request);
} catch (StorageException $e) {
Log::error("Storage upload failed: {$e->getMessage()}");
throw new \RuntimeException("Failed to upload file", 0, $e);
}
AWS SDK Configuration:
AWS_MERGE_CONFIG=true in Laravel’s .env can cause credential conflicts.aws.yaml is merged correctly (Symfony’s default behavior).Mock Client Limitations:
putObject may not immediately return the same URL).Laravel Service Binding:
onetomany.storage.client) aren’t auto-discovered in Laravel.AppServiceProvider (as shown in Getting Started).File Format Detection:
UploadRequest expects a format (e.g., 'png'), but Laravel’s UploadedFile uses getClientOriginalExtension().$format = strtolower($file->getClientOriginalExtension());
Custom URL Overrides:
custom_url is set but the storage service returns a pre-signed URL, the bundle may ignore custom_url.Enable AWS SDK Debugging:
Add to .env:
AWS_DEBUG=true
Logs will appear in storage/logs/aws.log.
Validate S3 Keys:
Use the mock client to test keys before deploying:
$uploadAction->act(new UploadRequest(__DIR__.'/test.txt', 'txt', 'test.txt'));
If the mock fails, the real client will too.
Check Bucket Permissions: Ensure your IAM role has:
s3:PutObjects3:GetObjects3:DeleteObject
For R2, verify Cloudflare API tokens have the correct permissions.Add a New Storage Provider:
Implement ClientInterface and tag it:
// src/Storage/GoogleClient.php
use OneToMany\StorageBundle\Contract\Client\ClientInterface;
class GoogleClient implements ClientInterface
{
// Implement methods...
}
Register in services.yaml:
services:
App\Storage\GoogleClient:
tags:
- { name: onetomany.storage.client, key: "google" }
Pre/Post-Processing: Extend an action class:
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
use OneToMany\StorageBundle\Request\UploadRequest;
use OneToMany\StorageBundle\Response\UploadResponse;
class LoggingUploadAction implements UploadActionInterface
{
public function __construct(private UploadActionInterface $decorated) {}
public function act(UploadRequest $request): UploadResponse
{
Log::info("Uploading {$request->getKey()}");
$response = $this->decorated->act($request);
Log::info("Uploaded to {$response->getUrl()}");
return $response;
}
}
Bind it in AppServiceProvider:
$this->app->bind(UploadActionInterface::class, function ($app) {
return new LoggingUploadAction($app->make(UploadActionInterface::class));
});
Laravel Vapor Integration:
Override the Storage facade to use the bundle:
// config/app.php
'aliases' => [
'Storage' => App\Storage\VaporStorage::class,
],
// app/Storage/VaporStorage.php
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
class VaporStorage extends \Illuminate\Filesystem\Filesystem
{
public function __construct(UploadActionInterface $uploadAction) {}
// Delegate methods to $uploadAction...
}
Parallel Uploads:
Use Laravel’s Bus to queue uploads:
UploadFileJob::dispatch($file, $storageKey)->onQueue('uploads');
Process jobs in parallel with supervisor.
Cache File URLs: Store `UploadResponse
How can I help you explore Laravel packages today?