05. 编辑 Chirps
让我们添加一个其他流行的以鸟类为主题的微博平台所缺少的功能——编辑 Chirps 的功能!
路由
首先,我们将更新我们的路由文件以启用资源控制器的 chirps.update
路由
routes/web.php
<?php
use App\Http\Controllers\ChirpController; use App\Http\Controllers\ProfileController; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Inertia\Inertia; Route::get('/', function () { return Inertia::render('Welcome', [ 'canLogin' => Route::has('login'), 'canRegister' => Route::has('register'), 'laravelVersion' => Application::VERSION, 'phpVersion' => PHP_VERSION, ]); }); Route::get('/dashboard', function () { return Inertia::render('Dashboard'); })->middleware(['auth', 'verified'])->name('dashboard'); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); Route::resource('chirps', ChirpController::class)- ->only(['index', 'store'])+ ->only(['index', 'store', 'update']) ->middleware(['auth', 'verified']);
require __DIR__.'/auth.php';
此控制器的路由表现在如下所示
动词 | URI | 操作 | 路由名称 |
---|---|---|---|
GET | /chirps |
index | chirps.index |
POST | /chirps |
store | chirps.store |
PUT/PATCH | /chirps/{chirp} |
update | chirps.update |
更新我们的组件
接下来,让我们更新 Chirp
组件,使其包含用于现有 Chirps 的编辑表单。
我们将使用 Breeze 附带的 Dropdown
组件,我们只将其显示给 Chirp 作者。我们还将通过比较 Chirp 的 created_at
日期与其 updated_at
日期来显示 Chirp 是否已被编辑的指示
resources/js/Components/Chirp.vue
<script setup>+import Dropdown from '@/Components/Dropdown.vue';+import InputError from '@/Components/InputError.vue';+import PrimaryButton from '@/Components/PrimaryButton.vue'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime';+import { useForm } from '@inertiajs/vue3';+import { ref } from 'vue'; dayjs.extend(relativeTime); -defineProps(['chirp']);+const props = defineProps(['chirp']);+ +const form = useForm({+ message: props.chirp.message,+});+ +const editing = ref(false); </script> <template> <div class="p-6 flex space-x-2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /> </svg> <div class="flex-1"> <div class="flex justify-between items-center"> <div> <span class="text-gray-800">{{ chirp.user.name }}</span> <small class="ml-2 text-sm text-gray-600">{{ dayjs(chirp.created_at).fromNow() }}</small>+ <small v-if="chirp.created_at !== chirp.updated_at" class="text-sm text-gray-600"> · edited</small> </div>+ <Dropdown v-if="chirp.user.id === $page.props.auth.user.id">+ <template #trigger>+ <button>+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">+ <path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />+ </svg>+ </button>+ </template>+ <template #content>+ <button class="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out" @click="editing = true">+ Edit+ </button>+ </template>+ </Dropdown> </div>- <p class="mt-4 text-lg text-gray-900">{{ chirp.message }}</p>+ <form v-if="editing" @submit.prevent="form.put(route('chirps.update', chirp.id), { onSuccess: () => editing = false })">+ <textarea v-model="form.message" class="mt-4 w-full text-gray-900 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"></textarea>+ <InputError :message="form.errors.message" class="mt-2" />+ <div class="space-x-2">+ <PrimaryButton class="mt-4">Save</PrimaryButton>+ <button class="mt-4" @click="editing = false; form.reset(); form.clearErrors()">Cancel</button>+ </div>+ </form>+ <p v-else class="mt-4 text-lg text-gray-900">{{ chirp.message }}</p> </div> </div> </template>
resources/js/Components/Chirp.jsx
-import React from 'react';+import React, { useState } from 'react';+import Dropdown from '@/Components/Dropdown';+import InputError from '@/Components/InputError';+import PrimaryButton from '@/Components/PrimaryButton'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime';+import { useForm, usePage } from '@inertiajs/react'; dayjs.extend(relativeTime); export default function Chirp({ chirp }) {+ const { auth } = usePage().props;+ + const [editing, setEditing] = useState(false);+ + const { data, setData, patch, clearErrors, reset, errors } = useForm({+ message: chirp.message,+ });+ + const submit = (e) => {+ e.preventDefault();+ patch(route('chirps.update', chirp.id), { onSuccess: () => setEditing(false) });+ }; return ( <div className="p-6 flex space-x-2"> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"> <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /> </svg> <div className="flex-1"> <div className="flex justify-between items-center"> <div> <span className="text-gray-800">{chirp.user.name}</span> <small className="ml-2 text-sm text-gray-600">{dayjs(chirp.created_at).fromNow()}</small>+ { chirp.created_at !== chirp.updated_at && <small className="text-sm text-gray-600"> · edited</small>} </div>+ {chirp.user.id === auth.user.id &&+ <Dropdown>+ <Dropdown.Trigger>+ <button>+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">+ <path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />+ </svg>+ </button>+ </Dropdown.Trigger>+ <Dropdown.Content>+ <button className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out" onClick={() => setEditing(true)}>+ Edit+ </button>+ </Dropdown.Content>+ </Dropdown>+ } </div>- <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>+ {editing+ ? <form onSubmit={submit}>+ <textarea value={data.message} onChange={e => setData('message', e.target.value)} className="mt-4 w-full text-gray-900 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"></textarea>+ <InputError message={errors.message} className="mt-2" />+ <div className="space-x-2">+ <PrimaryButton className="mt-4">Save</PrimaryButton>+ <button className="mt-4" onClick={() => { setEditing(false); reset(); clearErrors(); }}>Cancel</button>+ </div>+ </form>+ : <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>+ } </div> </div> ) }
更新我们的控制器
现在我们可以更新 `ChirpController` 类中的 `update` 方法来验证请求并更新数据库。即使我们只向 Chirp 的作者显示编辑按钮,我们也需要授权请求以确保确实是作者在更新它。
app/Http/Controllers/ChirpController.php
<?php
namespace App\Http\Controllers; use App\Models\Chirp; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request;+use Illuminate\Support\Facades\Gate; use Inertia\Inertia; use Inertia\Response; class ChirpController extends Controller {
/** * Display a listing of the resource. */ public function index(): Response { return Inertia::render('Chirps/Index', [ 'chirps' => Chirp::with('user:id,name')->latest()->get(), ]); } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request): RedirectResponse { $validated = $request->validate([ 'message' => 'required|string|max:255', ]); $request->user()->chirps()->create($validated); return redirect(route('chirps.index')); } /** * Display the specified resource. */ public function show(Chirp $chirp) { // } /** * Show the form for editing the specified resource. */ public function edit(Chirp $chirp) { // } /** * Update the specified resource in storage. */- public function update(Request $request, Chirp $chirp)+ public function update(Request $request, Chirp $chirp): RedirectResponse {- //+ Gate::authorize('update', $chirp);+ + $validated = $request->validate([+ 'message' => 'required|string|max:255',+ ]);+ + $chirp->update($validated);+ + return redirect(route('chirps.index')); }
/** * Remove the specified resource from storage. */ public function destroy(Chirp $chirp) { // } }
您可能已经注意到验证规则与 `store` 方法重复。您可以考虑使用 Laravel 的 表单请求验证 来提取它们,这使得重用验证规则和保持控制器轻量级变得容易。
授权
默认情况下,`authorize` 方法将阻止所有人更新 Chirp。我们可以通过以下命令创建一个 模型策略 来指定谁被允许更新它。
php artisan make:policy ChirpPolicy --model=Chirp
这将在 `app/Policies/ChirpPolicy.php` 中创建一个策略类,我们可以更新它以指定只有作者被授权更新 Chirp。
app/Policies/ChirpPolicy.php
<?php
namespace App\Policies; use App\Models\Chirp; use App\Models\User; use Illuminate\Auth\Access\HandlesAuthorization; class ChirpPolicy {
use HandlesAuthorization; /** * Determine whether the user can view any models. */ public function viewAny(User $user): bool { // } /** * Determine whether the user can view the model. */ public function view(User $user, Chirp $chirp): bool { // } /** * Determine whether the user can create models. */ public function create(User $user): bool { // } /** * Determine whether the user can update the model. */ public function update(User $user, Chirp $chirp): bool {- //+ return $chirp->user()->is($user); }
/** * Determine whether the user can delete the model. */ public function delete(User $user, Chirp $chirp): bool { // } /** * Determine whether the user can restore the model. */ public function restore(User $user, Chirp $chirp): bool { // } /** * Determine whether the user can permanently delete the model. */ public function forceDelete(User $user, Chirp $chirp): bool { // } }
测试它
是时候测试它了!继续使用下拉菜单编辑一些 Chirps。如果您注册了另一个用户帐户,您将看到只有 Chirp 的作者可以编辑它。