③では編集・削除機能の実装を行う。①、②は以下からどうぞ。
編集機能
CustomerController.phpのedit()メソッド
編集画面に顧客情報(customer
)を渡す。この処理においては、CustomerController
の edit
メソッド内で以下のように実装されている。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCustomerRequest;
use App\Http\Requests\UpdateCustomerRequest;
use App\Models\Customer;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function edit(Customer $customer)
{
return Inertia::render('Customers/Edit', [
'customer' => $customer
]);
}
}
このコードでは、編集対象の顧客データ($customer
)を Vue コンポーネントである Customers/Edit
に渡している。
なお、edit()
メソッドの処理に関しては、「開発メモ2」で解説した内容と同様。モデルのインジェクションを使用して、指定した customer
を自動的に取得し、編集画面に渡している。
Edit.vue作成
Edit.vue
を以下のように作成する。基本的な構造は Create.vue
と同じだが、編集時に必要な処理を追加している。
📝ポイント
defineProps
でcustomer
を受け取る
編集対象の顧客データを親コンポーネントやコントローラーから受け取り、それをローカルで使用する。customerForm
の初期値をprops
から受け取る
フォームデータの初期値として、customer
から受け取った情報を設定する。- HTTPメソッドの
put
を用いて送信する
更新処理にはPUT
メソッドを使用し、指定したルートにリクエストを送信する。
<script setup lang="ts">
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, useForm } from '@inertiajs/vue3';
import TextInput from '@/Components/TextInput.vue';
import TextArea from '@/Components/TextArea.vue';
import InputError from '@/Components/InputError.vue';
import { Core as YubinBangoCore } from "yubinbango-core2";
// customers のデータ構造を定義
type Customer = {
id: number;
last_name: string;
first_name: string;
last_name_kana: string;
first_name_kana: string;
postcode: string;
address: string;
tel: string;
birth: string;
gender: number;
memo: string;
};
const props = defineProps<{ customer: Customer }>();
const customerForm = useForm<Customer>({
id: props.customer.id,
last_name: props.customer.last_name,
first_name: props.customer.first_name,
last_name_kana: props.customer.last_name_kana,
first_name_kana: props.customer.first_name_kana,
postcode: props.customer.postcode,
address: props.customer.address,
tel: props.customer.tel,
birth: props.customer.birth,
gender: props.customer.gender,
memo: props.customer.memo,
})
const fetchAddress = () => {
new YubinBangoCore(String(customerForm.postcode), (value: any) => {
customerForm.address = value.region + value.locality + value.street
})
}
const updateCustomer = () => {
customerForm.put(route('customers.update', {
'customer' : customerForm.id
}))
}
</script>
<template>
<Head title="顧客編集" />
<AuthenticatedLayout>
<template #header>
<h2
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
>
顧客編集
</h2>
</template>
<div class="mb-10">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div
class="py-5 overflow-hidden bg-white shadow-lg sm:rounded-lg dark:bg-gray-800"
>
<v-container>
<v-row>
<v-col lg="3"></v-col>
<v-col md="12" lg="6" cols="12">
<form @submit.prevent="updateCustomer">
<InputError class="" :message="customerForm.errors.last_name" />
<InputError class="" :message="customerForm.errors.first_name" />
<div class="d-flex">
<TextInput
label="姓"
placeholder="例:山田"
id="last_name"
v-model="customerForm.last_name"
type="text"
icon="mdi-card-bulleted-outline"
class="block w-full"
required
autofocus
autocomplete="姓"
/>
<TextInput
label="名"
placeholder="例:太郎"
id="first_name"
v-model="customerForm.first_name"
type="text"
icon="mdi-card-bulleted-outline"
class="block w-full ml-2"
required
autocomplete="名"
/>
</div>
<InputError class="" :message="customerForm.errors.last_name_kana" />
<InputError class="" :message="customerForm.errors.first_name_kana" />
<div class="d-flex">
<TextInput
label="姓カナ"
placeholder="例:ヤマダ"
id="last_name_kana"
v-model="customerForm.last_name_kana"
type="text"
icon="mdi-card-bulleted-outline"
class="block w-full"
required
autocomplete="姓カナ"
/>
<TextInput
label="名カナ"
placeholder="例:タロウ"
id="first_name_kana"
v-model="customerForm.first_name_kana"
type="text"
icon="mdi-card-bulleted-outline"
class="block w-full ml-2"
required
autocomplete="名カナ"
/>
</div>
<InputError class="" :message="customerForm.errors.postcode" />
<TextInput
label="郵便番号"
placeholder="例:1234567"
id="postcode"
v-model="customerForm.postcode"
type="number"
icon="mdi-post-lamp"
class="block w-full"
required
autocomplete="郵便番号"
@change="fetchAddress"
/>
<InputError class="" :message="customerForm.errors.address" />
<TextInput
label="住所"
id="address"
v-model="customerForm.address"
type="text"
icon="mdi-home"
class="block w-full"
required
autocomplete="住所"
/>
<InputError class="" :message="customerForm.errors.tel" />
<TextInput
label="電話番号"
placeholder="例:09876543210"
id="tel"
v-model="customerForm.tel"
type="number"
icon="mdi-phone"
class="block w-full"
required
autocomplete="電話番号"
/>
<InputError class="" :message="customerForm.errors.birth" />
<TextInput
label="誕生日"
id="birth"
v-model="customerForm.birth"
type="date"
icon="mdi-cake"
class="block w-full"
required
autocomplete="誕生日"
/>
<InputError class="" :message="customerForm.errors.gender" />
<v-radio-group v-model="customerForm.gender" inline>
<template v-slot:label>
<div>性別</div>
</template>
<v-radio label="男性" :value="0"></v-radio>
<v-radio label="女性" :value="1" class="ml-2"></v-radio>
<v-radio label="不明" :value="2" class="ml-2"></v-radio>
</v-radio-group>
<InputError class="" :message="customerForm.errors.memo" />
<TextArea
label="メモ"
id="memo"
v-model="customerForm.memo"
icon="mdi-book-open-blank-variant-outline"
class="block w-full"
autocomplete="メモ"
/>
<v-btn :disabled="customerForm.processing" color="blue-darken-1" type="submit" class="text-none" rounded="xs" size="x-large" variant="flat" block>変更する</v-btn>
</form>
</v-col>
</v-row>
</v-container>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
CustomerController.phpのupdate()メソッド
$request->validated()
とすることで、UpdateCustomerRequest
クラスに定義されたバリデーションを通過したデータの配列を取得できる。その配列を update()
メソッドに渡すことで、対応するモデルのデータベースレコードを更新する。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCustomerRequest;
use App\Http\Requests\UpdateCustomerRequest;
use App\Models\Customer;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function update(UpdateCustomerRequest $request, Customer $customer)
{
$customer->update($request->validated());
return to_route('customers.index')->with([
'message' => '変更が完了しました!',
'status' => 'success',
]);
}
}
UpdateCustomerRequest.php編集
と同じなので割愛。以下を参照。
Show.vue編集(編集ボタン)
詳細画面で作成しておいた『編集する』ボタンに、click
イベントを追加する。
以下のテンプレートでは、@click
属性に goToCustomerEdit
メソッドを紐付けている。
<template>
<v-btn color="blue-darken-1" class="text-none w-full" rounded="xs" size="x-large" variant="flat"
@click="goToCustomerEdit">編集する</v-btn>
</template>
goToCustomerEdit()
メソッドを作成し、指定された顧客ID(customerForm.id
)を用いて編集画面に遷移させる処理を実装する。
<script setup lang="ts">
const goToCustomerEdit = () => {
router.get(route('customers.edit', {
'customer': customerForm.id
}))
}
</script>
このコードにより、ボタンをクリックすると該当顧客の編集画面(customers.edit
ルート)に遷移するようになる。
削除機能
Show.vue編集(削除ボタン)
詳細画面で作成しておいた『削除する』ボタンに、click
イベントを追加する。また、削除確認用のダイアログとして ConfirmDeleteDialog
コンポーネントを記述する。
以下のテンプレートでは、@click
イベントが発火すると dialog = true
となり、その状態が ConfirmDeleteDialog
コンポーネントに渡される。
<template>
<v-btn color="red-darken-1 w-full" class="text-none" rounded="xs" size="x-large" variant="flat" @click="dialog = true">
削除する</v-btn>
<ConfirmDeleteDialog v-model:visible="dialog" @confirm="goToCustomerDelete" />
</template>
script setup
内で以下の実装を行う。
dialog
をref
で定義して、ダイアログの開閉状態を管理する。- 削除処理を行う
goToCustomerDelete()
メソッドを作成し、削除対象の顧客ID(customerForm.id
)を利用して削除リクエストを送信する。 - 削除後にダイアログを閉じる処理を追加。
<script setup lang="ts">
const dialog = ref<boolean>(false);
const goToCustomerDelete = () => {
customerForm.delete(route('customers.destroy', {
'customer' : customerForm.id
}))
dialog.value = false
}
</script>
ConfirmDeleteDialog.vueコンポーネント作成
削除操作を行う際に確認ダイアログを表示するコンポーネント ConfirmDeleteDialog.vue
を作成する。このコンポーネントは以下の機能を提供する。
- 削除確認ダイアログの表示と非表示
親コンポーネントから受け取ったvisible
プロパティを基に、ダイアログを制御する。 - 削除確定時のイベント発火
ユーザーが削除を確定した際、confirm
イベントを親コンポーネントに通知する。
<script setup lang="ts">
import { ref, watch } from 'vue';
interface Props {
visible: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
(event: "update:visible", value: boolean) :void
(event: "confirm") :void
}>()
const localVisible = ref(props.visible)
watch(() => props.visible,
(newVal) => {
localVisible.value = newVal
})
const closeDialog = () => {
emit("update:visible", false)
}
const confirmDelete = () => {
emit("confirm")
closeDialog()
}
</script>
<template>
<v-dialog v-model="localVisible" max-width="400px">
<v-card>
<v-card-title class="text-h6">本当に削除しますか?</v-card-title>
<v-card-text>この操作は元に戻せません。</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<!-- キャンセルボタン -->
<v-btn @click="closeDialog">キャンセル</v-btn>
<!-- 削除確認ボタン -->
<v-btn color="red-darken-1" class="text-none" variant="flat" @click="confirmDelete">削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
CustomerController.php編集
顧客情報を管理する際、削除フラグ(delete_flg
)を用いた論理削除を採用することで、データの物理削除を防ぎ、安全性を確保する方法をとる。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCustomerRequest;
use App\Http\Requests\UpdateCustomerRequest;
use App\Models\Customer;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function destroy(Customer $customer)
{
$customer->delete_flg = 1;
$customer->save();
return to_route('customers.index')->with([
'message' => '削除が完了しました!',
'status' => 'success',
]);
}
}
グローバルスコープ作成
論理削除の仕組みを実装したが、現在、顧客一覧には delete_flg
に関係なく全てのデータが取得されている。
これを改善し、削除フラグが 0(未削除)のデータのみを取得するようにする。
一時的に Controller
の index
メソッドに条件を記述する方法もあるが、削除済みデータを基本的に扱わないので、Customer
モデルに グローバルスコープ を追加する。
これにより、モデルを通じたデータ操作全般で、自動的に delete_flg = 0
の条件が適用されるようになる。
以下のコマンドを実行してスコープクラスを作成する。
php artisan make:scope CustomerActiveScope
作成されたクラス内の apply()
メソッドに、削除済みデータを除外する条件を記述する。
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class CustomerActiveScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
$builder->where('delete_flg', 0);
}
}
スコープをモデルに登録する。ここでは、ScopedByアトリビュート を使用する。
<?php
namespace App\Models;
use App\Models\Scopes\CustomerActiveScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([CustomerActiveScope::class])]
class Customer extends Model
{
//略
}
補足: booted()
メソッドを使用する場合
ScopedByアトリビュートを使用せず、booted()
メソッドをオーバーライドして登録する方法もある。
<?php
namespace App\Models;
use App\Models\Scopes\CustomerActiveScope;
use Illuminate\Database\Eloquent\Model;
class Customer extends Model
{
/**
* モデルの起動時にグローバルスコープを追加
*/
protected static function booted(): void
{
static::addGlobalScope(new CustomerActiveScope);
}
}
これで、Customer
モデルを使用する全てのクエリで、delete_flg = 0
の条件が自動的に適用される。
選択肢 | メリット | デメリット |
---|---|---|
グローバルスコープ | ・コードの重複を減らせる ・安全性が向上する ・一貫した挙動を実現 | ・柔軟性が低下する ・必要に応じてスコープを無効化する手間 |
ローカルスコープ | ・必要なときだけ適用可能 ・柔軟性が高い | ・コードの記述量が増える |
クエリに直接記述 | ・完全に制御可能 | ・冗長になりやすい ・誤りが起きやすい |