7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravel + Nuxtで複数画像を投稿する

Posted at

概要

Nuxt + Laravel構成で複数ファイルをアップロードを行えるようにしたい

構成

Laravel 5.8
Nuxt 2.4

参考資料

FormData オブジェクトの利用

XMLHttpRequest

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (9) 写真投稿API

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (10) 写真投稿フォーム

仕様方針

  • 写真の実態はS3に保存し、ファイルパスのみをDBに保存する処理方式にする。
  • 今回は最大4枚まで保存できるようにする

フロント(Nuxt)の実装

今回は最大で4枚まで画像を登録できるデザインにしています。

image.png

画像表示

      <!-- 商品画像 -->
      <div class="field image-area">
        <div v-for="(file, index) in files" :key="index" class="product-image">
          <img :src="file" @click="selectProductImage(index)" />
          <input
            :id="`product_image_` + index"
            type="file"
            accept="image/png,image/jpeg,image/gif"
            @change="uploadProductImage($event, index)"
          />
        </div>
      </div>

画像選択した際に、選択した画像をプレビューさせ、登録画像を取得する

// inputタグは非表示にしており、画像イメージをクリック時にinputタグのクリックイベントを発火させる
    selectProductImage(index) {
      const input = document.querySelector('#product_image_' + index)

      // 既に画像が選択されているかチェックする
      if (input.value !== '') {
        // 既に画像が選択されている場合は削除する
        if (window.confirm('画像を削除してよろしいですか?')) {
          input.value = ''

          // 表示用の変数は指定場所の画像情報を削除して削除したところをNoImageで末尾に追加する
          this.files.splice(index, 1, require('@/assets/img/NoImage.png'))

          // 登録用の画像データは削除する
          this.form.images.splice(index, 1, null)
        }
      } else {
        // 画像選択されていない場合、選択ダイアログを表示させる
        input.click()
      }
    },
    uploadProductImage(event, index) {
      // nothing to do when 'files' is empty
      if (event.target.files.length === 0) {
        return null
      }

      // ファイルのオブジェクトURLを生成する
      const productImageUrl = (window.URL || window.webkitURL).createObjectURL(
        event.target.files[0]
      )

      // $setを利用する、Vueが監視出来る配列のメソッドを使う
      // https://jp.vuejs.org/v2/guide/list.html#%E9%85%8D%E5%88%97%E3%81%AE%E5%A4%89%E5%8C%96%E3%82%92%E6%A4%9C%E5%87%BA
      this.$set(this.files, index, productImageUrl)
      this.$set(this.form.images, index, event.target.files[0])
    }

画像を登録APIを呼び出す

  async register() {
      this.form.shop_id = this.$auth.user.id

      // ファイル送信
      const formData = new FormData()

      formData.append('shop_id', this.form.shop_id)
      formData.append('product_name', this.form.product_name)
      formData.append('category_id', this.form.category_id)
      formData.append('description_product', this.form.description_product)
      formData.append('price', this.form.price)
      formData.append('stock', this.form.stock)
      formData.append('shipping', this.form.shipping)
      formData.append('shipping_method', this.form.shipping_method)
      formData.append('shipping_origin', this.form.shipping_origin)
      formData.append('shipping_estimated', this.form.shipping_estimated)
      formData.append('shipping_cost', this.form.shipping_cost)

      // ファイル情報を取得する
      for (let index = 0; index < 4; index++) {
        // inputタグにファイルが設定されているかチェックを行う
        if (this.form.images[index] === null) {
          continue
        }

        // 存在すればファイル情報を登録情報に追加する
        formData.append('images[]', this.form.images[index])
        formData.append('order[]', index + 1)
      }

      await this.$axios
        .$post('/api/shop/product', formData)
        .then(data => {
          this.$router.push('/shop')
        })
        .catch(errors => {})
    },

バックエンド(Laravel)の実装

  1. 画像保存用のS3 バケットを作成し、LaravelからS3にアップロードするためのIAMユーザを作成
  2. envファイルに IAM ユーザーおよび S3 バケットの接続情報を記述する
  3. APIの実装

1. 画像保存用のS3 バケットを作成し、LaravelからS3にアップロードするためのIAMユーザを作成

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (9) 写真投稿API

上記を参考に、バケット作成とIAMユーザの作成を行う

Storage::cloud()のデフォルトをS3に設定する

filesystems.php
'cloud' => env('FILESYSTEM_CLOUD', 's3'),

S3 にアクセスするために必要なライブラリをインストール

composer require league/flysystem-aws-s3-v3

2. envファイルに IAM ユーザーおよび S3 バケットの接続情報を記述する

AWS_ACCESS_KEY_ID=アクセスキーID
AWS_SECRET_ACCESS_KEY=シークレットアクセスキー
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=バケット名
AWS_URL=https://s3-ap-northeast-1.amazonaws.com/バケット名/

3. APIの実装

基本的には、下記のサイトを参考に実装しました。

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (9) 写真投稿API

サムネイルの作成は書きを参考

LaravelでIntervention Imageを使って加工した画像をS3へ保存する

サムネイルの作成

サムネイルを作成するために
画像処理のライブラリであるIntervention Imageをサーバへインストール

$ composer require intervention/image

Laravelの設定ファイルの下記を追加する

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

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

キャッシュをクリアする

$ php artisan config:clear

コントローラの実装

バリデーションの実装部分

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class StoreProductRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'shop_id' => ['required', 'numeric'],
            'product_name' => ['required', 'string', 'max:255'],
            'category_id' => ['required', 'numeric'],
            'description_product' => ['required', 'string', 'max:1000'],
            'price' => ['required', 'numeric'],
            'stock' => ['required', 'numeric'],
            'shipping' => ['required', 'boolean'],
            'shipping_method' => ['required_if:shipping,1', 'numeric', 'nullable'],
            'shipping_origin' => ['required_if:shipping,1', 'numeric', 'nullable'],
            'shipping_estimated' => ['required_if:shipping,1', 'numeric', 'nullable'],
            'shipping_cost' => ['required_if:shipping,1', 'numeric', 'nullable'],
            'images.*' => ['file', 'mimes:jpg,jpeg,png,gif', 'max:10240']
        ];
    }

    /**
     * [Override] バリデーション失敗時
     *
     * @param Validator $validator
     * @throw HttpResponseException
     */
    protected function failedValidation( Validator $validator )
    {
        $response['success'] = false;
        $response['errors']  = $validator->errors()->toArray();

        throw new HttpResponseException(
            response()->json( $response, 422 )
        );
    }
}

画像のアップロードAPIの実装

    /**
     * 商品登録
     * @param Request $request
     * @return \Illuminate\Http\Response
     */
    public function create(StoreProductRequest $request)
    {

        // データベースエラー時にファイル削除を行うため
        // トランザクションを利用する
        DB::beginTransaction();

        $imagePaths[] = null;

//        dd($request['images']);

        // 画像情報を取得する
        $images = $request['images'];
        $order = $request['order'];


        try {
            // 商品情報を登録する
            $product = Product::create([
                'shop_id' => $request['shop_id'],
                'product_name' => $request['product_name'],
                'category_id' => $request['category_id'],
                'description_product' => $request['description_product'],
                'price' => $request['price'],
                'stock' => $request['stock'],
                'shipping' => $request['shipping'],
                'shipping_method' => $request['shipping_method'],
                'shipping_origin' => $request['shipping_origin'],
                'shipping_estimated' => $request['shipping_estimated'],
                'shipping_cost' => $request['shipping_cost'],
            ]);

            // 並び順の要素数を初期化する
            $index = 0;

            // 対象画像が存在する場合、画像登録処理を行う
            if($images != null) {

                foreach ($images as $image) {

                    // 投稿写真の拡張子を取得する
                    $extension = $image->extension();

                    $productImage = new ProductImage();

                    // インスタンス生成時に割り振られたランダムなID値と
                    // 本来の拡張子を組み合わせてファイル名とする
                    $fileName = $product->id . $product->shop_id . $this->getRandomFileName() . '.' . $extension;

                    // S3にファイルを保存する
                    // 第三引数の'public'はファイルを公開状態で保存するため
                    Storage::cloud()->putFileAs('', $image, $fileName, 'public');

                    // S3にアップロードしたファイルのURLをDBに保存する
                    $productImage->product_image = Storage::cloud()->url($fileName);
                    $imagePaths[] = $productImage->product_image;

                    // 一時保存するためのファイル名とファイルパスを生成する
                    $now = date_format(Carbon::now(), 'YmdHis');
                    $tmpFile = $now . '.' . $extension;
                    $tmpPath = storage_path('app/tmp/') . $tmpFile;

                    // 画像を横幅300px・縦幅アスペクト比維持の自動サイズへリサイズ
                    $image = Image::make($image)
                        ->resize(300, null, function ($constraint) {
                            $constraint->aspectRatio();
                        })
                        ->save($tmpPath);

                    // サムネイルを作成し、S3を保存する
                    $thumbnailFileName = 'thumbnail_' . $fileName;

                    // configファイルに定義したS3のパスへ指定したファイル名で画像をアップロード
                    Storage::cloud()->putFileAs('', new File($tmpPath), $thumbnailFileName, 'public');

                    // S3にアップロードしたファイルのURLをDBに保存する
                    $productImage->product_thumbnail_image = Storage::cloud()->url($thumbnailFileName);
                    $imagePaths[] = $productImage->product_thumbnail_image;

                    // 一時ファイルを削除
                    Storage::disk('local')->delete('tmp/' . $tmpFile);

                    // 商品IDと表示順を設定する
                    $productImage->product_id = $product->id;
                    $productImage->order_number = $order[$index];
                    $productImage->save();

                    $index++;

                }

            }

            DB::commit();

        } catch (\Exception $exception) {
            DB::rollBack();

            foreach ($imagePaths as $imagePath) {
                // DBとの不整合を避けるためアップロードしたファイルを削除
                Storage::cloud()->delete($imagePath);
            }

            throw $exception;
        }

        return response()->json([
            'success' => true,
            'data' => $product,
            'message' => '商品の登録が完了しました。',
        ], 201);


    }
7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?