質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.59%

Laravelでs3を使って写真を保存できない

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 1,022

score 56

前提・実現したいこと

Laravel + Vue勉強中の初心者です。

https://www.hypertextcandy.com/vue-laravel-tutorial-submit-photo/
このチュートリアルをやっています。

Postすると、500のエラーが帰ってきます。

awsのkeyとid,バケット名は一致しています。

コントローラーでの処理がおかしいのかなと。。。自分では解決出来ませんでした。

発生している問題・エラーメッセージ

POST http://127.0.0.1:8000/api/photos 500 (Internal Server Error)

{message: "Can only throw objects", exception: "Symfony\Component\Debug\Exception\FatalThrowableError",…}
exception: "Symfony\Component\Debug\Exception\FatalThrowableError"
file: "/Users/hoge/Desktop/phps/insta/app/Http/Controllers/PhotoController.php"
line: 53
message: "Can only throw objects"
trace: [{function: "create", class: "App\Http\Controllers\PhotoController", type: "->"}, {,…}, {,…}, {,…},…]

該当のソースコード

コントローラー

<?php

namespace App\Http\Controllers;

use App\Photo;
use App\Http\Requests\StorePhoto;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

class PhotoController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * 写真投稿
     * @param StorePhoto $request
     * @return \Illuminate\Http\Response
     */

     public function create(StorePhoto $request)
     {
         $extension = $request->photo->extension();

         $photo = new Photo();

        // インスタンス生成時に割り振られたランダムなID値と
        // 本来の拡張子を組み合わせてファイル名とする

        $photo->filename = $photo->id . '.' . $extension;

        // S3にファイルを保存する
        // 第三引数の'public'はファイルを公開状態で保存するため
        // putFileAsは指定したファイル位置のファイルのストリーミングを自動的にLaravelに管理できる
        // cloud() を呼んだ場合は config/filesystems.php の cloud の設定にしたがって使用されるストレージが決まります。
        Storage::cloud()->putFileAs('', $request->photo, $photo->filename, 'public');

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

        try{
            Auth::user()->photos()->save($photo);
            DB::commit();
        } catch (\Exception $exception) {
            DB::rollBack();
            // DBとの不整合を避けるためアップロードしたファイルを削除
            Storage::cloud()->delete($photo->filename);
            throw $extension;
        }

        // リソースの新規作成なので
        // レスポンスコードは201(CREATED)を返却する
        return response($photo, 201);
     }
}
フォームリクエスト

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePhoto 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 [
            'photo' => 'required|file|mimes:jpg,jpeg,png,gif'
        ];
    }
}
Photoモデル


<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Photo extends Model
{

    /** プライマリキーの型 */
    protected $keyType = 'string';

    /** IDの桁数 */
    const ID_LENGTH = 12;

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);

        if(! array_get($this->attributes, 'id')) {
            $this->setId();
        }
    }
     /**
     * ランダムなID値をid属性に代入する
     */
    private function setId()
    {
        $this->attributes['id'] = $this->getRandomId();
    }
     /**
     * ランダムなID値を生成する
     * @return string
     */
    private function getRandomId()
    {
        $characters = array_merge(
            range(0, 9), range('a', 'z'),
            range('A', 'Z'), ['-', '_']
        );

        $length = count($characters);

        $id = "";

        for($i = 0; $i < self::ID_LENGTH; $i++) {
            $id = $characters[random_int(0, $length - 1)];
        }

        return $id;
    }
}
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
    * リレーションシップ - photosテーブル
    * @return \Illuminate\Database\Eloquent\Relations\HasMany
    */
    public function photos()
    {
        return $this->hasMany('App\Photo');
    }
}
PhotoForm.vue

<template>
  <div v-show="value" class="photo-form">
    <h2 class="title">Submit a photo</h2>
    <div v-show="loading" class="panel">
      <Loader>Sending your photo...</Loader>
    </div>
    <form v-show="! loading" class="form" @submit.prevent="submit">
      <div class="errors" v-if="errors">
        <ul v-if="errors.photo">
          <li v-for="msg in errors.photo" :key="msg">{{ msg }}</li>
        </ul>
      </div>
      <input class="form__item" type="file" @change="onFileChange">
      <output class="form__output" v-if="preview">
        <img :src="preview" alt="">
      </output>
      <div class="form__button">
        <button type="submit" class="button button--inverse">submit</button>
      </div>
    </form>
  </div>
</template>

<script>

import {CREATED, UNPROCESSABLE_ENTITY } from '../util'

import Loader from './Loader.vue' 

export default {
  components: {
    Loader
  },
  // バリューを受け取れるようにpropsを追加
  props: {
    // バリュは表示/非表示の真偽値で表現するためBoolean型を使用する
    value: {
      type: Boolean,
      required: true
    }
  },
  // beforeSend: function (xhr) {
  //             return xhr.setRequestHeader('X-CSRF-TOKEN', "{{csrf_token()}}");
  //         },
  data() {
    return {
      loading: false,
      preview: null,
      photo: null,
      errors: null
    }
  },
  methods: {
    onFileChange(event) {
      // 何も選択されていなければ処理中断
      if(event.target.files.length === 0) {
        this.reset()
        return false
      }
      // ファイルが画像ではなかったら処理中断
      if(! event.target.files[0].type.match('image.*')) {
        this.reset()
        return false
      }

      // FileReaderクラスのインスタンスを取得
      const reader = new FileReader()

      // FileReaderクラスのインスタンスを取得
      reader.onload = e => {
      // previewに読み込み結果(データURL)を代入する
      // previewに値が入ると<output>につけたv-ifがtrueと判定される
      // また<output>内部の<img>のsrc属性はpreviewの値を参照しているので
      // 結果として画像が表示される
      this.preview = e.target.result
      }

      // ファイルを読み込む
      // 読み込まれたファイルはデータURL形式で受け取れる(上記onload参照)
      reader.readAsDataURL(event.target.files[0])

      this.photo = event.target.files[0]
    },
    reset() {
      this.preview = '',
      this.photo = null,
      // $elはコンポーネントそのものの DOM 要素を指している
      this.$el.querySelector('input[type="file"]').value = null
    },
    async submit() {
      // 送信した時にローディングを表示させる 
      this.loading = true
      const formData = new FormData()
      formData.append('photo', this.photo)
      const response = await axios.post('/api/photos', formData)

      this.loading = false

      // バリデーションエラーで帰ってきた場合422
      if(response.status === UNPROCESSABLE_ENTITY) {
        this.errors = response.data.errors
        return false
      }

      this.reset()
      this.$emit('input', false)

      if(response.status !== CREATED) {
        this.$store.commit('error/setCode', response.status)
        return false
      }

      this.$store.commit('message/setContent', {
        content: '写真が投稿されました',
        timeout: 6000
      })

      this.$router.push(`/photos/${response.data.id}`)
    }
  }
}
</script>

試したこと

photoモデルにbelongsToをつけてみたりしました。(チュートリアルには無い)変わらず。

eval(\Psy\sh());を使って値が取れているのか確認してみた。

aws関係のkeyやid、バケット名を確認しましたがあっていると思います。

どなたかご教授よろしくおねがします。

補足情報(FW/ツールのバージョンなど)

Laravel 5.8.36

PHP 7.1.23

Vue 2.6.11 

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • crhg

    2019/12/27 13:40

    PhotoControllerの例外処理のところで例外を$exceptionという変数で受けているのにthrows $extension;と変数名を間違えているので、そこでcatchされたのがどんな例外だったのかがわからなくなってしまっています。(かわりに例外でない変数をthrowしようとしているので、それがエラーとして表示されてしまっています) とりあえずそれを直してもう一度やりましょう。その例外の中身が大事な手がかりなのです。

    キャンセル

  • coco_bauer

    2019/12/27 17:25

    僕も、"throw $extension;"を"throw $exception;"に書き直せば済む話のように思います。単なるタイプミスで、ややこしげに見えるエラーメッセージが出ただけ。

    キャンセル

  • tenlife

    2019/12/29 23:26

    お二人とも回答ありがとうございます。おっしゃる通り変数の書き間違えでした。
    変数の処理の流れを負うべきでした。
    ありがとうございました!

    キャンセル

回答 1

check解決した方法

0

お二人のおかげ解決することができました、
ありがとうございました

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.59%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る