今回は前回に引き続いて写真複数枚投稿の実装を進めていきます。今回は更新処理になります。
コントローラーとかモデル、ルートとか重複している部分についてはそのまま使用するので前回の記事を見ていない人は是非。
今回メインなのはコンポーネントとコントローラーのupdateアクションです。
目次
前提
写真複数枚の新規投稿は出来る状態。新規と同じ考え方の部分は説明省いています。
どこまで実装するか?
複数枚の同時更新が出来る。
新しく追加した写真が混ざっていても追加処理できる。
写真の順番は変わらない。
バリデーションで戻って来ても元々ある値と、新しく追加した値の両方を保持できる。
blade側
<image-edit :old_images="{{ json_encode(Session::getOldInput()) }}" :edit_images="{{$item->images}}" :errors= "{{ $errors }}"></image-edit>
選択されていた値の保持、既に保存されている値の取得、エラーメッセージの取得をしています。これらをコンポーネント側に送っています。
コンポーネント側
<template>
<div class="mb-3">
<div class="form-group row">
<div class="container row my-5">
1 <div v-for="(image, index) in images" :key="index" class="mx-1">
2 <img v-if="image.image_path" :src="image.image_path" style="width: 100px; height: 101px;">
3 <img v-else :src="image" style="width: 100px; height: 102px;">
<input type="hidden" v-model="image.name">
<div class="row col mx-auto" style="width: 100px;">
<div class="mr-1">
<label :for="['edit_img_' + index]" style="cursor: pointer;">編集</label>
<input type="file" @change="editImage(image, index, $event)" :id="['edit_img_' + index]" hidden>
</div>
<div class="ml-1" @click="remove(image, index, $event)" style="cursor: pointer;">削除</div>
</div>
</div>
</div>
<label for="title" class="col-md-4 col-form-label text-md-right">画像<span class="ml-2" style="color: red; font-size: .7rem;">必須</span></label>
<div class="col-md-6">
<label class="text-center mb-0" for="title" style="cursor: pointer;">クリックで画像追加(複数枚可能)</label>
4 <span v-if="image_errors && images == []" class="invalid-feedback" role="alert" style="display: block;">
<strong>
{{image_errors[0]}}
</strong>
</span>
<input @change="selectImage" id="title" type="file" class="form-control" hidden>
<input type="hidden" name="images" :value="images">
5 <input type="hidden" name="edit_images" :value="edit_image_list">
6 <input type="hidden" name="remove_images" :value="remove_image_list">
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
images: [],
image_errors: null,
edit_image_list: [],
remove_image_list: []
}
},
props: ['old_images', 'errors', 'edit_images'], 7
created() {
if(this.errors != []) {
this.image_errors = this.errors.images
}
// まずはじめに初期の値を代入
this.images = this.edit_images 8
// バリデーションではね返された時の処理。
// 変更した写真があるかどうか。
if(this.old_images.edit_images != undefined) { 9
let test = this.old_images.edit_images.split(/(?<!data:image\/(jpeg|png);base64),/);
// undefinedを除いて、新しい配列を作成。
let newtest = test.filter(val => val != undefined) 10
let list = [];
for(let i=0; i<newtest.length; i+=2) list.push(newtest[i]+","+newtest[i+1]); 11
// jsonオブジェクトの配列の箱を用意する
let json_list = [];
// jsonオブジェクトの形に変更する。変更されたものが複数あると対応できていないので複数にも対応する様に書き換える。
list.forEach(function(ele) { 12
var obj = JSON.parse(ele); 13
json_list.push(obj)
})
let image_list = this.images
let image_list2 = this.old_images
json_list.forEach(function(obj) { 14
// edit_idがある場合、つまり写真変更の場合
if(obj.edit_id) {
// image_listの中にある、変更されるimageのidと一致するもののindex番号を取得
const index = image_list.findIndex(val => val.id == obj.edit_id) 15
// 一旦spliceで置き換えている。
// ここなんかリファクタ出来そうな感じ。splice2回も使う必要なさそう?。
const x = image_list.splice(index, 1, obj); 16
// 置き換えたxの情報に変更で追加された写真を代入する。
x[0] = obj.base64 17
// 完成したxをimage_listに元のindex番号に対して代入する
image_list.splice(index, 1, x[0]); 18
}
})
// 変更した写真もあるが、新しく追加した写真もある時に、二重にthis.imagesに追加されるのを防いでいる。
if(this.old_images.images != undefined ) { 19
// 新しく追加した写真を正規表現で分けて取得
const base64_list = this.old_images.images.split(/(?<!data:image\/(jpeg|png);base64),/); 20
base64_list.forEach((ele) => { 21
if(ele != undefined && ele.slice(0,10) == 'data:image') { 22
// 新しく追加された写真だと判定出来たら配列に追加する。
this.images.push(ele)
}
})
}
// 変更される写真リストを追加する。
this.edit_image_list = this.old_images.edit_images 23
}
// 変更した写真は無いが、新しく追加した写真がある場合。
if(this.old_images.images != undefined && this.old_images.edit_images == undefined) { 24
const base64_list = this.old_images.images.split(/(?<!data:image\/(jpeg|png);base64),/); 25
base64_list.forEach((ele) => { 26
if(ele != undefined && ele.slice(0,10) == 'data:image') { 27
this.images.push(ele)
}
})
}
// 最後に重複しているものが無いか確認している。https://qiita.com/netebakari/items/7c1db0b0cea14a3d4419
this.images = Array.from(new Set(this.images)); 28
},
methods: {
selectImage(e) {
this.createImage(e.target.files[0])
},
createImage(file) {
if(file) {
const reader = new FileReader();
reader.onload = (e) => {
this.images.push(e.target.result)
}
reader.readAsDataURL(file)
}
},
editImage(edit_image, i, e) {
// 写真が変更されたら
if(edit_image, i, e) {
// 変更前の写真のbase64データを取得。validate前後で値が入っている部分が違う。
// constだとifの中でしか使えなくなるので注意
if(edit_image) {
var before_base = edit_image
}
let test = edit_image
// 新しい写真データを元のデータに追加。入れ替え。
edit_image = e.target.files[0]
const reader = new FileReader();
reader.onload = (e) => {
// imagesの中に入っているbase64達を取り出し。imagesは送信する情報。
this.images.forEach((element, index) => {
// 取り出した値が、before_baseと一致しているか?
// 送信する情報の中で変更するものを取得。
if(element == before_base) {
// 一致したら新しい写真のbase64に入れ替える。indexはindex番号を振る事。
this.images.splice(index, 1, e.target.result)
const edit_image = { 29
base64: e.target.result,
edit_id: test.id
}
// jsオブジェクトだと[object Object]になってしまうのでJSON.stringifyを実行
const hi = JSON.stringify(edit_image); 30
this.edit_image_list.push(hi)
}
});
}
reader.readAsDataURL(edit_image)
}
},
remove(img, i, e) {
if(img) {
this.remove_image_list.push(img.id) 31
}
this.images.splice(i, 1)
}
}
}
</script>
1. 新規の時と同じくeachで回し、値とindexを取得しています。
2. 既に保存されている写真はカラム名を指定して表示します。
3. 新しく追加された写真や変更された写真などは新規と同じ表示の仕方です。
4. エラーメッセージの表示になります。若干違うのが、imagesが空の状態であればエラーメッセージを表示させる点です。こちらに関しては好みで外してもらっても特に問題ないかと。
5. 変更された写真のリストが入っています。中には編集される写真のidと新しく追加される写真のbase64情報が入っています。
6. 削除される写真の情報です。こちらは削除される写真のidだけが入っています。
7. blade側から既に登録されている写真、バリデーション時のエラーメッセージ、選択された値の取得を行なっています。
8. 最初に元々保存されている写真の値をimagesに格納していきます。
9. 変更した写真があるかどうかをチェックしています。存在した場合はsplit()メソッドを使って正規表現区切りで値を取得していきます。
10. 取得した値の中にundefinedが入ってしまうので、それをfilter()メソッドで取り除いたものを新しく配列で定義します。
11. newtestの中に入っている値をfor文で回して、出てきた値を連結してlistに新しく追加しています。写真をプレビュー表示出来るようにと、写真データとしてコントローラーに送られた際に扱えるようにしています。
12. listの中身をforeachで取り出し、jsonオブジェクトの形に変換しています。
13. 12で取得した値をJSON.parse()メソッドで形をjsonオブジェクトに変換し、配列に代入しています。こうする事でコントローラー側でも値を扱えるようになります。
14. 13で作成したjson_listの配列を再度foreach()してedit_idがあるかチェックし、ある場合は取得して値を代入していく。
15. image_listの中にあるidとedit_idが一致するものを取得している。
16. image_listの中にあるidとedit_idが一致するものを取り出してsplice()で置き換えている。xと定義して17の処理で使用する。
17. x[0]に対して写真データだけを代入する。(base64データ)
18. 17で作成したbase64データをimage_listに元の写真データと置き換える形で代入する。
19. 新しく追加した写真がある時の処理を実行します。
20. 新しく追加した写真があった場合はsplit()で正規表現で区切り配列に入れていく。
21. 20で作成した配列をeachで回していきます。
22. slice()メソッドで最初の10文字がdata…..であれば新しく追加された写真のbase64データとしてimagesに追加していく。
23. 変更される写真の情報は値を取得出来ているのでそのまま代入していきます。
24. 新しく追加した写真だけがある場合の処理になります。
25. 今まで通りsplit()メソッドで使える値に絞っていきます。使う値は配列にまとめます。
26. 25で作成した配列をeachで回していきます。
27. 22とほぼ同じになります。
28. Array.from(new Set())で最終的にimagesの中に重複している値はないのかチェックしていきます。重複しているものがあれば弾いてくれます。最後にimagesに代入し、viewで使用されていきます。
29. 編集する写真の情報はbase64データだけではなく、どの写真を変更処理するのかを判断するためのedit_idを与えます。
30. 29で作成した値をJSON.stringify()メソッドを使用してjson文字列に変換します。
31. 削除する写真についても既に保存されている写真を削除する場合に備えてidを取得しコントローラーに送るように設定しています。
コントローラー側
if($request->has('images')) {
$images = [];
// それぞれカンマもしくわ},でわけて取得している。
$image_list = explode(',', $request->images); 1
$list = explode('},', $request->edit_images); 2
// 取得した値を整えて、編集された写真は新しい配列に入れている。
// これをしないとjson_decode出来なくなる。
$ckeck_list = [];
if($list[0] != "") {
foreach($list as $el) { 3
if($el[-1] != '}') { 4
$el = $el.'}'; 5
array_push($ckeck_list, $el); 6
} else {
array_push($ckeck_list, $el);
}
}
}
// 編集した写真の有無。無い場合はstoreと同じ処理を実行。
if($ckeck_list != []) {
// 新しく追加した写真と編集した写真がある場合に比較するための配列を用意。
$checked_list = [];
// 編集する写真を取得。
foreach($ckeck_list as $edit_image) {
// 中身を取り出せる様にdecodeする。
$record = json_decode($edit_image, true);
// $recordの中身から写真をの情報だけを切り取る。
$rerecord = explode(',', $record['base64']); 7
// 編集する写真のidの有無の確認。
if(isset($record['edit_id'])) { 8
// 先ほど用意した配列に追加していく。これは新しい写真を保存する時に二重で保存されるのを防ぐため。
array_push($checked_list, $rerecord[1]); 9
// 配列の1に写真の情報が入っているので、decodeしていく。
$fileData = base64_decode($rerecord[1]); 10
// path_makeを呼び出しpathを取得。
$read_temp_path = $this->path_make($fileData);
// 拡張子取得はpathinfoとかでも良さそう。https://www.flatflag.nir87.com/basename-844
$extension = \File::extension($read_temp_path);
// edit_imageで写真編集し、配列に追加。https://qiita.com/kazu56/items/6947a0e4830eb556d575
11 $result = $this->edit_image($item, $record, $read_temp_path, $extension);
$images = array_merge($images, $result);
}
}
// 新しく追加した写真と、変更した写真がある場合。
foreach($image_list as $image) {
12 if($image != 'data:image/png;base64' && $image != '[object Object]') {
// 編集する写真と被っていないかをチェック。https://www.javadrive.jp/phpfunc/array/index5.html
13 $key = in_array($image, $checked_list);
// falseだった場合はそのままstoreと同じ流れで保存。
if($key == false) {
$fileData = base64_decode($image);
// path_makeを呼び出しpathを取得。
$read_temp_path = $this->path_make($fileData);
$extension = \File::extension($read_temp_path);
// new_imageで写真作成し、配列に追加。
$result = $this->new_image($read_temp_path, $extension);
$images = array_merge($images, $result);
}
}
}
} else {
// 新しい写真だけの場合。storeと同じ。
foreach($image_list as $image) {
$fileData = base64_decode($image);
// path_makeを呼び出しpathを取得。
$read_temp_path = $this->path_make($fileData);
$extension = \File::extension($read_temp_path);
// new_imageで写真作成し、配列に追加。
$result = $this->new_image($read_temp_path, $extension);
$images = array_merge($images, $result);
}
}
$item->images()->saveMany($images); 14
}
if($request->has('remove_images')) { 15
$list = explode(',', $request->remove_images); 16
foreach($list as $delete_image_id) {
$delete_id = (int)$delete_image_id; 17
foreach($item->images as $img) {
if($img->id == $delete_id) {
$img->delete(); 18
}
}
}
}
// 新規と全く同じ
private function path_make($fileData)
{
$tmpFilePath = sys_get_temp_dir() . '/' . Str::uuid()->toString();
file_put_contents($tmpFilePath, $fileData);
$tmpFile = new File($tmpFilePath);
$file = new UploadedFile(
$tmpFile->getPathname(),
$tmpFile->getFilename(),
$tmpFile->getMimeType(),
0,
true
);
$path = $file->store('public');
$extension = \File::extension($path);
$read_temp_path = str_replace('public/', '/storage/', $path);
return $read_temp_path;
}
private function edit_image($item, $record, $read_temp_path, $extension)
{
$images = [];
foreach($item->images as $img) {
if($img->id == $record['edit_id']) { 11-1
if($extension == 'png'){
$read_temp_path = str_replace('.txt', '.png', $read_temp_path);
$img->image_path = $read_temp_path;
array_push($images, $img);
} elseif($extension == 'jpeg') {
$read_temp_path = str_replace('.txt', '.jpeg', $read_temp_path);
$img->image_path = $read_temp_path;
array_push($images, $img);
} elseif($extension == 'jpg') {
$read_temp_path = str_replace('.txt', '.jpg', $read_temp_path);
$img->image_path = $read_temp_path;
array_push($images, $img);
} elseif($extension == 'gif') {
$read_temp_path = str_replace('.txt', '.gif', $read_temp_path);
$img->image_path = $read_temp_path;
array_push($images, $img);
}
}
}
return $images; 11-2
}
1. explode()メソッドを使用してカンマ区切りで値をわけて取得している。
2. edit_imagesに入っている値はjson文字列なので},で区切り取得します。
3. $list[0]が空でない場合は、$listをeachで回していく。
4. json文字列をjsonオブジェクトの形にするため、},を持っていないものを見つけている。
5. 4で取得したものに},を付け加えていく。
6. 5で正しい形(jsonオブジェクト)にしたものを$check_listの配列に代入していく。
7. カンマを除いて写真だけの情報を取得しています。
8. $recordの中にedit_idつまり編集される写真なのかをチェックしている。
9. 写真の情報が入っている$rerecord[1]から写真のデータを取得し、あとで使用する為に配列に格納している。
10. 9で取得したものと同じですが、こちらはedit処理に対して使っていく
11. 写真の情報を更新してデータベースに保存する値が入った配列を定義する。
11-1. edit_idを含んでいるかチェックしています。
11-2. 値を変更した写真が入っている配列を返り値として返します。
12. eachで回して取得した値が求めている値かどうかをチェックしています。
13. 9で設定した編集用の写真がin_array()メソッドを使用して、こちらに入っていないか重複チェックをしています。
14. 新規と同じく保存の処理をsaveManyを使い$itemに紐づく形で行います。
15. 削除する写真のリストを取得します。
16. カンマで区切り配列に追加していきます。
17. 削除する写真のidですが(int)をつけて整数で扱うようにしています。
18. idが$itemの中に一致するものがあれば削除します。
課題
同じ写真を選択出来てしまう問題。これは保存されているパスとbase64をどうにかして比較する必要がありそうと考えています。(長くなりそうなので今回はパスしましたw)お分かりになる方居ましたら教えてください〜!!!
まとめ
コンポーネント側は値を変換してコントローラーで扱えるようにしたり、プレビュー表示出来る形に変化させる処理が必要。
また上記の処理が元々保存されている写真、新しく追加された写真、変更された写真で分けて処理しているためコードが長くなる。
コントローラー側でも飛んでくる値が変更されたデータと、新しく追加されたデータで違う部分があるのでその部分を取り出し、写真データとして扱えるようにする処理が必要になってきます。
また、新規と更新で同じ写真を扱ってしまうと同じ写真が2回保存されてしまうので避ける処理も必要です。
こんな感じでしょうか。。。
更新処理となると、元々の写真、新しく追加した写真、変更した写真などが出て来て、バリデーションで戻って来た際の処理が複雑になったり、コントローラーで写真データとして扱える部分を取得する作業が増えてしまう印象でした。
作業していて、そもそもbase64で飛ばしているから処理が長くなっているのか?と思いながらやっていましたw
今回はこんな感じで!
以上!