開発環境(ローカルストレージ)と本番環境(Amazon S3)にトリミング+リサイズした画像を保存する方法

はじめに

以前、以下の記事を書きました。

今回はこれらの記事の発展形として、「オリジナル画像」と「トリミング+リサイズした画像」の2種類を、開発環境ならローカルストレージに保存し、本番環境ならAmazon S3に保存する方法を解説します。

今回の方針は以下の通りです。

  1. 開発環境+画像がGIF形式の場合
    •  ローカルストレージにオリジナル画像を保存
  2. 開発環境+画像がGIF形式でない場合
    •  ローカルストレージにオリジナル画像を保存
    •  ローカルストレージにトリミング+リサイズした画像を保存
  3. 本番環境+画像がGIF形式の場合
    •  Amazon S3にオリジナル画像を保存
  4. 本番環境+画像がGIF形式でない場合
    •  Amazon S3にオリジナル画像を保存
    •  Amazon S3にトリミング+リサイズした画像を保存

画像のトリミング+リサイズにはIntervention Imageというライブラリを使います。Intervention Imageは現在のところGIF形式には対応していないため、画像がGIF形式の場合はIntervention Imageを使わずにオリジナル画像のみを保存します。

¿Animated GIF support? · Issue #176 · Intervention/image

準備

Intervention Image

以下のコマンドを実行します。

$ php composer require intervention/image

config/app.phpに以下を追記します。

    'providers' => [
        ...
        Intervention\Image\ImageServiceProvider::class,
        ...
    ],
    'aliases' => [
        ...
        'Image' => Intervention\Image\Facades\Image::class,
        ...
    ],

テーブル

保存した画像の参照URIを保存するためのカラムを用意しておきます。

今回は、posts.imageposts.image_320_180という2つのカラムがあるという状態とします。

ビュー

画像を添付できるフォームを持つビューを用意しておきます。

今回は、ビューからimageという名前で画像ファイルがコントローラーに渡されてくる状態とします。

コントローラーの実装

まずはじめに、storeメソッドの全文を記載します。

use Image;

class PostsController extends Controller
{
    public function store(CreatePost $request)
    {
        if (!is_null($request->image)) {
            # 画像がNULLでない場合
            $hash = $request->file('image')->hashName();

            if ( app()->isLocal() || app()->runningUnitTests() ) {
                # 開発環境の場合
                if ($request->image->getClientOriginalExtension() === 'gif') {
                    # GIF形式の場合
                    $request->file('image')->storeAs('public/images/gif', $hash);
                    $post->image = Storage::url('public/images/gif/' . $hash);
                }
                else {
                    # GIF形式でない場合
                    $image = Image::make($request->file('image'));
                    $image_320_180 = Image::make($request->file('image'))->fit(320, 180);

                    $image->save(storage_path() . '/app/public/images/' . $hash);
                    $image_320_180->save(storage_path() . '/app/public/images/320-180/' . $hash);

                    $post->image = Storage::url('public/images/' . $hash);
                    $post->image_320_180 = Storage::url('public/images/320-180/' . $hash);
                }
            }
            else {
                # 本番環境の場合
                if ($request->image->getClientOriginalExtension() === 'gif') {
                    # GIF形式の場合
                    Storage::disk('s3')->put('/images/gif/' . $hash, file_get_contents($request->file('image')), 'public');
                    $post->image = Storage::disk('s3')->url('images/gif/' . $hash);
                }
                else {
                    # GIF形式でない場合
                    $image = Image::make($request->file('image'))->stream();
                    $image_320_180 = Image::make($request->file('image'))->fit(320, 180)->stream();

                    Storage::disk('s3')->put('/images/' . $hash, $image->__toString(), 'public');
                    Storage::disk('s3')->put('/images/320-180/' . $hash, $image_320_180->__toString(), 'public');

                    $post->image = Storage::disk('s3')->url('images/' . $hash);
                    $post->image_320_180 = Storage::disk('s3')->url('images/320-180/' . $hash);
                }
            }
        }
        else {
            # 画像がNULLの場合
            $post->image = '/images/noimage.png';
        }

        Auth::user()->posts()->save($post);

        return redirect()->route('posts.index');
    }
}

画像のハッシュネームを取得します。このハッシュネームは保存するときのファイル名に使います。保存ファイル名は一意であればなんでも構いません(処理日時+ファイル名など)。

$hash = $request->file('image')->hashName();

開発環境+画像がGIF形式の場合

保存先ディレクトリはstorage/app/public/images/gif/です。

$request->file('image')->storeAs('public/images/gif', $hash);
$post->image = Storage::url('public/images/gif/' . $hash);

画像がGIF形式の場合はIntervention Imageを使わず、普通に保存します。

開発環境+画像がGIF形式でない場合

保存先ディレクトリはstorage/app/public/images/storage/app/public/images/320-180/です。

$image = Image::make($request->file('image'));
$image_320_180 = Image::make($request->file('image'))->fit(320, 180);

$image->save(storage_path() . '/app/public/images/' . $hash);
$image_320_180->save(storage_path() . '/app/public/images/320-180/' . $hash);

$post->image = Storage::url('public/images/' . $hash);
$post->image_320_180 = Storage::url('public/images/320-180/' . $hash);

1〜2行目でIntervention Imageのインスタンスを作成しています。1行目はオリジナル画像、2行目はトリミング+リサイズした画像です。トリミング+リサイズにはfitメソッドを使います。

4〜5行目でローカルストレージに保存しています。

7〜8行目で保存先URIをDBに保存しています。保存した画像をビューで表示したいときは、これらのカラムをimgタグに指定することになります。

本番環境+画像がGIF形式の場合

保存先ディレクトリはimages/gif/です。

Storage::disk('s3')->put('/images/gif/' . $hash, file_get_contents($request->file('image')), 'public');
$post->image = Storage::disk('s3')->url('images/gif/' . $hash);

画像がGIF形式の場合はIntervention Imageを使わず、普通に保存します。putメソッドの第2引数は、必ずfile_get_contentsメソッドを使うようにしてください。

本番環境+画像がGIF形式でない場合

保存先ディレクトリはimages/images/320-180/です。

$image = Image::make($request->file('image'))->stream();
$image_320_180 = Image::make($request->file('image'))->fit(320, 180)->stream();

Storage::disk('s3')->put('/images/' . $hash, $image->__toString(), 'public');
Storage::disk('s3')->put('/images/320-180/' . $hash, $image_320_180->__toString(), 'public');

$post->image = Storage::disk('s3')->url('images/' . $hash);
$post->image_320_180 = Storage::disk('s3')->url('images/320-180/' . $hash);

1〜2行目でIntervention Imageのインスタンスを作成しています。基本的に開発環境の場合と同じですが、streamメソッドを使ってストリーム形式に変換していることに注意してください。

4〜5行目でローカルストレージに保存しています。putメソッドの第2引数は、ストリーム形式に変換したIntervention Imageインスタンスを__toStringメソッドで文字列化しています。

streamメソッドと__toStringメソッドは特に大事なので忘れないようにしてください。

7〜8行目で保存先URIをDBに保存しています。

Herokuへのデプロイ

Herokuにデプロイし画像を保存しようとすると、以下のエラーが出るかと思います。ひょっとしたらローカル環境でも出た人がいるかも知れません。

GD Library extension not available with this PHP installation.

実は、Intervention Imageは内部でGDライブラリを使用しているため、GDライブラリがないという内容のエラーが出ています。

エラーを解消するには。デプロイ先のHerokuにGDライブラリを入れればOKです。HerokuにGDライブラリを入れるには以下の手順を行います。

composer.jsonに以下を追記します。

{
    ...
    "require": {
        ...
        // 以下を追記
        "ext-gd": "*"
    }
    ...
}

次に、以下のコマンドを実行します。

# composer update

後は、HerokuにデプロイしたタイミングでGDライブラリがインストールされます。

まとめ

いかがでしたでしょうか。この記事でLaravelの画像アップロード周りはだいたい網羅できるようになったかと思います。

Intervension Imageにはfitメソッド以外にもたくさんの便利なメソッドがあるので、ぜひいろいろと試してみてください。

関連記事

Laravel Mixの仕組みをわかりやすく解説:CSSをSASS/SCSSで記述する方法
# はじめに Laravel Mixの仕組みをわかりやすく解説します。Laravel Mixを使ってアセットコンパイルを行うことで、SASS/SCSSでCSSを記述できるようになります。 # Laravel Mixの仕組み まずはじめに、 [...]
2020年3月6日 9:25
【Laravel 6.x】Laravelプロジェクトの名称を変更する方法
# はじめに あまりないケースではありますが、一度作成したLaravelプロジェクトの名称を変更する方法を解説します。ここで言うLaravelプロジェクトの名称とは、以下のコマンドの`Sample`のことを指します。 ```terminal [...]
2020年3月4日 16:24
【Laravel】ローカルディスクやAmazon S3にアップロードした画像をビューで表示する方法
# はじめに 本記事では、ローカルディスクやAmazon S3にアップロードした画像をビューで表示する方法について説明しています。 なお、本記事では画像のアップロードする方法については説明していません。本記事は既に実装済みの画像アップロード [...]
2020年3月4日 15:44
【Laravel】Amazon S3へのアップロードが「403 Access Denied」で失敗する原因と対処方法
# はじめに LaravelでAmazon S3へ画像アップロードする処理を実装し、いざアップロードしようとしたところ、以下のエラーが出て失敗しました。 ``` Error executing "PutObject" on "https: [...]
2020年3月4日 13:33
開発環境と本番環境で画像のアップロード先を分岐する方法
# はじめに この記事では、以下の方針で画像のアップロード先を分岐する方法について解説しています。 - 開発環境:ローカル(storage/app/public/) - 本番環境:Amazon S3 # 事前準備 AWSに未登録の [...]
2020年3月4日 12:52
独自ポリシーの導入手順と注意点(ハマリポイント)を解説
# はじめに ユーザーと記事を関連付けておいて、編集や削除は記事の作成者のみ許可したいときにはポリシーを作成します。今回はポリシーの導入手順と注意点(ハマリポイント)を解説します。 # 導入手順 ## ポリシー作成 ターミナルで以下 [...]
2020年3月1日 10:32
ユーザー認証機能の導入手順と生成されたフォームを日本語化する方法を解説
# はじめに Laravelではユーザー認証機能の導入が簡単にできてしまいます。この記事ではLaravelプロジェクトにユーザー認証機能を追加する手順と、生成されたフォーム(デフォルトでは英語)を日本語化する方法を解説します。 # 前提 [...]
2020年2月29日 13:54
【Laravel 6.x】ネスト構造になっているRESTfulルーティングの定義方法
# はじめに RESTfulルーティングを定義するときは`Route::resource`を使いますが、ネスト構造になっているRESTfulルーティングの定義方法を調べたので記事にします。 # 前提 おそらく、Laravel 6.xのみ対 [...]
2020年2月28日 20:06