跳至内容

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"> &middot; 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"> &middot; 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)
{
//
}
 
}

授权

默认情况下,`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 的作者可以编辑它。

An editted chirp

继续允许删除 Chirps...