Weave Code
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

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup for Laravel

  1. 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).

  2. 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
    
  3. 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');
        });
    }
    
  4. 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
    }
    

Implementation Patterns

1. Action-Based Workflows

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)
}

2. Integration with Laravel Filesystem

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...
}

3. Mocking for Tests

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());
}

4. Custom URL Handling

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

5. Error Handling

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);
}

Gotchas and Tips

Pitfalls

  1. AWS SDK Configuration:

    • Gotcha: Forgetting AWS_MERGE_CONFIG=true in Laravel’s .env can cause credential conflicts.
    • Fix: Ensure aws.yaml is merged correctly (Symfony’s default behavior).
  2. Mock Client Limitations:

    • Gotcha: The mock client doesn’t simulate S3’s eventual consistency (e.g., putObject may not immediately return the same URL).
    • Fix: Use it only for unit tests; integrate a real client for integration tests.
  3. Laravel Service Binding:

    • Gotcha: The bundle’s Symfony services (onetomany.storage.client) aren’t auto-discovered in Laravel.
    • Fix: Manually bind them in AppServiceProvider (as shown in Getting Started).
  4. File Format Detection:

    • Gotcha: The UploadRequest expects a format (e.g., 'png'), but Laravel’s UploadedFile uses getClientOriginalExtension().
    • Fix: Convert the extension to lowercase:
      $format = strtolower($file->getClientOriginalExtension());
      
  5. Custom URL Overrides:

    • Gotcha: If custom_url is set but the storage service returns a pre-signed URL, the bundle may ignore custom_url.
    • Fix: Ensure your storage client (e.g., S3) isn’t generating pre-signed URLs in the response.

Debugging Tips

  1. Enable AWS SDK Debugging: Add to .env:

    AWS_DEBUG=true
    

    Logs will appear in storage/logs/aws.log.

  2. 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.

  3. Check Bucket Permissions: Ensure your IAM role has:

    • s3:PutObject
    • s3:GetObject
    • s3:DeleteObject For R2, verify Cloudflare API tokens have the correct permissions.

Extension Points

  1. 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" }
    
  2. 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));
    });
    
  3. 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...
    }
    

Performance Tips

  1. Parallel Uploads: Use Laravel’s Bus to queue uploads:

    UploadFileJob::dispatch($file, $storageKey)->onQueue('uploads');
    

    Process jobs in parallel with supervisor.

  2. Cache File URLs: Store `UploadResponse

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.
calmfox/watch-sylius
damienfern/grpc-symfony-bundle
atoolo/index-bundle
atoolo/genai-bundle
coprotoai/laravel-ticket
davidjln/llm-carbon-bundle
cryonighter/valid-request-bundle
coolms/taxonomy-bundle
coolms/field-bundle
articulate-orm/symfony
aaix/laravel-tall-architect
ephoto/akeneo-connector
emmanuelballery/eb-plantumlbundle
emielburgman/symfony-visitor-beacon
emielburgman/symfony-visit-storage
emielburgman/symfony-security-headers
emielburgman/symfony-log-viewer
emarref/xdebug-bundle
emarref/pubnub-bundle
elriseio/finance-money-bundle