PHP Slim 環境構築(8) S3

# PHP Slim 環境構築(8) S3


## Introduction

[前回](https://qiita.com/dynamitecoolguy/items/bfedcb739dfdb47138c9)は、ローカルDynamoDBにアクセスしてみました。
今回は、ローカル用のS3互換サービスMinIOを使って、S3へのアクセスを行います。


## MinIO

[MinIO](https://min.io/)は、エンタープライズレベルのオブジェクトストレージサービスです。
・・・なんですが、手軽に使えるS3互換ストレージとして、ローカル開発環境によく使用されているようです。


## 変更点

### ソースツリー

前回からの変更・追加ソースは以下の通りです。

```
$(PROJECTROOT)
  /compose
    docker-compose.yml
  /src
    /hoge
      /lib
        /Controller
          StorageController.php            (NEW!)
        /Model
      /public
        index.php
      /test
        RequestTest.php
    composer.json
    composer.lock
```

### docker-compose.yml

新たにS3互換のminIO用コンテナを追加します。

なお、この記事を書いている途中で[docker-composeのlinksオプションはすでに時代遅れ](https://docs.docker.com/compose/compose-file/#links)なのを知ってしまったので、今回からlinksは全て削除しています。すでにnetworksを使っているので、linksは冗長だったんですね。

```yml
  ..(略)..  
  storage:
    image: minio/minio
    volumes:
      - storage-data:/data
    container_name: storage
    ports:
      - "19000:9000"
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: miniminio
    command: server /data
    networks:
      - local_net
```


### index.php

DIにstorage用の設定とstorageオブジェクトの定義を行い、storage用のrouteを追加します。

なお、ローカルS3スタックを使用する際には、"use_path_style_endpoint"を有効にする必要があります。
このフラグが無効だと、S3ストレージへのアクセスポイントがサブドメインを使用した形式になるため、localhostを使った名前解決が面倒になるためです。

```php:/src/hoge/public/index.php

..(略)..  

use Hoge\Controller\RedisController;
use Hoge\Controller\DynamodbController;
use Hoge\Controller\StorageController;                   (追加)

..(略)..  

$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
    'settings' => [

        ..(略)..  

        'storage' => [
            'endpoint' => 'http://storage:9000',
            'region' => 'ap-northeast-1'
        ]
    ],

    ..(略)..  

    'storage' => function (ContainerInterface $container) {
        $settings = $container->get('settings')['storage'];
        $sdk = new Aws\Sdk([
            'endpoint' => $settings['endpoint'],
            'region' => $settings['region'],
            'version' => '2006-03-01',
            'credentials' => [
                'key' => 'minio',
                'secret' => 'miniminio'
            ],
            //'bucket_endpoint' => true,
            'use_path_style_endpoint' => true
        ]);
        $s3 = $sdk->createS3();
        return $s3;
    }

..(略)..  

$app->group('/storage', function (RouteCollectorProxy $group) {
    $group->get('/{filename}', StorageController::class . ':get');
    $group->post('', StorageController::class . ':post');
});

$app->run();



### StorageController

ファイルのアップロードと、ファイルの表示(ダウンロードにするためにはcontent-typeとかいじればok)に対応しています。

```php:/src/hoge/lib/Controller/StorageController.php
<?php

namespace Hoge\Controller;

use Aws\S3\Exception\S3Exception;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Container\ContainerInterface;

class StorageController
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;

        /** @var \Aws\S3\S3Client $s3 */
        $s3 = $this->container->get('storage');
        try {
            // バケット確認
            $s3->headBucket(['Bucket' => 'dummy']);
        } catch (S3Exception $ex) {
            $errorCode = $ex->getAwsErrorCode();
            if ($errorCode === 'NotFound') {
                // バケットが無ければ作る
                $s3->createBucket([
                    'Bucket' => 'dummy',
                    'CreateBucketConfiguration' => [
                        'LocationConstraint' => 'ap-northeast-1'
                    ]
                ]);
            }
        }
    }

    public function get(Request $request, Response $response, array $args) : Response
    {
        // parameters (id)
        $key = $args['filename'];

        /** @var \Aws\S3\S3Client $s3 */
        $s3 = $this->container->get('storage');

        try {
            $result = $s3->getObject([
                'Bucket' => 'dummy',
                'Key' => $key
            ]);
        } catch (S3Exception $ex) {
            return $response->withStatus(404);
        }

        $body = $result->get('Body');
        $newResponse = $response->withBody($body);
        return $newResponse;
    }

    public function post(Request $request, Response $response, array $args) : Response
    {
        $files = $request->getUploadedFiles();
        /** @var \Psr\Http\Message\UploadedFileInterface $uploadedFile */
        $uploadedFile = $files['upload'];

        $err = $uploadedFile->getError();
        if ($err !== 0) {
            return $response->withStatus(500);
        }
        $clientFileName = $uploadedFile->getClientFilename();

        /** @var \Aws\S3\S3Client $s3 */
        $s3 = $this->container->get('storage');

        $result = $s3->putObject([
            'Bucket' => 'dummy',
            'Key' => $clientFileName,
            'Body' => $uploadedFile->getStream()
        ]);
        $url = $result->get('ObjectURL');

        $response->getBody()->write($url);
        return $response;
    }
}
```

### composer.json & composer.lock

今までのcontrollerをテストするためのコードを追加したのに伴い、それに使用したPHPUnitをcomposerに追加しました。

```shell
$ composer require phpunit/phpunit -dev
```

```json:/src/composer.json
{
    "require": {
        "slim/slim": "4.2.0",
        "slim/psr7": "0.5.0",
        "php-di/php-di": "6.0.9",
        "ext-pdo": "^7.2",
        "ext-json": "^1.6",
        "ext-redis": "^3.1",
        "ext-curl": "*",
        "aws/aws-sdk-php": "^3.112"
    },
    "require-dev": {
        "phpunit/phpunit": "^8.4"
    }
}
```

## 手動テスト

wgetでpost fileuploadをテストするために、以下のようなコマンドを実行しました。

```sh:postdata.txt
--cuthere
Content-Disposition: form-data; name="upload"; filename="videodrome.txt"
Content-Type: text/plain

THIS IS YOUR TAPE.
--cuthere--
```

```sh
$ wget http://hoge.localhost/.storage --header='Content-Type: multipart/form-data; boundary=cuthere' --post-file=postdata.txt
```

## 全部テスト

今までのControllerをテストするために、/src/hoge/test/RequestTest.phpを追加しました。
長いのでソースはここでは割愛します(リポジトリを直接参照ください)。

## ここまでのソース

[こちら](https://github.com/dynamitecoolguy/slimtemplate/tree/chapter8)でどうぞ。

## 更新

### 2019/10/09

* use_path_style_endpointに関する説明を追加。
* RequestTest.php投入に伴う修正