Laravelアプリの画像アップロード機能をローカル環境と本番環境で条件分岐

# 概要
PHP初学者にとって画像アップロード機能の実装はなかなかの鬼門ですよね。本番環境でS3を用いて画像を表示させるのも時間がかかりました。
という事で、備忘録として記事にしてみます。

今回は、新規投稿ページで画像を保存し詳細ページで画像を表示、編集ページで画像を変更できるように実装していきます。
さらに本番環境（Heroku）で画像が表示できるようにAWSのS3を使用します。
ローカルと本番のコードを条件分岐し、ローカルではstorage/app/public下のフォルダへ、本番ではS3へ画像を保存できるようにしていきます。

# 環境
- PHP 7.2.34
- Laravel 7.30.4
- mySQL 8.0.23
- Docker
- Heroku
- AWS S3

# 前提
画像アップロード機能を追加するアプリケーションがあらかじめ作成されていること、HerokuやAWSなどの登録が完了済であることを前提として進めていきます。
あくまでこの記事は画像アップロード機能の実装がメインです。

# ローカルで実装
私が参考にした記事はこちらです→https://note.com/koushikagawa/n/n74380a1f3643

### 既存のテーブルにimageカラム追加

```php:terminal
% php artisan make:migration add_image_url_to_posts_table --table=posts
```

postsテーブルに画像を保存するためのカラムを追加します。↑のコマンドを実行するとマイグレーションファイルが作成されます。

```php:0000_00_00_000000_add_image_to_posts_table.php
public function up()
{
  Schema::table('posts', function (Blueprint $table) {
    $table->string('image'); // 追加
  });
}

public function down()
{
  Schema::table('posts', function (Blueprint $table) {
    $table->dropColumn('image'); // 追加
  });
}
```

このように記述しマイグレーションします。

```php:terminal
% php artisan migrate
```

phpmyadminやSequel Aceでimageカラムができているかを確認してみて下さい。

### 新規投稿ページにて画像を保存

```php:PostController.php
public function store(Request $request)
{
  $time = date("Ymdhis");
  $post->image = $request->image->storeAs('public/post_images',$time.'_'.Auth::user()->id. '.jpg');
}
```

画像の保存先はstorage/app/public/post_imagesです。storage/app/public下にpost_imagesフォルダを作成しておいて下さい（post_imagesは任意の名前です）。

```php:create.blade.php
<form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data">
  <p>
    <input type="file" name="image">
  </p>
</form>
```

enctype="multipart/form-data"がないと画像保存ができないので忘れずに。
![039725bc405157e523d8d0ee9a6a48e6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851225/539fe6bd-f871-988e-7bd4-5e7d63ccc29b.png)
画像を選択し投稿すると
![8a0041462fdfe3d1343d228ad2a28e09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851225/241afaf6-1e86-ab69-de9a-1f2e0fd4545e.png)
Sequel Aceにて、imageにデータがちゃんと入っています。

### 詳細ページに画像を表示

```php:PostController.php
public function show(Post $post)
{
  return view('posts.show',[
    'post' => $post,
    'image' => str_replace('public/', 'storage/', $post->image) // 追加
  ]);
}
```

showメソッドにこのように追加します。str_replace関数で'public/'と'storage/'を置換しています。
laravelではpublic下に画像を直接置くのではなく、storage下に置くことを推奨しているそうです。

```php:terminal
% php artisan storage:link
```

public下からstorage下にシンボリックリンクを張ります。

```php:show.blade.php
@if ($image)
  <p><img class="img-fluid" src ="/{{ $image }}"></p>
@endif
```

Bootstrapを導入し、class="img-fluid"を追加すると画像が良き大きさになるのでオススメです。
![ce7c0aaffbccfd2f884d99a9e3c2ec83.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851225/cde20452-19c2-dd45-7a7e-537b3977bac5.jpeg)
画像が表示できました。

### 編集ページで画像を変更

```php:PostController.php
use Illuminate\Support\Facades\Storage; // 追加

public function update(Request $request)
{
  if($request->hasFile('image')) {
    Storage::delete('public/post_images/' . $post->image); // 画像削除
    $time = date("Ymdhis");
    $post->image = $request->image->storeAs('public/post_images', $time.'_'.Auth::user()->id. '.jpg');
  }
}
```

保存されてある画像を削除し新たに画像を保存します。

```php:edit.blade.php
<form action="{{ route('posts.update') }}" method="POST" enctype="multipart/form-data">
  <p class="card-text">
    <input type="file" name="image">
  </p>
</form>
```

createと一緒ですね。
これで画像アップロード機能が実装できました。

# 本番環境で画像アップロード
### AWSのS3に移動しバケットを作成します。
![a3ddd4d79078faac6a5002e66ac8dfd4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851225/15cbe766-7fb3-7302-35fb-c82433abc69b.png)
バケット名を入力します↑
![68f82c67a05f1686d6bdf1c40e774e95.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/851225/ef8cf976-bfbb-d43d-b55b-9c7f91e8ebc4.png)
バケットの設定をこのようにして「バケットを作成」を押します。

### Herokuに環境変数を設定

```php:terminal
% heroku config:set APP_ENV=production
% heroku config:set AWS_ACCESS_KEY_ID=◯◯◯◯◯◯◯◯◯◯
% heroku config:set AWS_BUCKET=バケット名
% heroku config:set AWS_DEFAULT_REGION=ap-northeast-1
% heroku config:set AWS_SECRET_ACCESS_KEY=△△△△△△△△△△
```

APP_ENVは条件分岐する上で必要になります。
アクセスキーとシークレットキーも忘れずに設定して下さい。

### ローカル環境と本番環境の記述を条件分岐
ローカルと本番の記述を条件分岐するにあたって参考にさせて頂きました↓
https://qiita.com/qwe001/items/96a83fadcfaeb3cd4a6e

```php:PostController.php
public function store(Request $request)
{
  if (app()->isLocal()) {
    // ローカル環境
    $time = date("Ymdhis");
    $post->image = $request->image->storeAs('public/post_images', $time.'_'.Auth::user()->id. '.jpg');
  } else {
    // 本番環境
    $image = $request->file('image');
    $path = Storage::disk('s3')->putFile('/', $image, 'public');
    $post->image = $path;
  }       
}

public function update(Request $request)
{
  if($request->hasFile('image')) {
    if (app()->isLocal()) {
      // ローカル環境
      Storage::delete('public/post_images/' . $post->image);
      $time = date("Ymdhis");
      $post->image = $request->image->storeAs('public/post_images', $time.'_'.Auth::user()->id. '.jpg');
    } else {
      // 本番環境
      $image = $request->image;
      Storage::disk('s3')->delete($image);
      $path = Storage::disk('s3')->putFile('/', $image, 'public');
      $post->image = $path;
    }
  }
}
```

ローカルか本番かの判断は、ローカルではenvファイルのAPP_ENV=lacal（デフォルトですでに設定されています）、本番は先ほど設定した環境変数のAPP_ENV=productionによってされているようです。

```php:show.blade.php
@if ($image)
  @if (App::environment('local'))
    // ローカル環境
    <p><img class="img-fluid" src="/{{ $image }}"></p>
  @else
    // 本番環境
    <p><img class="img-fluid" src="https://バケット名.s3.ap-northeast-1.amazonaws.com/{{ $image }}"></p>
  @endif
@endif
```

こちらでは違う書き方で条件分岐させてもらいました。いずれにしろ、挙動確認では問題なかったので試して頂ければと思います。
設定したバケット名を忘れずに当てはめて下さい。

```php:PostController.php
public function show(Post $post)
{
  return view('posts.show', [
    'post' => $post,
    'image' => str_replace('public/', 'storage/', $post->image),
  ]);
}
```

showメソッドなのですが、このままでも本番環境で問題なく動きます。本番環境において、public下とstorage下が置換されたところで何の意味も影響もないでしょうから。
ただ、「本番環境のコードだけでいい！」「条件分岐もしない！」という方もいるかもしれません、

```php:PostController.php
public function show(Post $post)
{
  return view('posts.show', [
    'post' => $post,
    'image' => $post->image // 変更
  ]);
}
```

その場合はこのコードの方がスッキリして簡潔です。

別に条件分岐しなくても本番環境のコードだけでいいと思います。ただ、それだと（S3設定以降の）ローカルで保存した画像も一緒にS3のバケットに保存されてごっちゃになります。
それが嫌なので条件分岐してみました。

私はこれでローカルでも本番でも問題なく動いたので参考にして頂ければと思います。
最後までこの記事を読んで頂き、ありがとうございました。