Installation:
composer require cesurapp/storage-bundle
Note: Requires Symfony 8+ and PHP 8.4+. For Laravel, use a wrapper service provider (see Implementation Patterns).
Configuration:
Create config/storage.yaml (or adapt to Laravel’s config/filesystems.php):
storage:
default: main
devices:
local:
driver: local
root: "%kernel.project_dir%/storage/files"
main:
driver: cloudflare
root: /
accessKey: "%env(STORAGE_CLOUDFLARE_KEY)%"
secretKey: "%env(STORAGE_CLOUDFLARE_SECRET)%"
bucket: "%env(STORAGE_BUCKET)%"
endPoint: "%env(STORAGE_ENDPOINT)%"
First Use Case:
Inject the Storage service into a controller or command:
use Cesurapp\StorageBundle\Storage\Storage;
class UploadController {
public function __construct(private Storage $storage) {}
public function handle() {
$this->storage->upload(
'/tmp/image.jpg',
'uploads/user123/image.jpg'
);
$url = $this->storage->getUrl('uploads/user123/image.jpg');
return response()->json(['url' => $url]);
}
}
Laravel Adaptation:
Create a service provider to bridge Symfony’s Storage to Laravel’s FilesystemManager:
// app/Providers/StorageBundleServiceProvider.php
public function register() {
$this->app->singleton('storage', function ($app) {
$config = $app['config']['storage'];
return new \Cesurapp\StorageBundle\Storage\StorageManager($config);
});
}
public function boot() {
$this->app->extend('filesystem.disk', function ($disk, $app) {
$storage = $app->make('storage');
return new StorageBundleAdapter($storage->device($disk));
});
}
Use the device() method to switch contexts dynamically:
// Upload to Cloudflare R2 (default)
$storage->upload('/tmp/backup.zip', 'backups/2023.zip');
// Upload to BackBlaze B2
$storage->device('backblaze')->upload(
'/tmp/backup.zip',
'backups/2023-b2.zip'
);
Leverage async operations and metadata for cloud providers:
// Upload with custom metadata (Cloudflare R2/BackBlaze)
$storage->upload(
'/tmp/photo.jpg',
'users/123/photo.jpg',
[
'ContentType' => 'image/jpeg',
'Metadata' => ['user_id' => '123', 'tags' => 'profile'],
]
);
// Generate pre-signed URL (Cloudflare R2)
$client = $storage->device('main')->getClient();
$url = $client->getPresignedUrl(
$storage->device('main')->getBucket(),
'users/123/photo.jpg',
new \DateTimeImmutable('+1 hour')
);
Use local driver methods for filesystem tasks:
$local = $storage->device('local');
$local->move('temp/file.pdf', 'documents/file.pdf');
$size = $local->getDirectorySize('users/123/');
Handle large files efficiently:
// Stream download (cloud)
foreach ($storage->downloadChunk('large-file.zip') as $chunk) {
yield $chunk;
}
// Stream upload (Laravel HTTP request)
$request->file('file')->storeAs(
'uploads',
'large-file.zip',
'custom' // Use the storage bundle disk
);
Filesystem Disk Integration:
Extend Laravel’s FilesystemAdapter to wrap the bundle’s Storage:
class StorageBundleAdapter implements FilesystemAdapter {
public function __construct(private Storage $storage) {}
public function writeStream($path, $contents) {
$this->storage->writeStream($contents, $path);
}
public function read($path) {
return $this->storage->download($path);
}
// Implement remaining FilesystemAdapter methods...
}
Configuration Sync:
Convert storage.yaml to Laravel’s filesystems.php:
// config/filesystems.php
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'cloudflare' => [
'driver' => 'custom', // Alias for StorageBundleAdapter
'storage' => 'main', // Maps to storage.yaml's 'main' device
],
],
Service Provider Binding:
Bind the bundle’s Storage to Laravel’s container:
public function register() {
$this->app->singleton('storage', function ($app) {
$config = $app['config']['storage'];
return new \Cesurapp\StorageBundle\Storage\StorageManager(
$config['devices'],
$config['default']
);
});
}
Async Uploads/Downloads: Use the bundle’s async S3 client for background jobs:
// In a queue job
$storage->device('main')->getClient()->upload(
new UploadFile('/tmp/large-video.mp4'),
'videos/large-video.mp4',
['ContentType' => 'video/mp4']
);
Caching URLs: Cache pre-signed URLs (e.g., with Laravel’s cache):
$url = cache()->remember("presigned_url:{$path}", now()->addHour(), function () use ($storage, $path) {
return $storage->device('main')->getClient()->getPresignedUrl(...);
});
Local Driver Testing:
Use Laravel’s Storage facade for local operations in tests:
public function test_local_upload() {
Storage::fake('local');
Storage::disk('local')->put('test.txt', 'Hello');
Storage::assertExists('test.txt');
}
Cloud Driver Mocking:
Mock the Storage service for cloud operations:
$storage = Mockery::mock(\Cesurapp\StorageBundle\Storage\Storage::class);
$storage->shouldReceive('upload')->once();
$this->app->instance('storage', $storage);
Environment-Based Testing:
Use Laravel’s .env.testing to switch providers:
STORAGE_DEFAULT=local
STORAGE_CLOUDFLARE_KEY=test_key
Symfony vs. Laravel DI:
ContainerInterface. Laravel’s container requires explicit binding.StorageManager (see Implementation Patterns).PHP 8.4 Features:
BackBlaze::US_WEST_001). Laravel’s PHP 8.2+ support may cause deprecation warnings.Async SDK Conflicts:
async-aws/s3, which may conflict with Laravel’s aws/aws-sdk-php.Local Driver Path Handling:
root. Incorrect paths (e.g., absolute paths) may cause silent failures.FilesystemAdapter wrapper:
$path = Str::replaceFirst($root, '', $path);
Pre-Signed URL Expiry:
Metadata Limitations:
if (!$storage->device()->supportsMetadata()) {
$metadata = null;
}
How can I help you explore Laravel packages today?