AUTOVICE

TECH BLOG

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

2020-03-07

はじめに

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

今回はこれらの記事の発展形として、「オリジナル画像」と「トリミング+リサイズした画像」の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メソッド以外にもたくさんの便利なメソッドがあるので、ぜひいろいろと試してみてください。