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
You need PHP 8.5 or later with theswoole 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"
http://localhost:30900, and its console is at http://localhost:30901.
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 # Dependencies
Install dependencies
Require the framework and the modules package:composer require suvera/winter-boot suvera/winter-modules
Source files
Switch between the source files. Each tab shows the exact file from the sample.- S3SampleApplication.php
- S3DemoController.php
- bin/application.php
- composer.json
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.The runnable sample in
{
"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/"
}
}
}
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
Switch between the two config files.application.yml enables the module, and s3-config.yml defines the primary connection to MinIO.
- application.yml
- s3-config.yml
The full sample file registers See Configuration for every
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.0
modules:
- module: dev\winterframework\s3\S3Module
enabled: true
configFile: s3-config.yml
application.yml key.Defines a The
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: 3
__default__ entry supplies shared defaults. See the S3 module for every connection property.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 install
php bin/application.php
curl -X POST "http://localhost:8080/s3demo/bucket/my-test-bucket"
curl -X POST "http://localhost:8080/s3demo/object?bucket=my-test-bucket&key=test-file.txt&content=Hello%20World"
curl "http://localhost:8080/s3demo/objects?bucket=my-test-bucket"
curl "http://localhost:8080/s3demo/object?bucket=my-test-bucket&key=test-file.txt"
curl "http://localhost:8080/s3demo/demo"