> ## Documentation Index
> Fetch the complete documentation index at: https://suvera.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# S3 Sample Application

> Build an end-to-end S3 app with Winter Boot and the Winter S3 module that uploads, downloads, lists, and deletes objects, tested locally with MinIO.

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](https://min.io/), an S3-compatible object storage server.

## 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:

```bash theme={null}
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

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:

```bash theme={null}
composer require suvera/winter-boot suvera/winter-modules
```

## Source files

Switch between the source files. Each tab shows the exact file from the sample.

<Tabs>
  <Tab title="S3SampleApplication.php">
    The single entry point. It points Winter Boot at the `config` directory and at the sample namespace.

    ```php theme={null}
    <?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);
        }
    }
    ```
  </Tab>

  <Tab title="S3DemoController.php">
    The controller. It autowires `S3Template` and exposes one endpoint per S3 operation, plus a `demo` endpoint that runs them all in sequence.

    ```php theme={null}
    <?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()
                ];
            }
        }
    }
    ```
  </Tab>

  <Tab title="bin/application.php">
    The launch script. It loads the Composer autoloader and starts the application.

    ```php theme={null}
    #!/usr/bin/env php
    <?php

    use dev\example\S3SampleApplication;

    require_once(dirname(__DIR__) . '/vendor/autoload.php');

    S3SampleApplication::main();
    ```
  </Tab>

  <Tab title="composer.json">
    The sample declares framework dependencies with PSR-4 autoloading for its own namespace.

    ```json theme={null}
    {
        "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.
  </Tab>
</Tabs>

## Configuration

Switch between the two config files. `application.yml` enables the module, and `s3-config.yml` defines the `primary` connection to MinIO.

<Tabs>
  <Tab title="application.yml">
    The full sample file registers `S3Module` and sets the server and app identity:

    ```yaml theme={null}
    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
    ```

    See [Configuration](/configuration) for every `application.yml` key.
  </Tab>

  <Tab title="s3-config.yml">
    Defines a `primary` connection pointing at MinIO with path-style endpoints:

    ```yaml theme={null}
    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
    ```

    The `__default__` entry supplies shared defaults. See the [S3 module](/modules/s3) for every connection property.
  </Tab>
</Tabs>

## 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:**

```bash theme={null}
composer install
php bin/application.php
```

**2. Create a bucket:**

```bash theme={null}
curl -X POST "http://localhost:8080/s3demo/bucket/my-test-bucket"
```

**3. Upload an object:**

```bash theme={null}
curl -X POST "http://localhost:8080/s3demo/object?bucket=my-test-bucket&key=test-file.txt&content=Hello%20World"
```

**4. List objects:**

```bash theme={null}
curl "http://localhost:8080/s3demo/objects?bucket=my-test-bucket"
```

**5. Download the object:**

```bash theme={null}
curl "http://localhost:8080/s3demo/object?bucket=my-test-bucket&key=test-file.txt"
```

**6. Run the complete demo:**

```bash theme={null}
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.

## Next steps

* Read the [S3 module](/modules/s3) for the full `S3Template` API and IAM-role setup.
* Browse all [Libraries](/modules/overview) when you need Kafka, SQS, Redis, or Doctrine in the same app.
