第10章 10.2 Laravel API 完整项目
上一章我们用 ThinkPHP 6 实战了一个简易 CMS 系统,学会了模型、视图、控制器的配合使用。但 CMS 只是开始——真正的现代后端开发,99% 是在写 API。你以为用户在浏览器里点点点,其实背后全是接口在来回传数据。
这一章,我们换一套更"正规军"的打法:用 Laravel + Sanctum + Eloquent,从零搭建一套完整的 RESTful API。你问为什么要学这个?因为现在你投10个后端岗位,8个要求"熟练 Laravel 开发 API 经验"。
学完本文,你能独立写出一个带认证、增删改查、数据关联的可运行 API 项目。
🎯 开场 3 分钟:为什么要学这个?
场景还原:你接了个外包,要给甲方做个手机 App 后端。甲方说"需要用户登录、能看到文章列表、还能评论"。你怎么办?写一堆 echo json_encode(...) 的 PHP 文件?
恭喜你,那就是 10 年前的写法。现在正经项目:所有功能都是 \n\n
\n\n
\n\nAPI 接口,App/Web/小程序都可以调用同一套接口。
痛点问题:
- 写原生 PHP API,要手动处理请求解析、响应格式、错误处理、认证验证……写一个接口能写半天
- 数据表关联查询(用户有哪些文章?文章有多少评论?)用原生 SQL 写到你怀疑人生
- 用户的登录态怎么管理?Cookie?Session?Token?
Laravel 帮你解决:一个命令生成 API 骨架,内置认证系统、ORM 映射、路由控制、输入校验……你只需要专注写业务逻辑。
🧱 基础 25 分钟:核心概念(小白视角)
10.2.1 Laravel 是什么?
类比:ThinkPHP 像是"快餐套餐",Laravel 更像是"自助厨房"。ThinkPHP 给你规定好了固定流程,Laravel 给你原材料和工具,你想怎么做都行。
Laravel 是 PHP 社区最流行的框架,特点是:
- 生态完善:认证、缓存、队列、ORM、API 工具……全都有
- 语法优雅:用链式调用替代嵌套回调,用 Facade 模式简化静态调用
- 社区活跃:遇到问题 Stack Overflow 一搜一大把
# 安装 Laravel(需要 Composer)
composer global require laravel/installer
laravel new my-api-project
cd my-api-project
php artisan serve
上面 4 行命令,你就有了个完整的 Laravel 项目并运行起来了。访问 http://localhost:8000,你应该能看到 Laravel 的欢迎页。
10.2.2 Sanctum:轻量级 API 认证
痛点:你要怎么知道请求是谁发的?Cookie 不靠谱(移动端、小程序用不了),Session 共享麻烦——Token 才是正道。
类比:Token 就像是健身房的手环。你进门刷会员卡,领一个手环戴着,之后在健身房里的所有操作(用器械、买蛋白粉、洗澡)都刷手环,不用每次都出示会员卡。手环过期了?重新刷会员卡领新的。
Sanctum 就是 Laravel 官方出的"发手环"工具。它的工作流程:
- 用户提交用户名 + 密码
- 服务器验证通过,发一个 Token(一串随机字符串)
- 客户端之后的每次请求,带着这个 Token
- 服务器看 Token 就知道是谁
# routes/api.php 配置认证路由
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
Route::post('/sanctum/token', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['提供的凭证有误'],
]);
}
// 发 Token,第三个参数是权限范围
return $user->createToken($request->device_name, ['*'])->plainTextToken;
});
解释:这 25 行代码,就是一个完整的登录接口。/sanctum/token 接收 email + password + device_name(设备名),验证通过后返回 Token。客户端拿到 Token,以后请求时放在 Authorization: Bearer <token> 头里就行。
10.2.3 Eloquent:让数据库操作变成对象操作
痛点:写原生 SQL?SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC——这种字符串拼来拼去,不仅容易出错,遇上复杂关联查询更是噩梦。
类比:Eloquent 就像是用点菜的方式操作数据库。你说"来一份宫保鸡丁",后厨知道怎么做。你说"Post::where('user_id', 1)->orderBy('created_at', 'desc')->get()",Laravel 知道去数据库里查什么、怎么查、查完返回什么格式。
先建表再写模型:
php artisan make:migration create_posts_table
php artisan make:migration create_comments_table
php artisan migrate
// database/migrations/2024_01_01_000001_create_posts_table.php
// 这个文件 Laravel 自动生成,你只需要定义表结构
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('content');
$table->timestamps();
});
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->timestamps();
});
写 Model:
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
protected $fillable = ['title', 'content', 'user_id'];
// 这篇文章属于哪个用户
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// 这篇文章有哪些评论
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
// 这篇文章的评论数(访问器)
public function getCommentsCountAttribute(): int
{
return $this->comments()->count();
}
}
// app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
protected $fillable = ['content', 'post_id', 'user_id'];
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
解释:就这么两个 Model 文件,定义好关联关系之后,你就可以这样查询:
// 查用户发的所有文章
$posts = User::find(1)->posts;
// 查某篇文章的所有评论,顺便把评论者名字也查出来
$comments = Post::find(1)->comments()->with('user')->get();
// 查文章时同时查出评论数和作者信息(一步到位)
$post = Post::with(['user', 'comments'])->find(1);
$post->comments_count; // 直接拿到评论数
10.2.4 Pest:测试也可以很优雅
痛点:手动测试?改一行代码,挨个接口点一遍?累死了。自动化测试? PHPUnit 语法太啰嗦。
Pest 是 Laravel 的测试框架,用更简洁的语法写测试:
// tests/Feature/PostApiTest.php
use App\Models\User;
use App\Models\Post;
it('可以创建文章', function () {
$user = User::factory()->create();
$response = actingAs($user)->postJson('/api/posts', [
'title' => '我的第一篇文章',
'content' => '内容内容内容',
]);
$response->assertStatus(201)
->assertJsonPath('data.title', '我的第一篇文章');
$this->assertDatabaseHas('posts', [
'title' => '我的第一篇文章',
'user_id' => $user->id,
]);
});
解释:这个测试的意思是"作为已登录用户,发起 POST 请求创建文章,期望返回 201 状态码,并且数据库里真的有这条记录"。一行 actingAs($user) 搞定认证,不用自己构造 Token。
🔥 实战 35 分钟:3 个递进的小项目
项目 1:用户认证 API(5 分钟)
目标:搭一个能注册、登录、获取个人信息的 API。
完整代码:
// routes/api.php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Route;
// 注册
Route::post('/register', function (Request $request) {
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
return response()->json([
'message' => '注册成功',
'data' => $user,
], 201);
});
// 登录
Route::post('/login', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['邮箱或密码错误'],
]);
}
return $user->createToken($request->device_name ?? 'default')->plainTextToken;
});
// 登录后才能访问的路由
Route::middleware('auth:sanctum')->group(function () {
Route::get('/me', function (Request $request) {
return $request->user();
});
Route::post('/logout', function (Request $request) {
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => '已退出登录']);
});
});
测试方法:
# 注册
curl -X POST http://localhost:8000/api/register \
-H "Content-Type: application/json" \
-d '{"name":"小明","email":"xiaoming@test.com","password":"123456","password_confirmation":"123456"}'
# 登录(返回 Token)
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"xiaoming@test.com","password":"123456"}'
# 获取个人信息(把 <TOKEN> 换成上面返回的 Token)
curl http://localhost:8000/api/me \
-H "Authorization: Bearer <TOKEN>"
预期输出:
{"message":"注册成功","data":{"id":1,"name":"小明","email":"xiaoming@test.com",...}}
一句话:这 50 行代码实现了完整的注册-登录-Token 获取-获取用户信息-退出流程,以后写 API 认证照着这个改就行。
项目 2:文章增删改查 API(15 分钟)
目标:实现文章的创建、列表、详情、更新、删除,带上关联的用户信息。
完整代码:
// app/Models/Post.php(同上,已省略)
// app/Models/User.php 需要加上 posts 关联
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password'];
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
// routes/api.php 继续添加
Route::middleware('auth:sanctum')->group(function () {
// ... 上面已有的 /me, /logout
// 文章列表(分页 + 关联用户)
Route::get('/posts', function () {
return Post::with('user')
->orderBy('created_at', 'desc')
->paginate(10);
});
// 创建文章
Route::post('/posts', function (Request $request) {
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$post = $request->user()->posts()->create([
'title' => $request->title,
'content' => $request->content,
]);
return response()->json([
'message' => '文章创建成功',
'data' => $post->load('user'),
], 201);
});
// 查看单篇文章
Route::get('/posts/{id}', function ($id) {
$post = Post::with(['user', 'comments.user'])->findOrFail($id);
return $post;
});
// 更新文章(只能更新自己的文章)
Route::put('/posts/{id}', function (Request $request, $id) {
$post = Post::where('id', $id)
->where('user_id', $request->user()->id)
->firstOrFail();
$post->update($request->only(['title', 'content']));
return response()->json([
'message' => '文章更新成功',
'data' => $post,
]);
});
// 删除文章(只能删除自己的文章)
Route::delete('/posts/{id}', function (Request $request, $id) {
$post = Post::where('id', $id)
->where('user_id', $request->user()->id)
->firstOrFail();
$post->delete();
return response()->json(['message' => '文章已删除']);
});
});
测试方法:
# 获取文章列表
curl http://localhost:8000/api/posts \
-H "Authorization: Bearer <TOKEN>"
# 创建文章
curl -X POST http://localhost:8000/api/posts \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"title":"Laravel真香","content":"用了Laravel我再也不想写原生PHP了"}'
# 更新文章(id=1)
curl -X PUT http://localhost:8000/api/posts/1 \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"title":"Laravel真的香"}'
# 删除文章
curl -X DELETE http://localhost:8000/api/posts/1 \
-H "Authorization: Bearer <TOKEN>"
预期输出:
{
"data": [
{"id":1,"title":"Laravel真香","content":"...","user":{"id":1,"name":"小明"},...}
],
"links":{...},
"meta":{"current_page":1,...}
}
一句话:Post::with('user') 一行代码把用户信息也查出来了,不用 JOIN 不用手动拼数组,Eloquent 帮你搞定。
项目 3:评论系统 + 数据导出(15 分钟)
目标:给文章加评论功能,然后把所有文章和评论导出成 JSON 文件。
完整代码:
// routes/api.php 添加评论路由
Route::middleware('auth:sanctum')->group(function () {
// ... 上面已有的文章路由
// 添加评论
Route::post('/posts/{post}/comments', function (Request $request, $post) {
$request->validate([
'content' => 'required|string|max:1000',
]);
$post = Post::findOrFail($post);
$comment = $post->comments()->create([
'user_id' => $request->user()->id,
'content' => $request->content,
]);
return response()->json([
'message' => '评论成功',
'data' => $comment->load('user'),
], 201);
});
// 删除评论
Route::delete('/comments/{id}', function (Request $request, $id) {
$comment = Comment::where('id', $id)
->where('user_id', $request->user()->id)
->firstOrFail();
$comment->delete();
return response()->json(['message' => '评论已删除']);
});
});
// 导出数据(不需要登录)
Route::get('/export', function () {
$posts = Post::with(['user', 'comments.user'])->get();
$exportData = $posts->map(function ($post) {
return [
'id' => $post->id,
'title' => $post->title,
'content' => $post->content,
'author' => $post->user->name,
'comments_count' => $post->comments->count(),
'comments' => $post->comments->map(fn($c) => [
'content' => $c->content,
'author' => $c->user->name,
'created_at' => $c->created_at,
]),
'created_at' => $post->created_at,
];
});
$filename = 'export_' . date('Ymd_His') . '.json';
file_put_contents(storage_path("app/exports/{$filename}"), $exportData->toJson(JSON_UNESCAPED_UNICODE));
return response()->json([
'message' => '导出成功',
'file' => $filename,
'count' => $exportData->count(),
]);
});
测试方法:
# 添加评论
curl -X POST http://localhost:8000/api/posts/1/comments \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"content":"写得真好,收藏了!"}'
# 导出数据
curl http://localhost:8000/api/export
预期输出:
{"message":"导出成功","file":"export_20240101_120000.json","count":1}
导出的文件在 storage/app/exports/ 目录下,用文本编辑器打开看内容。
一句话:评论关联查出来、导出 JSON 一条链搞定——这就是 Eloquent 链式调用的威力。下一章我们把这个能力用到博客系统里,做一个完整的仿掘金平台。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:Token 不过期?是你没设置!
❌ 错误:以为 Sanctum Token 永久有效
$user->createToken($request->device_name); // 默认永久有效!
✅ 正确:如果想限制 Token 有效期
// 在 .env 添加配置
SANCTUM_EXPIRATION=120 # 分钟
// 或者用 Abilities 限制权限
$user->createToken($request->device_name, ['posts:write', 'posts:read']);
坑 2:N+1 查询——看起来没问题,一查就慢
❌ 错误:循环里查关联
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // 循环10次,查了11次数据库!
}
✅ 正确:预加载
$posts = Post::with('user')->get(); // 2次查询搞定
foreach ($posts as $post) {
echo $post->user->name;
}
坑 3:模型赋值时漏了 fillable
❌ 错误:Model 没法批量赋值
class Post extends Model {
// 没写 $fillable
}
// Post::create(['title'=>'x', 'content'=>'y']); // 报错!
✅ 正确:声明哪些字段可批量赋值
class Post extends Model {
protected $fillable = ['title', 'content', 'user_id'];
}
坑 4:分页不用 paginate() 用 all()
❌ 错误:数据多了直接爆炸
$posts = Post::all(); // 一次性查出所有数据,10000条就炸了
✅ 正确:分页是美德
$posts = Post::paginate(15); // 每页15条,自动处理分页逻辑
坑 5:没验证就直接入库
❌ 错误:直接拿用户输入存数据库
$post = new Post();
$post->title = $request->title; // 用户可能传了 10000 字!
$post->save();
✅ 正确:用 validate 或者白名单
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
// 验证失败自动返回 422 错误
性能小贴士:善用 select 和 index
// 只查需要的字段,少传数据就是快
$posts = Post::select('id', 'title', 'created_at')->with('user:id,name')->get();
// 数据库里给常用查询字段加索引
// database/migrations/xxxx_add_index_to_posts.php
Schema::table('posts', function (Blueprint $table) {
$table->index(['user_id', 'created_at']); // 查某用户文章时飞快
});
调试技巧:日志是最好的朋友
// 在关键位置打日志
use Illuminate\Support\Facades\Log;
Log::info('创建文章', [
'user_id' => $request->user()->id,
'title' => $request->title,
]);
// 查看日志
tail -f storage/logs/laravel.log
或者用 Laravel 自带的 dd()(dump and die)打断点:
$post = Post::find(1);
dd($post->toArray()); // 打印完直接停止,看清楚再继续
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):换个字段登录
- 输入:把项目 1 的登录接口从 email 改成 phone(手机号)
- 预期输出:登录接口接受 phone + password,返回 Token
- 提示:User 模型里加个 phone 字段,登录查询时换字段
练习 2(2 分钟):加个「只能看自己文章」的限制
- 输入:在项目 2 的文章列表接口,加个 ?mine=1 参数
- 预期输出:加参数时只返回当前用户的文章,不加时返回所有人的
- 提示:用 $request->boolean('mine') 判断参数
练习 3(3 分钟):给评论加上「回复」功能
- 输入:评论表加个 parent_id 字段,实现二级评论
- 预期输出:可以给某条评论回复,回复显示在该评论下方
- 提示:用 Comment::where('parent_id', $parentId)->get() 查回复
练习 4(3 分钟):导出 CSV 格式
- 输入:在项目 3 的导出接口,把 toJson() 改成导出 CSV
- 预期输出:生成 .csv 文件,包含文章标题、作者、评论数
- 提示:用 fputcsv() 函数,或者搜「Laravel CSV export」
练习 5(5 分钟):分析这个报错
- 输入:代码执行后得到 SQLSTATE[42S22]: Column not found: 1054 Unknown column 'posts.user'
- 预期输出:说出原因并修复
- 提示:检查 with('user') 时 Post 模型有没有定义 user() 关联
作业题(30 分钟-2 小时)
作业:做一个「笔记 API」
小明想做个个人笔记工具,需要这些功能:
- 用户认证:注册、登录、获取 Token
- 笔记本:笔记本的增删改查(name, description)
- 笔记:笔记本里的笔记(notebook_id, title, content, tags)
- 标签:一个笔记可以有多个标签(tag_id + notebook_id)
功能点:
1. 笔记本列表和详情
2. 在笔记本里创建/查看/删除笔记
3. 给笔记打标签(多对多关联)
4. 导出某个笔记本的所有笔记为 JSON
加分项:
1. 笔记支持模糊搜索(标题或内容含关键词)
2. 给 API 加请求频率限制(防刷)
验收标准:
- 能跑起来(php artisan serve)
- 所有接口用 curl 能测通
- 代码有注释(每段逻辑干嘛的)
- Eloquent 模型关联写对(hasMany/belongsTo/manyToMany)
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 件事:
1. Sanctum 做认证:Token 模式让 API 认证变得简单,登录发 Token、请求带 Token 就够了
2. Eloquent 做数据:用对象思维操作数据库,关联查询、批量赋值、分页,一个都不难
3. RESTful 路由设计:GET 查、POST 创、PUT 改、DELETE 删,URL 设计有规律可循
延伸学习资源:
- Laravel 官方文档(英文,但例子清晰)
- Laravel China 文档(中文翻译,质量高)
- 书籍《Laravel 之道》——深入讲解框架设计思想
互动钩子:你在实际项目里用过 Laravel 吗?遇到过什么奇葩 bug?评论区聊聊,老粉优先回复!
下一章,我们要用一个仿掘金博客系统把所有知识串起来——用户、文章、评论、点赞、收藏……做一个能拿得出手的作品。准备好了吗?

评论(0)