ReactとLaravelでSPAを構築中です。
その中に必要な機能として、画像登録を実装しました。ユーザーが登録した画像をリサイズすることで、データサーバーの負担も軽減するようにしました。
ちなみに今回は、ユーザーのアイコン画像と名前を同時に登録するという前提です!
ReactコンポーネントからaxiosでLaravelにデータを渡す
import React, { Component } from 'react'
import axios from 'axios'
class UserProfile extends Component {
constructor() {
super()
this.state = {
img: '',
name:''
}
this.onChange = this.onChange.bind(this);
this.uploadFile = this.uploadFile.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
onChange(e) { //name用
this.setState({ [e.target.name]: e.target.value })
}
uploadFile(e) { //img用
console.log(e.target.files[0]); //File {name: "me.png", lastModified: 1578446578390, lastModifiedDate: Wed Jan 08 2020 13:16:19 GMT+0100 (中央ヨーロッパ標準時), webkitRelativePath: "", size: 319611, …}
this.setState({ img: e.target.files[0] })
console.log(this.state.img); //null
};
onSubmit(e) {
e.preventDefault()
const data = new FormData();
data.append('img', this.state.img ? this.state.img : ''); // nullのときに文字列の'null'が入るのを防ぐ
data.append('name', this.state.name ? this.state.name : '');
console.log(data); // FormData{}
console.log(data.get('img')); //File {name: "me.png", lastModified: 1578446578390, lastModifiedDate: Wed Jan 08 2020 13:16:19 GMT+0100 (中央ヨーロッパ標準時), webkitRelativePath: "", size: 319611, …}
return axios.post('api/editprofile', data).then(res => {
}).catch(err => {
console.log('失敗!')
})
}
render() {
return (
<form noValidate onSubmit={this.onSubmit} encType="multipart/form-data">
<input multiple
type="file"
name="img"
onChange={this.uploadFile} />
<input type="text"
name="name"
value={this.state.title || ''}
onChange={this.onChange} />
<button type="submit">Save</button>
</form>
)
}
}
export default UserProfile
FormDataオブジェクトを使ったものやらFileReaderオブジェクトを使ってbase64にエンコードさせたりやら試しに試しまくりました。
その中でずっっっっと詰まっていた原因は、このconsole.logで出していたnull(23行)やら空のFormData{}(30行)です。
ただ30行目に関しては、FormDataオブジェクトのデータ取得方法を知らなかったことが大きな原因でした。次の31行目のように、get(キー)でアクセスできます。
そして、これらのデータにLaravelからどうやってアクセスしたら良いのかわからず、結構な時間が溶けていきました。
setStateが実行されるのはすぐじゃないらしい
input[type=”file”]にファイルが選択されると、uploadFile()が実行されます。
15行目でe.target.files[0]に値が入ってることがわかってるのに、次のsetState後に確認してもnullが出てきました。
なんでも、setStateでstateが変更されるタイミングは即時ではないらしいのです。というか、いつかわからないっぽい。
その後、FormDataオブジェクトをnewしてappendし、getでアクセスすると中に入っているのがわかります。
Laravelで受け取った値が見たこともない形…
Laravelで画像アップローダーを作ったことはあるのですが、簡単すぎてdebugすらしていなかった気がします。
今回初めてdebugしてみて、見たこともない値にパニックになりました(笑)。
Log::debug($request);
//array (
// 'img' =>
// Illuminate\Http\UploadedFile::__set_state(array(
// 'test' => false,
// 'originalName' => 'me.png',
// 'mimeType' => 'image/png',
// 'error' => 0,
// 'hashName' => NULL,
// )),
// 'name' => 'Me'
//)
Log::debug($request->file('img'))
///private/var/folders/wn/ad9jFunmkgloibmfot3fOlkUip020gn/T/phpd4Pc5Ls
なにこれ?(笑)これどうやってvalidation通したりリサイズするんだろう……。
Reactから受け取ったデータをLaravelのコントローラーで処理する
普通にアクセスできました。
<?php
namespace App\Http\Controllers;
use App\User; // Userモデル
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Intervention\Image\Facades\Image; //このあと説明するintervention/image
class UserController extends Controller
{
public function editprofile(Request $request)
{
$user = // ユーザー情報はJWTを使用して取得しているため今回は省略
$validator = Validator::make($request->all(), [
'img' => 'nullable|file|image|mimes:jpeg,png,jpg,gif|max:2048',
'name' => 'required|string|max:255|',
]);
if ($validator->fails()) { //エラーがある場合
return response()->json($validator->errors(), 400);
}
if ($img = $request->file('img')) {
$image = Image::make(file_get_contents($img->getRealPath()));
// 画像のリサイズ
$image->resize(600, null, function ($constraint) {
$constraint->aspectRatio();
});
$path = public_path('storage/images/' . $img->hashName());
$image->save($path);
$user->img = basename($path);
}
$user->name = $request->name;
$user->save();
return response()->json(compact('user'));
}
}
(バリデーション のエラーメッセージを表示するには、Reactの方でもうひと工夫必要です。)
Reactで値が取得できていないことも頭にあって、勝手にすごく複雑に考えていたんですね。
$request->all()でいいんかい!!とツッコミました。普通やん……と。
imgは$request->file(‘img’)だから他のテキストとは別にバリデーションするのかな。
データを渡すときに画像とテキストを別の変数に入れて配列にして渡すか。
などなど、$request->all()は初めから使えないと思っていたので、色々試しつつ時間が流れていきました(笑)。いや、一回くらい試したのかな?そのときにnullの件で怒られてダメだと思ったのかも。
nullの件というのは、Reactの方の29行目をみていただきたいのですが、data.appendしているときにif文で ‘ ‘ を入れてますよね。
これをしないと画像を登録しないとき(this.state.imgがnullのとき)に、NULLじゃなくて ‘ null ‘ という文字列を入れられてしまうのです。
そうすると、文字列なので「ファイルじゃないよ!」とLaravelに怒られてしまいます。
はぁ〜〜〜…つらかった。
intervention/imageで画像をリサイズする
なぜ画像をリサイズするのか。それはユーザーが何も考えずにiPhoneで撮ったバカでかいファイルをアップロードするおそれがあるから。
もちろん初めからファイルの最大サイズを制限すればいいのですが、それって結構ユーザーにとってストレスだと思います。TwitterでもInstagramでも、アプリが勝手にリサイズしてくれるし、絶対その方が良いです。
というわけでintervention/imageを使って画像をリサイズします!
まずは準備から。
$ php composer.phar require intervention/image
'providers' => [
// 追記
Intervention\Image\ImageServiceProvider::class,
],
'aliases' => [
// 追記
'Image' => Intervention\Image\Facades\Image::class,
],
今回使用したintervention/imageのメソッドはresize()です。
$img->resize(300,300); // (width, height)
とすると、縦横300ピクセルの正方形にリサイズされます。
でも、ユーザーの登録する画像の縦横比なんてわかりません。勝手に正方形にしちゃったらビックリされますね。
そのため、縦横の比率を維持したままリサイズするようにします。
// widthを300に、heightを比率キープで自動リサイズ
$img->resize(300, null, function ($constraint) {
$constraint->aspectRatio();
});
// heightを200に、widthを比率キープで自動リサイズ
$img->resize(null, 200, function ($constraint) {
$constraint->aspectRatio();
});
もちろんこのままでは、元の画像の大きさが300以下の場合にも、300まで大きくなってしまいます。
そんなときは、widthメソッドで横幅・heightメソッドで縦幅を取得して、条件分岐でリサイズすることが可能です。
ちなみに、Laravelのstorage/app/public以下に保存した画像は、
$ php artisan storage:link
でpublicフォルダからシンボリックリンクをはらないと表示できません。忘れないようにしましょう。
参考公式サイト:Intervention Image
画像圧縮でおなじみのTinyPingにもAPIがあります。サイズはそのままで圧縮をしたいときに使うと良さそうです!