S3 Sample Application
Build a REST app that performs S3 object-storage operations through an S3DemoController. You use Winter Boot for the application runtime and the Winter S3 module from Winter Modules for storage access. You test the whole flow locally with MinIO, an S3-compatible object storage server.
Prerequisites
Section titled “Prerequisites”You need PHP 8.5 or later with the swoole and pcntl extensions. You also need Docker to run MinIO.
Start MinIO before you run the app:
docker run -d -p 30900:9000 -p 30901:9001 --name minio \ -e "MINIO_ROOT_USER=admin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ quay.io/minio/minio server /data --console-address ":9001"MinIO listens on http://localhost:30900, and its console is at http://localhost:30901.
Project structure
Section titled “Project structure”The sample uses this layout:
s3/├── bin/│ └── application.php # Application entry point├── config/│ ├── application.yml # Winter Boot application config│ └── s3-config.yml # S3 connections config├── src/│ ├── S3SampleApplication.php # Main application class│ └── controller/│ └── S3DemoController.php # S3 operations controller└── composer.json # DependenciesInstall dependencies
Section titled “Install dependencies”Require the framework and the modules package:
composer require suvera/winter-boot suvera/winter-modulesSource files
Section titled “Source files”Switch between the source files. Each tab shows the exact file from the sample.
The single entry point. It points Winter Boot at the config directory and at the sample namespace.
<?php
namespace dev\example;
use dev\winterframework\stereotype\WinterBootApplication;
#[WinterBootApplication( configDirectory: [__DIR__ . "/../config"], scanNamespaces: [ ['dev\\example', __DIR__ . ''] ])]class S3SampleApplication {
public static function main(): void { $winterApp = new \dev\winterframework\core\app\WinterWebSwooleApplication(); $winterApp->run(self::class); }}The controller. It autowires S3Template and exposes one endpoint per S3 operation, plus a demo endpoint that runs them all in sequence.
<?php
namespace dev\example\controller;
use dev\winterframework\s3\S3Template;use dev\winterframework\stereotype\Autowired;use dev\winterframework\stereotype\RestController;use dev\winterframework\stereotype\web\GetMapping;use dev\winterframework\stereotype\web\PostMapping;use dev\winterframework\stereotype\web\DeleteMapping;use dev\winterframework\stereotype\web\PutMapping;use dev\winterframework\stereotype\web\PathVariable;use dev\winterframework\stereotype\web\RequestBody;use dev\winterframework\stereotype\web\RequestParam;use Exception;
#[RestController]class S3DemoController {
#[Autowired] private S3Template $s3;
/** * Create a bucket */ #[PostMapping(path: "/s3demo/bucket/{bucketName}")] public function createBucket(#[PathVariable] string $bucketName): array { try { $result = $this->s3->createBucket([ 'Bucket' => $bucketName ]);
return [ 'success' => true, 'message' => "Bucket {$bucketName} created successfully", 'data' => $result ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * Upload an object to S3 */ #[PostMapping(path: "/s3demo/object")] public function uploadObject( #[RequestParam] string $bucket, #[RequestParam] string $key, #[RequestParam] string $content ): array { try { $result = $this->s3->putObject([ 'Bucket' => $bucket, 'Key' => $key, 'Body' => $content, 'ContentType' => 'text/plain' ]);
return [ 'success' => true, 'message' => "Object {$key} uploaded successfully", 'etag' => $result['ETag'], 'versionId' => $result['VersionId'] ?? null ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * Download an object from S3 */ #[GetMapping(path: "/s3demo/object")] public function downloadObject( #[RequestParam] string $bucket, #[RequestParam] string $key ): array { try { $result = $this->s3->getObject([ 'Bucket' => $bucket, 'Key' => $key ]);
$body = $result['Body']->getContents();
return [ 'success' => true, 'key' => $key, 'content' => $body, 'contentType' => $result['ContentType'] ?? 'unknown', 'contentLength' => $result['ContentLength'] ?? 0, 'lastModified' => $result['LastModified']->format('c') ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * List objects in a bucket */ #[GetMapping(path: "/s3demo/objects")] public function listObjects( #[RequestParam] string $bucket, #[RequestParam(required: false, defaultValue: '')] ?string $prefix = null, #[RequestParam(required: false, defaultValue: '10')] ?int $maxKeys = 10 ): array { try { $params = [ 'Bucket' => $bucket, 'MaxKeys' => $maxKeys ];
if ($prefix !== null) { $params['Prefix'] = $prefix; }
$result = $this->s3->listObjectsV2($params);
$objects = []; if (isset($result['Contents'])) { foreach ($result['Contents'] as $obj) { $objects[] = [ 'key' => $obj['Key'], 'size' => $obj['Size'], 'lastModified' => $obj['LastModified']->format('c'), 'etag' => $obj['ETag'] ]; } }
return [ 'success' => true, 'bucket' => $bucket, 'prefix' => $prefix ?? '', 'maxKeys' => $maxKeys, 'isTruncated' => $result['IsTruncated'] ?? false, 'keyCount' => $result['KeyCount'] ?? 0, 'objects' => $objects ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * Get object metadata */ #[GetMapping(path: "/s3demo/object/head")] public function headObject( #[RequestParam] string $bucket, #[RequestParam] string $key ): array { try { $result = $this->s3->headObject([ 'Bucket' => $bucket, 'Key' => $key ]);
return [ 'success' => true, 'key' => $key, 'metadata' => $result['Metadata'] ?? [], 'contentType' => $result['ContentType'] ?? 'unknown', 'contentLength' => $result['ContentLength'] ?? 0, 'lastModified' => $result['LastModified']->format('c'), 'etag' => $result['ETag'] ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * Delete an object from S3 */ #[PostMapping(path: "/s3demo/object/delete")] public function deleteObject( #[RequestParam] string $bucket, #[RequestParam] string $key ): array { try { $this->s3->deleteObject([ 'Bucket' => $bucket, 'Key' => $key ]);
return [ 'success' => true, 'message' => "Object {$key} deleted successfully from bucket {$bucket}" ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * Delete a bucket from S3. */ #[PostMapping(path: "/s3demo/bucketops/delete")] public function deleteBucket( #[RequestParam] string $bucket ): array { try { $this->s3->deleteBucket(['Bucket' => $bucket]);
return [ 'success' => true, 'message' => "Bucket {$bucket} deleted successfully" ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }
/** * Demo all operations together */ #[GetMapping(path: "/s3demo/demo")] public function demoAllOperations(): array { try { $bucketName = 'demo-bucket-' . time(); $objectKey = 'demo-object.txt'; $content = 'This is a demo content for testing S3 operations.';
// 1. Create bucket $this->s3->createBucket(['Bucket' => $bucketName]);
// 2. Upload object $putResult = $this->s3->putObject([ 'Bucket' => $bucketName, 'Key' => $objectKey, 'Body' => $content ]);
// 3. Get object metadata $headResult = $this->s3->headObject([ 'Bucket' => $bucketName, 'Key' => $objectKey ]);
// 4. Download object $getResult = $this->s3->getObject([ 'Bucket' => $bucketName, 'Key' => $objectKey ]); $downloadedContent = $getResult['Body']->getContents();
// 5. List objects $listResult = $this->s3->listObjectsV2([ 'Bucket' => $bucketName ]);
// 6. Delete object $this->s3->deleteObject([ 'Bucket' => $bucketName, 'Key' => $objectKey ]);
return [ 'success' => true, 'operations' => [ 'createBucket' => "Created bucket: {$bucketName}", 'putObject' => "Uploaded object: {$objectKey}", 'headObject' => "Retrieved metadata for: {$objectKey}", 'getObject' => "Downloaded object: {$objectKey}", 'listObjectsV2' => "Found " . count($listResult['Contents'] ?? []) . " objects", 'deleteObject' => "Deleted object: {$objectKey}" ], 'bucketName' => $bucketName, 'objectKey' => $objectKey, 'contentMatch' => $content === $downloadedContent ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } }}The launch script. It loads the Composer autoloader and starts the application.
#!/usr/bin/env php<?php
use dev\example\S3SampleApplication;
require_once(dirname(__DIR__) . '/vendor/autoload.php');
S3SampleApplication::main();The sample declares framework dependencies with PSR-4 autoloading for its own namespace.
{ "name": "suvera/winter-boot-s3-sample", "require": { "ext-pcntl": "*", "ext-swoole": "*", "suvera/winter-boot": "@dev", "suvera/winter-modules": "@dev" }, "autoload": { "psr-4": { "dev\\example\\": "src/" } }}The runnable sample in winter-boot-samples adds local path repositories for winter-boot and winter-modules so it resolves them from a sibling checkout. You do not need those entries when you install released packages from Packagist.
Configuration
Section titled “Configuration”Switch between the two config files. application.yml enables the module, and s3-config.yml defines the primary connection to MinIO.
The full sample file registers S3Module and sets the server and app identity:
server: port: 8080 address: 0.0.0.0 context-path: /winter: application: name: S3 Sample Application id: s3-sample-app version: 1.0.0modules: - module: dev\winterframework\s3\S3Module enabled: true configFile: s3-config.ymlSee Configuration for every application.yml key.
Defines a primary connection pointing at MinIO with path-style endpoints:
s3: - name: __default__ version: latest region: minio use_path_style_endpoint: true force_path_style: true retries: 3
- name: primary version: latest region: minio credentials: - key: admin secret: minioadmin endpoint: http://localhost:30900 use_path_style_endpoint: true force_path_style: true retries: 3The __default__ entry supplies shared defaults. See the S3 module for every connection property.
Run the app
Section titled “Run the app”Start the app, then call the endpoints to create a bucket, upload an object, and read it back.
1. Start the application:
composer installphp bin/application.php2. Create a bucket:
curl -X POST "http://localhost:8080/s3demo/bucket/my-test-bucket"3. Upload an object:
curl -X POST "http://localhost:8080/s3demo/object?bucket=my-test-bucket&key=test-file.txt&content=Hello%20World"4. List objects:
curl "http://localhost:8080/s3demo/objects?bucket=my-test-bucket"5. Download the object:
curl "http://localhost:8080/s3demo/object?bucket=my-test-bucket&key=test-file.txt"6. Run the complete demo:
curl "http://localhost:8080/s3demo/demo"The demo creates its own bucket, runs create, upload, head, download, list, and delete in sequence, and returns one status entry per operation.