PHPでAWSのS3に画像アップロード&画像を画面表示(設定〜実装まで)

#背景
ファイルシステムに画像をアップロードする処理を実装してHerokuにアップロードしたところ、Herokuは１日１度のDynoの再起動に伴いアップロードしたファイルを全消去するという仕様があることが判明。

それはアカン！ということで画像アップロード先をAWSのS3に変更しようとしたところHerokuにドキュメントがあったので参考にしたけど何一つうまくいかなかった(多分事前の設定とか必要だったけどそこら辺は省かれていた)ので色々調べて実装した内容をシェアします。

事前にS3側で必要な設定〜実装までの流れを出来るだけ省かずに記載するのできっと参考になると思います。

#AWSアカウント作成
何はともあれ、まずはAWSのアカウント作成からです。
AWSアカウント作成はここを見れば普通に出来ると思います(見なくても出来ます)
https://aws.amazon.com/jp/register-flow/

途中自動音声電話に応答しないといけないのが面倒ですが、こちらが何か話すことがある訳ではないのでご安心下さい。

また、アカウント作成後のタイミングでAccessKeyやSecretAccessKeyが記載されたcsvをダウンロードするタイミングがあるので、ダウンロードしたファイルは保管するようにしておきましょう。

#S3バケットの作成
アカウント作成出来たらログインして「AWSマネジメントコンソール」という画面を表示します。
こんな感じの画面です。
<img width="1225" alt="スクリーンショット 2017-12-06 10.57.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/09261eef-1258-4c65-3db9-0665baa68250.png">

S3を使う場合はデータを格納するバケットを作成する必要があります。
まずはS3の画面に遷移します。
画面左上の「サービス」をクリックするとAWSで使えるサービス一覧が表示されるので、その中の「ストレージ」という項目から「S3」を選択します。
<img width="600" alt="スクリーンショット 2017-12-06 11.01.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/84c6fa56-20dd-f1f0-a2e3-95319a20c07a.png">

S3のサービス画面が表示されました。
![S3.png](https://qiita-image-store.s3.amazonaws.com/0/121811/da5ba4a6-6ae2-ee03-9942-96be588daecf.png)

画面左の「バケットを作成する」をクリックしてバケットを作成していきます。
クリックすると以下のような画面が出てきます。

<img width="705" alt="スクリーンショット 2017-12-06 11.07.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/0dc636b4-d0b1-1973-a7e6-e9fcd8b87d45.png">

###DNS準拠のバケット名
以下のDNS要件に従う形でバケット名を決める必要があります。
・Bucket名にアンスコ(_)を使わない
・3～63の間の文字長
・ダッシュ(-)で終わらない
・ピリオド(.)を連続して表記しない(..)
・ダッシュ(-)前後でピリオドを使わない(-.または.-)
・ピリオド(.)、アンスコ(_)、ダッシュ(-)で終わらない。（ドキュメントには載っていません）
(例)my-project

こちらの記事も参考になります。
[Amazon S3 の Bucket 命名ルールについて](https://dev.classmethod.jp/cloud/amazon-s3-bucket-name-rule/)

###リージョン
バケットを置くリージョンを選択します。
僕の場合はHerokuからアクセスすることになり、Herokuのリージョンが米国だったので米国を選択しました。
(と思ったけど読み込みの場合は日本から接続することになるから東京リージョンの方が良かったかも..)

この2つを入力したら「次へ」をクリックします。
この後プロパティの設定、アクセス許可の設定へと進んでいきます。

###プロパティ設定
プロパティは特に何も設定しなくてもバケットは作れるみたいですが、一応「デフォルト暗号化」の項目だけ設定しました。
<img width="702" alt="スクリーンショット 2017-12-06 11.29.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/2ea65488-8dd4-a228-d67a-a8b49c979a99.png">

###アクセス許可設定
ここはデフォルトのまま特に変更しませんでした。
「パブリックアクセス許可を管理する」の項目は推奨されている「このバケットにパブリック読み取りアクセス権限を付与しない (推奨)」を選んだ方がいいようです。
<img width="699" alt="スクリーンショット 2017-12-06 11.30.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/5470f719-3c09-a1b3-01c6-b94bddc0e898.png">

最後に確認画面でバケットの作成をクリックして完了です。
<img width="700" alt="スクリーンショット 2017-12-06 11.32.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/431bb2cc-7f55-6080-eb0e-f8f33ec21522.png">


#IAMの作成
ここまでアカウント作成とS3バケットの作成をやってきました。
この状態でも画像アップロードの実装は出来るのですが、作成したアカウントはS3以外にも色んなサービスを扱うことが出来てしまうので、それをプログラム内に記述するのは宜しくないだろう、ということで、S3だけを扱うことが出来る専用のユーザーを作成する必要があります。
これをIAM(Identity and Access Management)と言います。

これも画面左上の「サービス」をクリックし、「セキュリティ、アイデンティティ、コンプライアンス」項目にある「IAM」をクリックします。
<img width="1016" alt="スクリーンショット 2017-12-06 11.39.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/121811/a8f1779d-6724-bedf-ae8f-c095d32d6874.png">

IAMのダッシュボードはこんな感じです。
ユーザーが作成されていない最初の段階では何も記載されてないかもしれません。
![IAM.png](https://qiita-image-store.s3.amazonaws.com/0/121811/1e6780aa-b539-f474-34b8-e9df89983d37.png)

左側の項目から「ユーザー」をクリックすると以下の画面が出てくるので「ユーザーを追加」をクリックします。
![IAMuser.png](https://qiita-image-store.s3.amazonaws.com/0/121811/b95b980c-a813-a799-06f8-43b058300111.png)

ユーザー情報の設定画面に遷移しますのでここからIAMのユーザー情報を設定していきます。
こちらの記事に詳しく解説されています。
ページで解説されている「インラインポリシーの追加」で「アクション」を選択すると思うのですが、ここで`putObject`に加え`putObjectAcl`も選択しておきます。
(理由は後述します)
[「AWS SDK for PHP v3」を使ったS3へのアップロード・ダウンロード処理 IAMの設定](http://beyondjapan.com/blog/2017/04/%E3%80%8Caws-sdk-for-php-v3%E3%80%8D%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fs3%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%83%BB%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC#iam-)


IAMユーザー作成後AccessKeyとSecretAccessKeyが記載されたcsvがダウンロード出来るのでダウンロードして保管しておきましょう。
このIAMユーザーのAccessKeyとSecretAccessKeyを.envファイルで環境変数に設定します。

#AWS SDK for PHPをインストール
composerでインストール出来ます。
最新は3系みたいなので3系がインストールされます。

```
composer install aws/aws-sdk-php
```

僕の場合は、一度Herokuのドキュメントを見ながらv2.6をインストールしてしまっていたので、composer.jsonを以下のように変更して`composer update`しました。

```json
"require": {
    "aws/aws-sdk-php": "3.*",
}
```

#環境変数の設定
3つ環境変数を`.env`に設定します。
ここで設定するのは最初に作成したAWSアカウントのAccessKeyではなく、その後作成したIAMユーザーのAccessKey等を設定します。
先述しましたが、AWSアカウントだとS3以外のサービスも使えてしまいます。今回S3のみ利用するため権限をS3のみに限定したIAMユーザーを使います。

```
AWS_ACCESS_KEY_ID=IAMユーザーのAccessKey
AWS_SECRET_ACCESS_KEY=IAMユーザーのSecretAccessKey
S3_BUCKET_NAME=設定したS3バケット名
```

Herokuの場合はHerokuの環境変数を設定する部分に上記を設定します。

#画像アップロードの実装
ここまでで下準備は完了しました。
画像アップロード処理の実装は以下のようになりました。

```php
public function upload(Request $request,int $id)
{
    //拡張子で画像でないファイルをはじく
    $ext = substr($filename, strrpos($_FILES['img_path']['name'], '.') + 1);
    if(strtolower($ext) !== 'png' && strtolower($ext) !== 'jpg' && strtolower($ext) !== 'gif'){
        echo '画像以外のファイルが指定されています。画像ファイル(png/jpg/jpeg/gif)を指定して下さい';
        exit();
    }
    //読み込みの際のキーとなるS3上のファイルパスを作る(作り方は色々あると思います)
    $tmpname = str_replace('/tmp/','',$_FILES['img_path']['tmp_name']);
    $new_filename = 'profiles/'.$id.'-'.time().'-'.$tmpname.'.'.$ext;

    //S3clientのインスタンス生成(各項目の説明は後述)
    $s3client = S3Client::factory([
        'credentials' => [
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
        ],
        'region' => 'us-east-2',
        'version' => 'latest',
    ]);
    //バケット名を指定
    $bucket = getenv('S3_BUCKET_NAME')?: die('No "S3_BUCKET_NAME" config var in found in env!');
    //アップロードするファイルを用意
    $image = fopen($_FILES['img_path']['tmp_name'],'rb');

    //画像のアップロード(各項目の説明は後述)
    $result = $s3client->putObject([
        'ACL' => 'public-read',
        'Bucket' => $bucket,
        'Key' => $new_filename,
        'Body' => $image,
        'ContentType' => mime_content_type($_FILES['img_path']['tmp_name']),
    ]);

    //読み取り用のパスを返す
    $path = $result['ObjectURL'];

    //パスをDBに保存(ここの詳細処理は今回は記述しません)
    $this->userRepository->updateUserProfsById($id, 'img_path', $path);
}
```

前半のファイルパス生成までは大丈夫だと思うので、S3クライアントのインスタンス生成とアップロード処理の部分を解説します。

###S3クライアントのインスタンス生成

アップロード処理やダウンロード処理を行う際にこのS3クライアントのインスタンスを生成し、そのメソッドである`putObject`や`getObject`を使ってアップロード処理やダウンロード処理を行います。
そのため、まずS3クライアントのインスタンスを生成する必要があります。

今回使った`factory`メソッドは実は非推奨になってしまっていて、他のやり方を探そうと思ったのですが時間関係上一旦これで実装して後でなおす予定です。

生成の際はいくつかパラメータを設定する必要があります。

**credentials**
　・・・IAMユーザーのAccessKeyやSecretAccessKeyを設定します
**region**
　・・・バケット生成の際に設定したのリージョンを設定します。リージョンによって文字列が異なりますが、こちらのページに一覧が載っています。
[AWS Regions and Endpoints](http://docs.aws.amazon.com/general/latest/gr/rande.html)(下の方にあるAmazon Simple Storage Service (Amazon S3)の項目を見ましょう)
**version**
　・・・ちょっとよく分からなかったのですが下記を参考にlatestにしました。
[「AWS SDK for PHP v3」を使ったS3へのアップロード・ダウンロード処理](http://beyondjapan.com/blog/2017/04/%E3%80%8Caws-sdk-for-php-v3%E3%80%8D%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fs3%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%83%BB%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC#php-)

ちなみに'credentials'は必須だと思うのですが、`region`も設定しないとエラーになりました。

```php
    $s3client = S3Client::factory([
        'credentials' => [
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
        ],
        'region' => 'us-east-2',
        'version' => 'latest',
    ]);
```

###putObjectでアップロード
IAMユーザーを作成した際にアクションに`putObject`を入れたのでこのメソッドが使るようになっています。

**ACL**・・・アップロードするファイルのアクセス権限を設定します。`public-read`にすることで、ブラウザで画像を表示できるようになります。IAMユーザーを作成した際にアクションに`putObjectAcl`を入れていなかった場合、この設定が出来ません。
**Bucket**・・・バケット名を指定
**Key**・・・バケット内にファイルを保存する際のキーになります。例えば`/public/sample.png`だとバケット内のpublicフォルダ内にsample.pngという名前で保存されます。僕の場合はあらかじめバケット内にpublicというフォルダを作成しておきました。
**Body**・・・ファイルの中身を設定します。string型かResource型か`Psr\Http\Message\StreamInterface`を設定出来ます。今回はfopenで作成したResource型を設定しました。
**ContentType**・・・ファイルのMIMEタイプを指定します。これが無いとデフォルトのタイプになってしまい画像として認識されないようです。

```php
    $result = $s3client->putObject([
        'ACL' => 'public-read',
        'Bucket' => $bucket,
        'Key' => $new_filename,
        'Body' => $image,
        'ContentType' => mime_content_type($_FILES['img_path']['tmp_name']),
    ]);
```

その他にも設定できる項目がたくさんあり、こちらのページで確認出来ます。
[putObjectリファレンス](http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putobject)


#アップロードした画像を画面表示
ここまでで画像のアップロードまで完了しました。
上述しましたが、以下のコードの$resultにアップロードしたオブジェクトの情報が入っています。そこから`ObjectURL`というキーで対象の画像のURLを取得できるため、表示の際はこのURLをimgタグのsrcに設定することで画像を表示出来ます。

```php
    //画像のアップロード(各項目の説明は後述)
    $result = $s3client->putObject([
        'ACL' => 'public-read',
        'Bucket' => $bucket,
        'Key' => $new_filename,
        'Body' => $image,
        'ContentType' => mime_content_type($_FILES['img_path']['tmp_name']),
    ]);

    //読み取り用のパスを返す
    $path = $result['ObjectURL'];
```

(参考)
■画像のアップロード・ダウンロード処理全体の流れ
https://remotestance.com/blog/3044/
http://beyondjapan.com/blog/2017/04/%E3%80%8Caws-sdk-for-php-v3%E3%80%8D%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fs3%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%83%BB%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC#php-

■画像アップロードのバリデート
・拡張子が画像のものになっているか
　http://affikatsu.com/picture-file-format-752/
・リクエストに画像が含まれるか
・画像サイズ(必要なら)

■AccessDeniedになる
https://stackoverflow.com/questions/39849949/php-amazon-sdk-s3-bucket-access-denied
(putObjectAclをIAMの権限に設定して無いのに’ACL’を設定してた)

■S3Clientの各メソッドリファレンス・putObjectメソッドのパラメータ等
http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putobject

■MIMEタイプとは
https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types
image/png、text/plainとか

■MIMEタイプ取得
http://php.net/manual/ja/function.mime-content-type.php
mime_content_type($_FILES['img_path']['tmp_name’])で取得出来るぽい

■S3アクセス権限コントロール
https://qiita.com/ryo0301/items/791c0a666feeea0a704c

■S3リージョン一覧
http://docs.aws.amazon.com/general/latest/gr/rande.html

■画面で画像を表示する
・IAMのポリシーでs3:PutObjectAclを有効にして、
・putObjectメソッド内で’ACL’ => 'public-read’を設定する
https://www.larajapan.com/category/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89/
・画像パスは['ObjectURL’]で取れる
https://ja.stackoverflow.com/questions/19964/aws%E3%81%AEs%EF%BC%93%E3%81%ABphp%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89

■LaravelでS3に画像アップロード
https://www.larajapan.com/category/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89/