跳至内容

03. 创建 Chirps

现在您已准备好开始构建您的新应用程序!让我们允许用户发布名为Chirps的简短消息。

模型、迁移和控制器

为了允许用户发布 Chirps,我们需要创建模型、迁移和控制器。让我们更深入地探讨这些概念。

  • 模型为您提供了一个强大且愉快的界面来与数据库中的表进行交互。
  • 迁移允许您轻松创建和修改数据库中的表。它们确保您的应用程序在任何运行的地方都具有相同的数据库结构。
  • 控制器负责处理对您的应用程序发出的请求并返回响应。

您构建的几乎每个功能都将涉及所有这些部分协同工作,因此artisan make:model命令可以一次性为您创建所有这些部分。

让我们使用以下命令为我们的 Chirps 创建一个模型、迁移和资源控制器

php artisan make:model -mrc Chirp

此命令将为您创建三个文件

  • 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中间件。
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'])
+ ->middleware(['auth', 'verified']);
 
require __DIR__.'/auth.php';

这将创建以下路由

动词 URI 操作 路由名称
GET /chirps index chirps.index
POST /chirps store chirps.store

让我们通过从新ChirpController类的index方法返回测试消息来测试我们的路由和控制器

app/Http/Controllers/ChirpController.php
<?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/chirpshttp://localhost/chirps(如果您使用的是 Sail!)时,您应该会看到您的消息。

惯性

还不够印象深刻?让我们更新ChirpController类的index方法,以使用惯性渲染前端页面组件。惯性是将我们的 Laravel 应用程序与我们的 Vue 或 React 前端连接在一起的东西

app/Http/Controllers/ChirpController.php
<?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页面组件

resources/js/Pages/Chirps/Index.vue
<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>
resources/js/Pages/Chirps/Index.jsx
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 提供的默认布局中呈现!

Chirp form

现在我们的前端由 JavaScript 驱动,我们对 JavaScript 模板所做的任何更改都会在 Vite 开发服务器通过 npm run dev 运行时自动重新加载到浏览器中。

导航菜单

让我们花点时间在 Breeze 提供的导航菜单中添加一个链接。

更新 Breeze 提供的 AuthenticatedLayout 组件以添加桌面屏幕的菜单项

resources/js/Layouts/AuthenticatedLayout.vue
<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>
resources/js/Layouts/AuthenticatedLayout.jsx
<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>

以及移动屏幕

resources/js/Layouts/AuthenticatedLayout.vue
<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>
resources/js/Layouts/AuthenticatedLayout.jsx
<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

app/Http/Controllers/ChirpController.php
<?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 模型上创建此方法以定义一个 "一对多" 关系

app/Models/User.php
<?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);
+ }
}

批量赋值保护

将来自请求的所有数据传递给您的模型可能很危险。想象一下,您有一个页面,用户可以在其中编辑他们的个人资料。如果您要将整个请求传递给模型,那么用户可以编辑他们喜欢的任何列,例如 is_admin 列。这被称为 批量赋值漏洞

Laravel 默认情况下会阻止批量赋值,从而防止您意外执行此操作。批量赋值非常方便,因为它可以避免您逐个分配每个属性。我们可以通过将安全属性标记为“fillable”来启用对它们的批量赋值。

让我们将 `$fillable` 属性添加到我们的 `Chirp` 模型中,以启用对 `message` 属性的批量赋值。

app/Models/Chirp.php
<?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:show
php artisan db:table users

因此,唯一缺少的是数据库中的额外列,用于存储 `Chirp` 与其 `User` 之间的关联关系以及消息本身。还记得我们之前创建的数据库迁移吗?现在是打开该文件添加一些额外列的时候了。

database/migrations/<timestamp>_create_chirps_table.php
<?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

测试一下

我们现在可以使用刚刚创建的表单发送 Chirp 了!我们还无法看到结果,因为我们还没有在页面上显示现有的 Chirps。

Chirp form

如果您将消息字段留空,或输入超过 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

继续开始显示 Chirps...