【Laravel×Vue.js】form要素から画像をアップロードする方法!(input type=file)
この記事からわかること
- Laravel×Vue.jsを使ったアプリ開発での問題解決
- 画像をアップロードする方法
- input要素の使い方とJavaScriptでのformデータのPOST送信
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
LaravelとVue.jsを使ってSPA(Single page application)を作成中、ユーザー機能実装時に画像アップロード機能が欲しくなりました。組み込もうとしていたら意外と苦戦したので記事にまとめておきます。
Laravel×Vue.jsアプリで画像をアップロードする手順
細かい詳細、説明は一度すっ飛ばして実装したコードと記述すべきファイルと構造も含めて先に載せておきます。
今回記述する場所
- form.vue(Vue.js)
- web.php(Laravel)
- CRUDController.php(Laravel)
- ターミナル(Mac)
form.vue:input要素部分(Vue.js側template内)
<template>
<form>
<!-- csrf対策 Vuex.state.csrf -->
<input type="hidden" name="_token" :value="$store.state.csrf">
<input type="text" name="name" v-model="name">
<input type="file" @change="onCheckUploaded">
<input type="file" @change="onCheckUploaded">
<!-- 必要であればその他のinput要素 -->
<input type="submit" :onclick="uploadImage">
</form>
</template>
form.vue:バリデーション部分(Vue.js側method内)
export default {
name: 'upload-form',
data() {
return {
err:{
name:false,
image:false,
},
name:'',
image:'',
},
methods: {
onCheckUploaded(e) {
// event(=e)から画像データを取得する
this.image = e.target.files[0];
let size = this.image.size,
type = this.image.type;
// 2MBまで
if (size > 2000000) {
this.err.image = true;
}
if (type != 'image/jpg' && type != 'image/jpeg' && type != 'image/png' && type != 'application/pdf') {
this.err.image = true;
}
},
};
form.vue:非同期POST送信部分(Vue.js側method内)
uploadImage() {
// エラーがない時のみ処理を行う
if(!this.err.name && !this.err.image){
// ヘッダー定義
const config = {
headers: {
'content-type': 'multipart/form-data'
}
};
// Formデータ作成
const formData = new FormData();
formData.append("file",this.image);
formData.append("name",this.name);
// POST送信
axios
.post("/upload",formData , config)
.then((res) => {
// テストのため
console.log(res);
})
.catch((err) => {
console.log(err);
});
}
},
web.php:CRUDルーティング部分(Laravel)
Route::post('/upload', 'App\Http\Controllers\CRUDController@upload');
CRUDController.php:画像保存部分(Laravel)
public function upload(Request $request)
{
// formDataからfileを取り出す
$image =$request->file;
// storeAs→名前を変更して保存して保存先のパスを返す
$imagePath = $image->storeAs('public/uploads', 'user-name-'. $request->input('name').'.png');
// HTML出力用の整形とパスの修正
$imagePath= 'storage'.str_replace('public','',$imagePath);
}
}
ターミナル:シンボリックリンクを貼る
$ cd public
$ php artisan storage:link
Laravel×Vue.jsで画像アップロード機能を実装するための大まかな流れは以下のとおりです。
- HTML側で画像選択部分を実装
- JavaScriptで選択されたファイルの拡張子やサイズをチェック
- axiosを使って非同期でPOST送信(formデータにファイルを載せる)
- Laravel側のルーティング処理
- コントローラーに保存処理を実装
- シンボリックリンクを貼る
以下は上記のコードの解説をしていきます。詳しく知りたい方や仕組みを理解したい方は是非読んでみてください。
【HTML】画像を選択できるinput要素
まず基本的な知識としてHTMLで画像をアップロードする場合の方法を整理しておきます。ここでキーになってくるのはinput要素
です。
input要素はtext
やemail
など様々なtype属性
を指定できます。type属性の中にfile
があるのでtype="file"
と指定するだけで簡単に以下のようなファイル(画像)選択アップロード機能を実装することができます。
HTMLではユーザーから指定された画像を選択→データとして保持までしか実装できないので指定の場所に保存やformデータとして操作するのはPHPやJavaScript部分での実装になります。
ですがその前に指定されたファイルがちゃんと正しいものであるかJavaScriptでチェックします。
アップロードファイルのバリデーション
バリデーションとはデータが正しいものであるかチェックすることです。ここではアップロードされたファイルが「画像形式のファイルであるか」と「サイズが大きすぎないか」をチェックしています。
まずVue.jsのv-on(省略形の@)
でchangeイベントを取得します。これで input要素にファイルが選択されたタイミングで処理を発火させることができます。
onCheckUploaded(e) {
// event(=e)から画像データを取得する
this.image = e.target.files[0];
let size = this.image.size,
type = this.image.type;
// 2MBまで
if (size > 2000000) {
this.err.image = true;
}
// 許可する拡張子のタイプを指定する
if (type != 'image/jpg' && type != 'image/jpeg' && type != 'image/png' && type != 'application/pdf') {
this.err.image = true;
}
},
定義したメソッドの引数からイベントを取得できます。イベントの中のtarget→ファイル配列の0番目
に該当の画像データが格納されていますので変数に格納しておきます。
変数に格納した画像データのプロパティに画像タイプ(拡張子)やサイズ、ファイル名などが紐付いています。
あとはそれぞれの許容する値と比較し、良ければそのまま、反していればエラーフラグを立てるなどの分岐処理をしておけばバリデーションは完了です。
formDataオブジェクトを使って生成
POST送信の前にフォームデータを構築します。
今回はform要素内に入力されたデータに対してバリデーションを挟んだり、入力値以外のデータも追加して送信したいのでフォームデータを1から構築します。バリデーションやデータの拡張がなければフォームデータをそのまま取得する方法でも行けると思います。
フォームデータを定義するには2つ方法があります。
- ただの連想配列(オブジェクト)として定義する方法
- JavaScriptに備わっているformオブジェクトを使って定義する方法
1つ目の方法は以下のように普通に連想配列(オブジェクト)を定義するだけです。これでキー値と値の組み合わせのデータがアクション先のURLにPOSTされます。
axios
.post("/test", {
// キー値 : データ本体
user_id: this.userId,
board_id: this.boardId,
})
2つ目はJavaScriptに備わっているformオブジェクトを使用した方法です。formオブジェクトを使う場合でもキー値と値の組み合わせを持ったオブジェクトを作ると言う流れは変わりません。ただ先程は一気に羅列して定義していたオブジェクトですがformオブジェクトを使う場合はインスタンス化した後に1つずつappendメソッドを使用してデータを追加していきます。
// formオブジェクトのインスタンス化
const formData = new FormData();
// データをオブジェクトに追加
formData.append("file",this.image);
formData.append("name",this.name);
インスタンス化したformオブジェクトの中身を確認したい場合は以下のコードで確認できます。
// 中身確認用
for (let value of formData.entries()) {
console.log(value);
}
formオブジェクトを使うメリットとして簡単に通常のinput要素などの入力値を取得することができます。以下のようにform element
を取得し、インスタンス化時の引数に渡すだけでデータが格納されます。バリデーションや拡張がない場合はこれだけで行けると思います。
const formElement = document.querySelector("form");
const formData = new FormData(formElement);
axiosを使ってPOST送信
次に非同期のHTTP通信を簡単に行えるJavaScriptライブラリaxiosを使ってPOST送信を実現します。Laravel/uiで開発ベースを構築していた場合はデフォルトで組み込まれているのでインストールやimportする手間は必要ありません
axiosについてはこちらの記事を参考にしてください。
今回のaxiosでは3つの引数に値を渡します。
axios.post("/upload",formData , config)
axios.HTTPメソッド("アクション先URL",フォームデータ , 設定(コンフィグレーション))
1つ目はPOST送信するURL、2つ目は先程定義したデータ部分、3つ目はHTTPリクエストの設定情報です。フォームデータと設定はオブジェクト形式のデータを渡してあげます。
今回設定(config)に渡すのは以下の内容です。ファイルを送信したいのでHTTPヘッダのコンテンツの種類をmultipart/form-data
に指定します。コンテンツの種類は色々ありますがファイルやテキストなどがマルチな種類が扱えるのがmultipart/form-data
になります。
// ヘッダー定義
const config = {
headers: {
'content-type': 'multipart/form-data'
}
};
HTMLのform要素側に指定する時はこんな感じになるやつですね!
<form method="POST" action="/upload" enctype="multipart/form-data">
これでPOST送信部分は完了です。次にLaravel側で送信されたデータを保存する処理に移ります。
axios
.post("/upload",formData , config)
.then((res) => {
// テストのため
console.log(res);
})
.catch((err) => {
console.log(err);
});
},
web.phpでのルーティング設定
Laravel側ではまずPOST送信されるURLのルーティング設定をしておきます。
web.phpの中に以下のようなコードを追加します。URLは先ほどaxiosで指定したURLを、呼び出すコントローラー(アクションメソッド)は自分の好きな物を指定してください。今回はCRUDControllerのuploadメソッド
としておきます。
Route::post('/upload', 'App\Http\Controllers\CRUDController@upload');
Laravel側で画像を保存する処理
ここではやることは4つ
- リクエストを引数で受け取る
- リクエストの中から画像データを取り出す
- Laravel内の指定場所に保存する
- 必要であればデータベースにパスを保存する
まずはアクションメソッドの引数でユーザーとサーバー間のやり取り情報にアクセスできるrequest(もう一つはresponse)を受け取れるようにuse文を追加しておきます。
use Illuminate\Http\Request;
次に引数に指定した変数$request
の中からfile
を呼び出し変数$image
に格納します。変数$image
に対してstoreAsメソッドを使います。
storeAsメソッドは保存先の指定、ファイル名の変更、保存先パスの取得が1度に行える便利なメソッドです。
public function upload(Request $request)
{
// formDataからfileを取り出す
$image =$request->file;
// storeAs→名前を変更して保存して保存先のパスを返す
$imagePath = $image->storeAs('public/uploads', 'user-name-'. $request->input('name').'.png');
// HTML出力用の整形とパスの修正
$imagePath= 'storage'.str_replace('public','',$imagePath);
}
保存先には「storeフォルダ」の中が指定されているのでその中の保存したいパスを第一引数に指定します。今回は「store」>「public」>「uploads」というフォルダの中に格納していきます。もし指定したフォルダがない場合でも自動でフォルダが作成されます。
第二引数には画像ファイルの名前を設定できます。ここではPOSTされた別データ(ユーザ名)をファイル名にして、拡張子を決めうちで「.png」にしています。
storeAsメソッドは戻り値として保存先のパスを返します。データベースに保存するのは画像ファイルではなく、画像ファイルを保存したパスです。なのでこのパスをデータベースに保存していけばオッケーです。
画像のパスを保存する際に不要となるpublic部分はstr_replace
で置換しておきます。
シンボリックリンクを貼る
storeの中に格納された画像にアクセスしやすいようにLaravelプロジェクト内の「public」フォルダの中にシンボリックリンクを貼ります。
シンボリックリンクとはフォルダ同士を繋げる中継機のようなものです。リンクさせることでpublic内にstoreのpublicにアクセスできる中継ファイルが作られます。
シンボリックリンクを貼るにはコマンドライン(Macならターミナル)からコマンドを叩きます。
artisanコマンドでstoreフォルダへのシンボリックリンクを作成します。実行するディレクトリはプロジェクト直下で大丈夫です。
$ php artisan storage:link
以下のようなエラーが出た場合はコマンドを実行しているディレクトリが違う可能性があるのでプロジェクト直下(artisanがある階層)に移動して実行してください。
Could not open input file: artisan
Laravelプロジェクト内の「public」フォルダの中に以下のような矢印の入ったstorageフォルダができていれば成功です。
ターミナル上で確認するには「public」ディレクトリに移動して、$ ls -l
を実行すると確認できます。
$ cd public
// シンボリックリンクを確認
$ ls -l
合計 20
drwxr-xr-x 3 131 3月 1 21:04 css
-rw-r--r-- 1 0 2月 24 19:16 favicon.ico
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
lrwxrwxrwx 1 54 3月 2 21:04 storage -> ~/storage/app/public
これでVue側に実装したform要素から画像を送信してLaravel内に保存する一連の流れが出来ました。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。