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.
## Getting Started
### Minimal Setup for Laravel
1. **Install the package** via Composer:
```bash
composer require 1tomany/storage-bundle
For S3/R2 support:
composer require aws/aws-sdk-php-symfony
Configure the bundle in config/packages/onetomany_storage.yaml:
onetomany_storage:
client: "amazon" # or "mock" for testing
bucket: "your-bucket-name"
custom_url: "https://your-cdn.com" # Optional CDN override
Set AWS credentials in .env (use Symfony secrets for production):
AWS_KEY=%env(AWS_KEY)% # Encrypted via `php bin/console secrets:generate`
AWS_SECRET=%env(AWS_SECRET)%
AWS_ENDPOINT="https://your-account.r2.cloudflarestorage.com" # For R2
AWS_MERGE_CONFIG=true
First use case: Upload a file in a Laravel controller/service:
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
use OneToMany\StorageBundle\Request\UploadRequest;
class FileUploader {
public function __construct(private UploadActionInterface $uploadAction) {}
public function upload(string $localPath, string $storageKey) {
$response = $this->uploadAction->act(
new UploadRequest($localPath, 'image/png', $storageKey)
);
return $response->getUrl(); // Returns CDN URL if configured
}
}
Dependency Injection:
UploadActionInterface, DownloadActionInterface) over the base ClientInterface for clarity and testability.final readonly class FileService {
public function __construct(
private UploadActionInterface $upload,
private DownloadActionInterface $download
) {}
public function handleFileUpload(UploadDto $dto) {
$response = $this->upload->act(
new UploadRequest($dto->path, $dto->mimeType, $dto->storageKey)
);
return new FileUploaded($response->getUrl());
}
}
Request/Response Pattern:
UploadRequest, DownloadRequest) to encapsulate parameters.UploadResponse::isSuccess()) before processing.Testing:
UploadActionInterface mock) instead of the full client.mock client in onetomany_storage.yaml for integration tests:
onetomany_storage:
client: "mock"
bucket: "test-bucket"
Laravel Filesystem: Bridge with Laravel’s Storage facade by extending the bundle’s client:
use Illuminate\Support\Facades\Storage;
use OneToMany\StorageBundle\Contract\Client\ClientInterface;
class LaravelStorageAdapter implements ClientInterface {
public function act(RequestInterface $request) {
$contents = file_get_contents($request->getPath());
Storage::disk('s3')->put($request->getKey(), $contents);
return new UploadResponse(true, Storage::disk('s3')->url($request->getKey()));
}
}
Register it in config/packages/onetomany_storage.yaml:
onetomany_storage:
client: "laravel_adapter"
Validation: Use Symfony’s Validator to validate UploadRequest/DownloadRequest before processing:
use Symfony\Component\Validator\Validator\ValidatorInterface;
$errors = $validator->validate($request);
if (count($errors) > 0) {
throw new \InvalidArgumentException((string) $errors);
}
Async Uploads: Dispatch a queue job for large files:
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class UploadFileJob implements ShouldQueue {
use Queueable, SerializesModels;
public function __construct(
private UploadActionInterface $uploadAction,
private UploadRequest $request
) {}
public function handle() {
$this->uploadAction->act($this->request);
}
}
AWS SDK Configuration:
AWS_MERGE_CONFIG=true causes Symfony to ignore .env overrides..env and clear cache:
php bin/console cache:clear
Custom URL Overrides:
custom_url in config is ignored if the storage service returns a public URL (e.g., S3 bucket with static website hosting).custom_url to null or omit it to use the service’s canonical URL.Mock Client Limitations:
use OneToMany\StorageBundle\Client\MockClient;
class CustomMockClient extends MockClient {
public function act(RequestInterface $request) {
if ($request->getKey() === 'fail-test') {
throw new \RuntimeException('Simulated failure');
}
return parent::act($request);
}
}
Register it in services.yaml:
services:
OneToMany\StorageBundle\Contract\Client\ClientInterface:
class: App\Client\CustomMockClient
tags: ['onetomany.storage.client', { key: 'mock' }]
File Permissions:
UploadRequest:
$request = new UploadRequest($path, $mimeType, $key);
$request->setAcl('public-read'); // For S3
Enable AWS SDK Debugging:
Add to config/packages/aws.yaml:
aws:
debug: true
Check logs for SDK requests/responses.
Validate Requests:
Use Symfony’s Validator to catch malformed requests early:
$validator = $container->get(ValidatorInterface::class);
$errors = $validator->validate($request);
Check Response Codes:
Inspect UploadResponse/DownloadResponse for HTTP status codes:
if (!$response->isSuccess()) {
throw new \RuntimeException(
'Upload failed: ' . $response->getStatusCode()
);
}
Custom Clients:
Implement ClientInterface and tag it for DI:
use OneToMany\StorageBundle\Contract\Client\ClientInterface;
class BackblazeClient implements ClientInterface {
public function act(RequestInterface $request) {
// Custom Backblaze logic
}
}
Register in services.yaml:
services:
App\Client\BackblazeClient:
tags: ['onetomany.storage.client', { key: 'backblaze' }]
Middleware: Add pre/post-processing to actions by decorating the interface:
use OneToMany\StorageBundle\Contract\Action\UploadActionInterface;
class LoggingUploadAction implements UploadActionInterface {
public function __construct(private UploadActionInterface $decorated) {}
public function act(RequestInterface $request) {
\Log::info('Uploading file', ['key' => $request->getKey()]);
$response = $this->decorated->act($request);
\Log::info('Upload complete', ['url' => $response->getUrl()]);
return $response;
}
}
Register in services.yaml:
services:
OneToMany\StorageBundle\Contract\Action\UploadActionInterface:
class: App\Action\LoggingUploadAction
decorates: 'onetomany_storage.upload_action'
Event Dispatching:
Trigger events before/after actions using Symfony’s EventDispatcher:
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class UploadEventDispatcher {
public function __construct(
private EventDispatcherInterface $dispatcher,
private UploadActionInterface $uploadAction
) {}
public function uploadWithEvents(RequestInterface $request) {
$this->dispatcher->dispatch(new PreUploadEvent($request));
$response = $this->uploadAction->act($request);
$this->dispatcher->dispatch(new PostUploadEvent($response));
return $response;
}
How can I help you explore Laravel packages today?