03. 创建 Chirps
现在您已准备好开始构建您的新应用程序!让我们允许用户发布名为Chirps的简短消息。
模型、迁移和控制器
为了允许用户发布 Chirps,我们需要创建模型、迁移和控制器。让我们更深入地探讨这些概念。
- 模型为您提供了一个强大且愉快的界面来与数据库中的表进行交互。
- 迁移允许您轻松创建和修改数据库中的表。它们确保您的应用程序在任何运行的地方都具有相同的数据库结构。
- 控制器负责处理对您的应用程序发出的请求并返回响应。
您构建的几乎每个功能都将涉及所有这些部分协同工作,因此artisan make:model
命令可以一次性为您创建所有这些部分。
让我们使用以下命令为我们的 Chirps 创建一个模型、迁移和资源控制器
php artisan make:model -mrc Chirp
您可以通过运行php artisan make:model --help
命令查看所有可用选项。
此命令将为您创建三个文件
-
app/Models/Chirp.php
- Eloquent 模型。 -
database/migrations/<timestamp>_create_chirps_table.php
- 将创建数据库表的数据库迁移。 -
app/Http/Controllers/ChirpController.php
- 将接收传入请求并返回响应的 HTTP 控制器。
路由
我们还需要为我们的控制器创建 URL。我们可以通过添加“路由”来实现,这些路由在项目的routes
目录中进行管理。因为我们使用的是资源控制器,所以我们可以使用单个Route::resource()
语句来定义所有遵循传统 URL 结构的路由。
首先,我们将启用两个路由
index
路由将显示我们的表单和 Chirps 列表。store
路由将用于保存新的 Chirps。
我们还将在两个中间件后面放置这些路由
auth
中间件确保只有登录用户才能访问该路由。- 如果您决定启用电子邮件验证,则将使用
verified
中间件。
<?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'])+ ->middleware(['auth', 'verified']); require __DIR__.'/auth.php';
这将创建以下路由
动词 | URI | 操作 | 路由名称 |
---|---|---|---|
GET | /chirps |
index | chirps.index |
POST | /chirps |
store | chirps.store |
您可以通过运行php artisan route:list
命令查看应用程序的所有路由。
让我们通过从新ChirpController
类的index
方法返回测试消息来测试我们的路由和控制器
<?php
namespace App\Http\Controllers; use App\Models\Chirp; use Illuminate\Http\Request; +use Illuminate\Http\Response; class ChirpController extends Controller { /** * Display a listing of the resource. */- public function index()+ public function index(): Response {- //+ return response('Hello, World!'); }
/** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * 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) { // } /** * Remove the specified resource from storage. */ public function destroy(Chirp $chirp) { // } }
如果您之前仍然登录,则在导航到http://localhost:8000/chirps或http://localhost/chirps(如果您使用的是 Sail!)时,您应该会看到您的消息。
惯性
还不够印象深刻?让我们更新ChirpController
类的index
方法,以使用惯性渲染前端页面组件。惯性是将我们的 Laravel 应用程序与我们的 Vue 或 React 前端连接在一起的东西
<?php namespace App\Http\Controllers; use App\Models\Chirp; use Illuminate\Http\Request;-use Illuminate\Http\Response;+use Inertia\Inertia;+use Inertia\Response; class ChirpController extends Controller { /** * Display a listing of the resource. */ public function index(): Response {- return response('Hello, World!');+ return Inertia::render('Chirps/Index', [+ //+ ]); }
/** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * 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) { // } /** * Remove the specified resource from storage. */ public function destroy(Chirp $chirp) { // } }
然后,我们可以使用用于创建新 Chirps 的表单创建我们的前端Chirps/Index
页面组件
<script setup>import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';import InputError from '@/Components/InputError.vue';import PrimaryButton from '@/Components/PrimaryButton.vue';import { useForm, Head } from '@inertiajs/vue3'; const form = useForm({ message: '',});</script> <template> <Head title="Chirps" /> <AuthenticatedLayout> <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8"> <form @submit.prevent="form.post(route('chirps.store'), { onSuccess: () => form.reset() })"> <textarea v-model="form.message" placeholder="What's on your mind?" class="block w-full 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" /> <PrimaryButton class="mt-4">Chirp</PrimaryButton> </form> </div> </AuthenticatedLayout></template>
import React from 'react';import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';import InputError from '@/Components/InputError';import PrimaryButton from '@/Components/PrimaryButton';import { useForm, Head } from '@inertiajs/react'; export default function Index({ auth }) { const { data, setData, post, processing, reset, errors } = useForm({ message: '', }); const submit = (e) => { e.preventDefault(); post(route('chirps.store'), { onSuccess: () => reset() }); }; return ( <AuthenticatedLayout user={auth.user}> <Head title="Chirps" /> <div className="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8"> <form onSubmit={submit}> <textarea value={data.message} placeholder="What's on your mind?" className="block w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm" onChange={e => setData('message', e.target.value)} ></textarea> <InputError message={errors.message} className="mt-2" /> <PrimaryButton className="mt-4" disabled={processing}>Chirp</PrimaryButton> </form> </div> </AuthenticatedLayout> );}
就是这样!刷新浏览器中的页面,您将看到新表单在 Breeze 提供的默认布局中呈现!
现在我们的前端由 JavaScript 驱动,我们对 JavaScript 模板所做的任何更改都会在 Vite 开发服务器通过 npm run dev
运行时自动重新加载到浏览器中。
导航菜单
让我们花点时间在 Breeze 提供的导航菜单中添加一个链接。
更新 Breeze 提供的 AuthenticatedLayout
组件以添加桌面屏幕的菜单项
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> <NavLink :href="route('dashboard')" :active="route().current('dashboard')"> Dashboard </NavLink>+ <NavLink :href="route('chirps.index')" :active="route().current('chirps.index')">+ Chirps+ </NavLink> </div>
<div className="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> <NavLink href={route('dashboard')} active={route().current('dashboard')}> Dashboard </NavLink>+ <NavLink href={route('chirps.index')} active={route().current('chirps.index')}>+ Chirps+ </NavLink> </div>
以及移动屏幕
<div class="pt-2 pb-3 space-y-1"> <ResponsiveNavLink :href="route('dashboard')" :active="route().current('dashboard')"> Dashboard </ResponsiveNavLink>+ <ResponsiveNavLink :href="route('chirps.index')" :active="route().current('chirps.index')">+ Chirps+ </ResponsiveNavLink> </div>
<div className="pt-2 pb-3 space-y-1"> <ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}> Dashboard </ResponsiveNavLink>+ <ResponsiveNavLink href={route('chirps.index')} active={route().current('chirps.index')}>+ Chirps+ </ResponsiveNavLink> </div>
保存 Chirps
我们的表单已配置为将消息发布到我们之前创建的 chirps.store
路由。让我们更新 ChirpController
类上的 store
方法以验证数据并创建一个新的 Chirp
<?php namespace App\Http\Controllers; use App\Models\Chirp;+use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; 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', [ // ]); } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */- public function store(Request $request)+ 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) { // } /** * Remove the specified resource from storage. */ public function destroy(Chirp $chirp) { // } }
我们使用 Laravel 强大的验证功能来确保用户提供消息,并且消息不会超过我们将要创建的数据库列的 255 个字符限制。
然后,我们通过利用 chirps
关系创建一个属于登录用户的记录。我们很快就会定义这种关系。
最后,在使用 Inertia 时,我们可以返回一个重定向响应来指示 Inertia 重新加载我们的 chirps.index
路由。
创建关系
您可能在之前的步骤中注意到,我们在 $request->user()
对象上调用了一个 chirps
方法。我们需要在我们的 User
模型上创建此方法以定义一个 "一对多" 关系
<?php
namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory;+use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable {
use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for serialization. * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', ]; /** * Get the attributes that should be cast. * * @return array<string, string> */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } + public function chirps(): HasMany+ {+ return $this->hasMany(Chirp::class);+ } }
Laravel 提供了许多不同类型的模型关系,您可以在 Eloquent 关系 文档中了解更多信息。
批量赋值保护
将来自请求的所有数据传递给您的模型可能很危险。想象一下,您有一个页面,用户可以在其中编辑他们的个人资料。如果您要将整个请求传递给模型,那么用户可以编辑他们喜欢的任何列,例如 is_admin
列。这被称为 批量赋值漏洞。
Laravel 默认情况下会阻止批量赋值,从而防止您意外执行此操作。批量赋值非常方便,因为它可以避免您逐个分配每个属性。我们可以通过将安全属性标记为“fillable”来启用对它们的批量赋值。
让我们将 `$fillable` 属性添加到我们的 `Chirp` 模型中,以启用对 `message` 属性的批量赋值。
<?php
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Chirp extends Model {
use HasFactory; + protected $fillable = [+ 'message',+ ]; }
您可以在 文档 中了解更多关于 Laravel 的批量赋值保护的信息。
更新迁移
在应用程序创建期间,Laravel 已经应用了包含在 `database/migrations` 目录中的默认迁移。您可以使用 `php artisan db:show` 和 `php artisan db:table` 命令检查当前数据库结构。
php artisan db:showphp artisan db:table users
因此,唯一缺少的是数据库中的额外列,用于存储 `Chirp` 与其 `User` 之间的关联关系以及消息本身。还记得我们之前创建的数据库迁移吗?现在是打开该文件添加一些额外列的时候了。
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('chirps', function (Blueprint $table) { $table->id();+ $table->foreignId('user_id')->constrained()->cascadeOnDelete();+ $table->string('message'); $table->timestamps(); }); }
/** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('chirps'); } };
我们还没有迁移数据库,因为我们添加了这个迁移,所以现在就来做吧。
php artisan migrate
每个数据库迁移只会被执行一次。要对表进行额外的更改,您需要创建另一个迁移。在开发过程中,您可能希望更新未部署的迁移并使用 `php artisan migrate:fresh` 命令从头开始重建您的数据库。
测试一下
我们现在可以使用刚刚创建的表单发送 Chirp 了!我们还无法看到结果,因为我们还没有在页面上显示现有的 Chirps。
如果您将消息字段留空,或输入超过 255 个字符,那么您将看到验证生效。
Artisan Tinker
现在是学习 Artisan Tinker 的好时机,它是一个 REPL (Read-eval-print loop),您可以在其中执行 Laravel 应用程序中的任意 PHP 代码。
在您的控制台中,启动一个新的 tinker 会话。
php artisan tinker
接下来,执行以下代码以显示数据库中的 Chirps。
App\Models\Chirp::all();
=> Illuminate\Database\Eloquent\Collection {#4512 all: [ App\Models\Chirp {#4514 id: 1, user_id: 1, message: "I'm building Chirper with Laravel!", created_at: "2022-08-24 13:37:00", updated_at: "2022-08-24 13:37:00", }, ], }
您可以使用 `exit` 命令退出 Tinker,或按 Ctrl + c。