reconnect/s3bundle
Reconnect S3Bundle provides a streamlined PHP integration for Amazon S3, making bucket setup, uploads, downloads, and storage operations easier with minimal configuration. It helps Laravel/PHP applications manage S3-backed files without repetitive boilerplate code.
1. Install and pin to v3
composer require reconnect/s3bundle:^3.0
v3.0.0 is explicitly flagged as BC-breaking: the bundle class and the root configuration key were both renamed. Pin to ^3.0 (or an exact 3.0.x) rather than a floating constraint — the package has a single recent release and no dependents, so there's no crowd-sourced signal that a 4.0 would be painless.
2. Register the bundle (new class name)
// config/bundles.php
return [
// ...
Reconnect\S3Bundle\ReconnectS3Bundle::class => ['all' => true],
];
v2 registered Reconnect\S3Bundle\S3Bundle; v3 uses ReconnectS3Bundle. Watch the casing — class names in PHP are case-insensitive at runtime but composer's autoloader maps them case-sensitively on case-sensitive filesystems, so a copy-pasted ReConnectS3Bundle from older docs/READMEs will fail confusingly on Linux CI while working on macOS.
3. Discover the real config keys before guessing
The README is thin, so let the console tell you the truth:
php bin/console config:dump-reference reconnect_s3
The root key is reconnect_s3 (v2 used reconnect_s3_bundle). The release notes' own example uses bucketHost as the primary option, which is a good hint that the bundle's config shape is host-oriented rather than the bucket/region/key/secret shape you might assume from the AWS SDK:
# config/packages/reconnect_s3.yaml
reconnect_s3:
bucketHost: '%env(BUCKET_HOST)%'
# ...remaining keys exactly as revealed by the dump
Add the matching env entries to .env and never commit real credentials.
4. Upgrading an existing app from v2 — three renames
This is the bulk of the v3 work and it is entirely mechanical:
Reconnect\S3Bundle\S3Bundle::class → Reconnect\S3Bundle\ReconnectS3Bundle::class in config/bundles.phpreconnect_s3_bundle: → reconnect_s3: in the YAMLconfig/packages/reconnect_s3_bundle.yaml → config/packages/reconnect_s3.yamlOr let Flex do it, which is the documented path:
symfony console recipes:update reconnect/s3bundle
Then grep -rn "reconnect_s3_bundle\|S3Bundle::class" config/ src/ to catch stragglers (test fixtures, config/packages/*/ env overrides, infra repos that template the YAML).
5. Verify what got registered
php bin/console debug:container | grep -i s3
php bin/console debug:autowiring s3
That gives you the service ID (usually the class name, or an alias) to type-hint in a constructor.
6. First use case: store an uploaded file
The day-to-day entry point in Symfony is a controller pushing an UploadedFile at the configured client:
use Symfony\Component\HttpFoundation\File\UploadedFile;
public function store(UploadedFile $file, SomeS3Client $client): Response
{
$key = 'uploads/' . uniqid('', true) . '.' . $file->guessExtension();
$client->putObject([
'Bucket' => $this->getParameter('app.s3_bucket'),
'Key' => $key,
'Body' => fopen($file->getPathname(), 'rb'),
'ContentType' => $file->getMimeType(),
]);
return $this->json(['key' => $key]);
}
If the bundle exposes its own facade service rather than a raw Aws\S3\S3Client, inject that instead — using the configured service is the entire reason to take the dependency.
Inject the configured service; never build clients by hand. The bundle's value is centralized configuration plus a pre-wired client. In a controller:
public function list(S3Client $s3): JsonResponse
{
$result = $s3->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => 'uploads/',
]);
return $this->json(array_column($result['Contents'] ?? [], 'Key'));
}
In a service, constructor injection:
final class FileUploader
{
public function __construct(private S3Client $s3, private string $bucket) {}
}
Bind the bucket in services.yaml:
services:
App\Service\FileUploader:
arguments:
$bucket: '%env(BUCKET_HOST)%'
Presigned URLs are the highest-leverage day-to-day pattern. They offload bandwidth from PHP entirely — the browser or mobile client talks to S3 directly:
$cmd = $this->s3->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $key,
]);
$request = $this->s3->createPresignedRequest($cmd, '+15 minutes');
return (string) $request->getUri();
For direct uploads, use PutObject with the same pattern and a ContentType constraint in Params so callers can't upload arbitrary MIME types.
Async work with Messenger. Large transfers should happen in a worker that receives the object key plus metadata, not inside the HTTP request. This decouples upload latency from the user's response and gives you retries for transient S3 errors — which v3's rename does not help with, but which matters more than the bundle choice.
Local development. Point the client at MinIO or LocalStack via an env-specific override:
# config/packages/dev/reconnect_s3.yaml
reconnect_s3:
# keys as revealed by config:dump-reference — e.g. a custom endpoint / path-style flag
If the dump doesn't expose an endpoint key, decorate the client service in dev rather than forking.
Rolling the v3 upgrade across an estate. Do the rename in one commit per repo, config file renamed with git mv so history follows it. Don't try to run a mixed fleet: v2's reconnect_s3_bundle key and v3's reconnect_s3 key are distinct roots, so a shared config template deployed to both is guaranteed to break one of them (Symfony fails container compilation on an unrecognized root key — a loud, safe failure, but a deploy blocker).
Bridging into Laravel. This remains a Symfony bundle and does not auto-register in Laravel. The v3 rename is a non-event for Laravel apps because you shouldn't be loading it there at all: register a service provider that builds the client with your Laravel config, or — usually better — use league/flysystem-aws-s3-v3 directly and skip the DI bridging entirely.
Testing. Use a fake S3Client for unit tests, and LocalStack in CI for round-trip assertions on put/get/presign. After the v3 upgrade, add one test that boots the kernel so a stale reconnect_s3_bundle.yaml or old bundle class fails in CI instead of at deploy.
S3Bundle → ReconnectS3Bundle) and config root (reconnect_s3_bundle → reconnect_s3). The recipe name for recipes:update is still reconnect/s3bundle — the package name did not change, only the class and config key.reconnect_s3.yaml and leaves reconnect_s3_bundle.yaml behind. Keeping both means the old file still declares the removed root key and container compilation fails with "There is no extension able to load the configuration for reconnect_s3_bundle".php bin/console cache:clear. A stale compiled container referencing the old service class produces autowiring errors that look like the package is broken.Reconnect\S3Bundle\ReconnectS3Bundle. Older READMEs and blog posts render it ReConnect\S3Bundle\ReConnectS3Bundle; that breaks the composer autoloader on case-sensitive filesystems and on composer dump-autoload -o.bucketHost ≠ bucket. The release notes' config example uses bucketHost, which is not an AWS SDK S3Client option name in the same way. Confirm the exact key set with config:dump-reference reconnect_s3 before copying any snippet, including this one.AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION automatically. The bundle's own keys (per the notes, BUCKET_HOST) are separate — don't assume the bundle reads the AWS-standard names.recipes:update requires Symfony Flex and a clean working tree. Commit or stash first; it modifies config/bundles.php and writes a new YAML file, and you want that diff reviewable in isolation.s3:ListBucket is a bucket-level permission while s3:GetObject / s3:PutObject are object-level — granting only the object actions yields a working putObject and a mysteriously failing listing.ContentType (via CLI or a handler that forgets it) leaves objects as application/octet-stream, which browsers download instead of display.fopen($path, 'rb') for Body; file_get_contents on a multi-GB upload OOMs the worker.listObjectsV2 returns at most 1000 keys — use the paginator:$paginator = $this->s3->getPaginator('ListObjectsV2', ['Bucket' => $this->bucket]);
foreach ($paginator as $page) { /* ... */ }
php bin/console debug:autowiring s3, debug:container --env-vars, and debug:container --tag=container.service_subscriber are far faster than grepping compiled container files.services:
App\S3\LoggingS3Client:
decorates: <confirm via debug:container>
arguments: ['@.inner', '@logger']
services.yaml (~20 lines). The bundle's real value is config validation and a shared convention — weigh that against the churn of a major rename in a package with one release and zero dependents.How can I help you explore Laravel packages today?