0. 前言

Laravel 是一个流行的开源 PHP Web 开发框架,由 Taylor Otwell 创建,主打“优雅语法 + 高开发效率”。

0.1 🧩 一句话理解

Laravel = 帮你快速开发网站/接口的 PHP 框架,把常见功能都帮你封装好了。

0.2 核心特点

0.2.1. MVC 架构

  • Model(模型):处理数据(数据库)
  • View(视图):页面展示
  • Controller(控制器):业务逻辑
    👉 结构清晰,方便维护

0.2.2. 强大的 ORM(Eloquent)

  • 用“面向对象”方式操作数据库
User::where('id', 1)->first();

👉 不用手写 SQL 也能操作数据库

0.2.3 路由系统简单

Route::get('/user', function () {
    return 'Hello User';
});

👉 URL 和逻辑绑定非常直观

0.2.4 内置功能丰富

开箱即用:

  • 用户认证(登录注册)
  • 队列(Queue)
  • 缓存(Cache)
  • 文件上传
  • API 开发(RESTful)

0.2.5 Artisan 命令行工具

php artisan make:controller UserController

👉 自动生成代码,提升效率

0.2.6 Blade 模板引擎

{{ $name }}

👉 写页面更简洁、安全

0.3 适合做什么

  • 后端 API(比如给 Flutter 用 👍)
  • 管理系统(CMS、后台)
  • 电商网站
  • 博客系统

0.4 学习建议(结合你当前情况)

你现在:

  • ✅ 会 Flutter
  • ✅ 正在做 Django + DRF
  • ✅ 想尝试 Laravel

👉 推荐路线:

  1. 先用 Laravel 做 简单 API
  2. 用 Flutter 调接口(你已经会)
  3. 对比 Django 和 Laravel 的差异

0.5 简单对比

框架语言风格
DjangoPython强规范
LaravelPHP灵活 + 优雅

1. 环境搭建与基础概念

在正式写代码之前,我们要先把“地基”打好,否则后面会各种报错、迷路。

1.1 开发环境安装

Laravel 运行需要三个核心东西:

  • PHP(>= 8.1)
  • Composer(依赖管理)
  • Web 服务环境(Nginx / Apache / 内置服务器)

1.1.1 方案一:使用 Laravel Herd(推荐新手 ⭐)

👉 优点:极其简单,一键启动

1.1.1.1 安装步骤:
  1. 下载 Laravel Herd

  2. 安装完成后,它会自动帮你:

    • 安装 PHP
    • 配置环境变量
    • 启动本地服务器
1.1.1.2 创建 Laravel 项目
composer create-project laravel/laravel my_project

👉 解释:

composer                # 使用 Composer 工具
create-project          # 创建项目
laravel/laravel         # Laravel 官方模板
my_project              # 项目名称(文件夹名)
1.1.1.3 启动项目
cd my_project
php artisan serve

打开浏览器:

http://127.0.0.1:8000

看到 Laravel 欢迎页面 = 成功 🎉

1.1.2 方案二:Docker(Laravel Sail)

👉 适合想学后端工程化的人

1.1.2.1 初始化 Sail
curl -s "https://laravel.build/my_project" | bash
cd my_project
./vendor/bin/sail up
1.1.2.2 运行 Artisan
./vendor/bin/sail artisan list

👉 注意:Docker 里必须用 sail artisan

1.1.3 Composer 是什么?

👉 PHP 世界的“npm”

1.1.3.1 常用命令:
composer install     # 安装依赖
composer update      # 更新依赖
composer require xxx # 安装新包

1.2 目录结构详解(重点!!!🔥)

Laravel 项目结构非常清晰,必须掌握。

your-project/
├── app/                # 【核心逻辑】控制台、模型、控制器都在这
│   ├── Http/           # 
│   │   └── Controllers/# ← 控制器文件(处理业务逻辑)
│   └── Models/         # ← 模型文件(对应数据库表)
├── config/             # 所有配置文件(数据库、邮件、缓存等设置)
├── database/           # 数据库相关
│   ├── migrations/     # ← 数据库迁移文件(用代码建表)
│   └── seeders/        # 填充测试数据的地方
├── public/             # 外部访问入口(入口文件 index.php 和 静态资源)
├── resources/          # 
│   └── views/          # ← 视图文件(写 HTML/Blade 模板的地方)
├── routes/             # 【路由】
│   ├── api.php         # ← 定义 API 接口路由
│   └── web.php         # ← 定义普通网页路由
├── storage/            # 存放日志、缓存、上传的文件(系统自动写入)
├── .env                # 【核心配置】数据库密码、密钥全写在这里
└── artisan             # 命令行工具(比如 php artisan make:...)

1.2.1 📁 app/ —— 核心代码区

👉 你写的“业务逻辑”基本都在这里

1.2.1.1 示例结构:
app/
 ├── Models/
 ├── Http/
 │    ├── Controllers/
 │    └── Middleware/
1.2.1.2 示例:创建一个控制器
php artisan make:controller UserController

生成:

app/Http/Controllers/UserController.php
1.2.1.3 控制器代码示例(带注释)
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    /**
     * 显示用户列表
     */
    public function index()
    {
        // 模拟数据(实际来自数据库)
        $users = [
            ['id' => 1, 'name' => 'Tom'],
            ['id' => 2, 'name' => 'Jerry'],
        ];

        // 返回 JSON 数据
        return response()->json($users);
    }

    /**
     * 显示单个用户
     */
    public function show($id)
    {
        return "当前用户ID是:" . $id;
    }
}

1.2.2 📁 routes/ —— 路由(入口)

👉 所有请求从这里进来!

1.2.2.1 关键文件:
routes/web.php   # 浏览器访问
routes/api.php   # API 接口
1.2.2.2 示例:定义路由
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;

/**
 * 基础路由
 */
Route::get('/', function () {
    return 'Hello Laravel!';
});

/**
 * 控制器路由
 */
Route::get('/users', [UserController::class, 'index']);

/**
 * 带参数的路由
 */
Route::get('/users/{id}', [UserController::class, 'show']);
1.2.2.3 路由解释:
Route::get('/users', ...)  
// GET 请求
// URL: /users

Route::get('/users/{id}', ...)
// {id} 是动态参数

1.2.3 📁 resources/ —— 视图与前端资源

👉 用来写页面(Blade 模板)

1.2.3.1 示例结构:
resources/
 ├── views/
 │    ├── welcome.blade.php
1.2.3.2 Blade 示例:
<!-- resources/views/user.blade.php -->

<!DOCTYPE html>
<html>
<head>
    <title>用户页面</title>
</head>
<body>

<h1>用户列表</h1>

<ul>
@foreach ($users as $user)
    <li>{{ $user['name'] }}</li>
@endforeach
</ul>

</body>
</html>
1.2.3.3 控制器返回视图:
public function index()
{
    $users = [
        ['name' => 'Tom'],
        ['name' => 'Jerry']
    ];

    // 返回 Blade 视图
    return view('user', ['users' => $users]);
}

1.3 Artisan 命令行(Laravel 的瑞士军刀 🔧)

👉 Laravel 自带 CLI 工具,超级重要!

1.3.1 查看所有命令

php artisan list

1.3.2 常用命令(必须会)

1.3.2.1 创建控制器
php artisan make:controller PostController
1.3.2.2 创建模型
php artisan make:model Post
# 这里的 Post 是模型名称
1.3.2.3 创建模型 + 迁移 + 控制器(超常用)
php artisan make:model Post -mcr
# 或 php artisan make:model Post -mcr
# 针对 JWT/API 开发:php artisan make:model Post -ma

👉 -mcr 等价于:

  • -m → migration(生成数据库迁移文件)
  • -c → controller(生成控制器文件)
  • -r → resource controller

👉 -mc 等价于:

  • -m → migration(生成数据库迁移文件)
  • -c → controller(生成空的控制器文件)

👉更推荐的组合(针对 JWT/API 开发):

php artisan make:model Post -ma

👉 -ma 等价于:

  • -m:Migration(迁移文件)。
  • -aController (API)。它会生成一个“API 资源控制器”,只包含 index, store, show, update, destroy 这 5 个方法,去掉了 API 不需要处理的页面跳转方法。
1.3.2.4 生成结果:
app/Models/Post.php
database/migrations/xxxx_create_posts_table.php
app/Http/Controllers/PostController.php
1.3.2.5 启动服务器

默认端口是 8000

php artisan serve

可以自定义端口

php artisan serve --port=7890
1.3.2.6 数据库迁移
php artisan migrate
1.3.2.7 示例:迁移文件
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();              // 主键
        $table->string('title');   // 标题
        $table->text('content');  // 内容
        $table->timestamps();     // created_at & updated_at
    });
}
1.3.2.8 清缓存(开发必备)
php artisan cache:clear
php artisan route:clear
php artisan config:clear

1.4 小总结(重点记住)

你现在已经掌握:

✔ Laravel 怎么安装
✔ 项目怎么跑起来
✔ 三大核心目录:

  • app/ 👉 写逻辑
  • routes/ 👉 控制入口
  • resources/ 👉 页面展示

✔ Artisan 常用命令

2. Laravel 核心架构 (The Big Three)

这是 Laravel 最核心的三大模块:
👉 路由 + 控制器
👉 数据库 + 模型(Eloquent)
👉 Blade 模板

掌握这三块,基本就能做 80% 的 Web 应用了。

2.1 路由与控制器 (Routing & Controllers)

2.1.1 路由:代码写在哪里?

在 Laravel 中,路由就像是公司的“前台”。

  • 文件位置:项目根目录下的 routes/ 文件夹。
  • 网页路由routes/web.php(你在浏览器地址栏输入网址访问的)。
  • 接口路由routes/api.php(给 App 或小程序调用的,注意:访问时会自动带上 /api 前缀)。

2.1.2 路由的“内置语法”总结

路由最常用的语法是 Route::方法名('路径', 处理逻辑)

语法方法作用场景举例
Route::get()获取数据/页面访问首页、查看文章
Route::post()提交数据注册账号、发布评论
Route::put()修改数据修改个人资料、重置密码
Route::delete()删除数据删除某条动态
2.1.2.1 语法示例(小白最爱用的闭包写法):
// 文件路径:routes/api.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// 1. 【GET】访问地址:http://localhost/api/about
Route::get('/about', function () {
    // API 通常返回数组,Laravel 会自动转为 JSON
    return [
        'app_name' => '我的第一个 API',
        'version' => '1.0.0'
    ];
});

// 2. 【GET】带必选参数:http://localhost/api/user/10
Route::get('/user/{id}', function ($id) {
    return [
        'message' => "正在查询 ID 为 {$id} 的用户",
        'status' => 'success'
    ];
});

// 3. 【POST】提交数据:通常用工具(如 Postman)模拟访问 http://localhost/api/register
Route::post('/register', function (Request $request) {
    // request()->all() 可以获取用户上传的所有 JSON 数据
    $data = $request->all();
    
    return [
        'message' => '账号创建成功!',
        'user_data' => $data
    ];
});

// 4. 【DELETE】删除数据:访问 http://localhost/api/user/5
Route::delete('/user/{id}', function ($id) {
    return [
        'action' => 'delete',
        'target_id' => $id,
        'result' => '用户已被删除'
    ];
});

2.1.3 控制器:代码写在哪里?(API 改写版)

由于 API 通常返回 JSON 数据,我们的控制器写法和路由绑定需要稍作调整。

2.1.3.1 第一步:编写 API 控制器逻辑

文件位置app/Http/Controllers/Api/UserController.php
(注:为了整洁,API 的控制器通常放在 Api 文件夹下,但放在根目录下也可以)

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    // 查看所有用户:GET /api/users
    public function index()
    {
        return [
            ['id' => 1, 'name' => 'Tom'],
            ['id' => 2, 'name' => 'Jack']
        ]; // 直接返回数组,Laravel 自动转 JSON
    }

    // 创建新用户:POST /api/users
    public function store(Request $request)
    {
        // 接收 POST 过来的数据
        $name = $request->input('name');
        
        return [
            'status' => 'success',
            'message' => "用户 {$name} 已创建"
        ];
    }

    // 查看单个用户:GET /api/users/{id}
    public function show($id)
    {
        return [
            'id' => $id,
            'name' => '查询结果',
            'email' => 'user@example.com'
        ];
    }
}
2.1.3.2 第二步:在 API 路由里绑定

文件路径routes/api.php

use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

// 1. 获取列表
Route::get('/users', [UserController::class, 'index']);

// 2. 获取详情
Route::get('/users/{id}', [UserController::class, 'show']);

// 3. 提交数据
Route::post('/users', [UserController::class, 'store']);

2.1.4 常用 API 内置助手速查表

在写 API 时,你不会用到 view()(那是返回网页的),你会用到这些:

语法作用API 示例
return [数组];自动转 JSONreturn ['code' => 200];
response()->json();手动控制 JSON 响应return response()->json($data, 201); // 201 表示创建成功
$request->input('key')获取某个具体的输入值$email = $request->input('email');
$request->only(['a','b'])只接收指定的字段$data = $request->only(['name', 'email']);

2.1.5 总结口诀(API 版):

  1. 定地址 →\rightarrowroutes/api.phpRoute::get/post... (记得访问时加 /api 前缀)。
  2. 建文件 →\rightarrow 运行 php artisan make:controller UserController
  3. 写逻辑 →\rightarrowapp/Http/Controllers/ 里的方法直接 return 数组。
  4. 牵红线 →\rightarrow 在路由里用 [UserController::class, '方法名'] 绑定。

2.1.6 终极懒人包:API 资源路由

2.1.6.1 第一步:一键生成控制器

在终端输入以下命令:

# --api 参数会自动帮你生成 index, store, show, update, destroy 这 5 个方法
# 它会跳过网页版才需要的 create 和 edit(因为 API 不需要返回填表页面)
php artisan make:controller UserController --api

文件位置app/Http/Controllers/UserController.php
你会发现 Laravel 已经帮你把方法名都取好了,你只需要在里面填逻辑。

2.1.6.2 第二步:一键注册路由

打开 routes/api.php,删掉之前零散的路由,只写这一行:

use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

// 这一行等于帮你写了 5 行路由!
Route::apiResource('users', UserController::class);

2.1.7 懒人包生成的“全家桶”对照表

这一行 apiResource 到底帮你干了什么?看下表就清楚了:

请求方式URL 地址 (需加 /api)控制器方法语义
GET/usersindex获取所有用户列表
POST/usersstore创建一个新用户
GET/users/{user}show获取某个用户的详情
PUT/PATCH/users/{user}update修改某个用户信息
DELETE/users/{user}destroy删除某个用户

2.1.8 练习:完善你的 API 控制器逻辑

现在打开 app/Http/Controllers/UserController.php,我们可以快速填入一些模拟逻辑:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    // 1. 列表
    public function index()
    {
        return ['data' => [['id' => 1, 'name' => 'Tom']]];
    }

    // 2. 新增
    public function store(Request $request)
    {
        return response()->json(['message' => '创建成功'], 201);
    }

    // 3. 详情
    public function show($id)
    {
        return ['id' => $id, 'name' => 'Tom'];
    }

    // 4. 更新
    public function update(Request $request, $id)
    {
        return ['message' => "用户 {$id} 已更新"];
    }

    // 5. 删除
    public function destroy($id)
    {
        return response()->json(null, 204); // 204 表示删除成功且无内容返回
    }
}

2.1.9 为什么这是“懒人包”?

  1. 统一规范:全公司的程序员看到 index 都知道是查列表,看到 store 都知道是保存。
  2. 代码整洁:你的路由文件 (api.php) 不再会有几十行 Route::get,而是一排整齐的 apiResource
  3. 自动化:结合我们之前讲的 2.2 数据库模型,你甚至可以在这些方法里直接写 User::all()User::create(),开发效率极高。

总结口诀:

  • 生成make:controller --api
  • 路由Route::apiResource
  • 方法index/store/show/update/destroy

2.2 数据库与模型 (Eloquent ORM)

Laravel 最强大的地方之一:👉 ORM(对象关系映射)

2.2.0 数据库配置与连接(以MySQL为例)

在 Laravel 中,你不需要在代码里写 mysqli_connect。所有的环境配置都集中在项目根目录的 .env 文件中。

2.2.0.1 修改 .env 文件

找到以下以 DB_ 开头的配置项,根据你的 MySQL 实际情况进行修改:

DB_CONNECTION=mysql          # 数据库类型
DB_HOST=127.0.0.1            # 数据库地址(本地通常是 127.0.0.1)
DB_PORT=3306                 # 端口
DB_DATABASE=my_laravel_db    # 你在 MySQL 中手动创建的数据库名
DB_USERNAME=root             # 数据库用户名
DB_PASSWORD=root             # 数据库密码
2.2.0.2 在终端手动创建数据库

注意: Laravel 不会自动帮你创建数据库(Database),你需要在执行迁移前手动创建。

打开终端,输入以下命令:

# 1. 登录 MySQL (会提示输入密码)
mysql -u root -p

# 2. 创建数据库 (注意要和 .env 里的 DB_DATABASE 一致)
CREATE DATABASE my_laravel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

# 3. 退出
exit;

(注:如果你使用的是 Laravel 11,执行 php artisan migrate 时如果数据库不存在,系统会询问是否自动创建,输入 yes 即可。)

2.2.0.3 确认 PHP 扩展

确保你的 PHP 环境已经开启了 pdo_mysql 扩展,否则连接会失败。


2.2.1 Migrations(数据库迁移)

👉 用代码管理数据库结构。你可以把它看作数据库的“版本控制系统”。

2.2.1.1 创建迁移文件
# 命令格式:php artisan make:migration create_表名_table
# ------------------------------------------------------------
# 1. Laravel 看到 "create_":
#    会自动在文件里写下 Schema::create('...', function...); —— 这是【建表】模板。
#
# 2. Laravel 看到中间的 "users":
#    会自动把这个词填进代码,设为【数据库表名】。如果你写 orange,表名就叫 orange。
#
# 3. Laravel 看到 "_table":
#    只是为了符合命名规范,让它看起来更像一个关于数据库表的动作。
# ------------------------------------------------------------

php artisan make:migration create_users_table
2.2.1.2 迁移文件示例(含常用内置字段)

文件路径:database/migrations/xxx_create_users_table.php

public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        // --- 常用内置字段方法 ---
        $table->id();                       // 主键 ID (BigInt UNSIGNED)
        $table->string('name', 100);        // VARCHAR,长度 100
        $table->string('email')->unique();  // 唯一索引
        $table->string('password');         // 密码
        $table->text('bio')->nullable();    // 长文本,允许为空
        $table->integer('age')->default(18);// 整数,默认值 18
        $table->decimal('balance', 8, 2);   // 小数(金额),总8位,2位小数
        $table->boolean('is_active');       // 布尔值 (TinyInt)
        $table->json('settings');           // JSON 类型存储
        
        // --- 记录时间 ---
        $table->timestamps();               // 自动创建 created_at 和 updated_at
        $table->softDeletes();              // 软删除字段 deleted_at
    });
}

迁移字段方法 vs 数据库类型对照表

Laravel 方法对应 MySQL 类型适用场景小贴士
$table->id()BIGINT UNSIGNED AUTO_INCREMENT主键 ID每张表必须有一个,默认叫 id
$table->string('字段名', 100)VARCHAR(100)用户名、标题、邮箱最常用。不传第二个参数默认是 255 位。
$table->text('字段名')TEXT文章内容、个人简介存放长文本,没有字符长度限制(或者说很大)。
$table->integer('字段名')INT年龄、排序、计数纯整数。
$table->boolean('字段名')TINYINT(1)状态开关只有 0 (假) 和 1 (真)。
$table->decimal('字段名', 8, 2)DECIMAL(8,2)价格、金额精确小数。8 是总位数,2 是小数点后几位。
$table->json('字段名')JSON复杂配置、多选项适合存放不固定的格式数据。
$table->timestamps()created_at & updated_at数据记录时间必带。Laravel 自动帮你维护这两个时间。

字段修饰符(给字段加“额外规则”)

在定义完字段类型后,常需要给字段加一些特殊限制,这时候就要用链式调用

修饰符作用示例代码
->nullable()允许为空$table->string('avatar')->nullable();
->default(默认值)设置默认值$table->integer('score')->default(0);
->unique()唯一约束(不能重复)$table->string('phone')->unique();
->comment('备注')添加注释$table->string('status')->comment('0:禁用, 1:启用');
2.2.1.3 执行与回滚命令

你可以把这看作是数据库的“后悔药”和“前进键”。

命令它的作用(白话文)适用场景
php artisan migrate执行所有新写的迁移文件。刚写好建表或加字段的代码,需要同步到数据库。
php artisan migrate:rollback撤销(回滚)最后一次迁移操作。刚才运行的迁移写错了(比如字段名打错),撤回后改代码重来。
php artisan migrate:refresh格式化:先全部撤销,再全部重新执行。开发初期,想清空所有测试数据并更新所有表结构。
php artisan migrate:status查看哪些文件运行了,哪些还没运行。确认你的代码是否已经生效。

💡 关于“回滚”的深度笔记(小白必记):

  1. 原理:Ctrl + Z
    回滚执行的是迁移文件里的 down() 方法。如果你在 up() 里建了表,down() 就会把这张表 Drop(彻底删除)

  2. 数据丢失警告 ⚠️
    回滚不仅仅是撤回代码,它会删除数据库里对应的真实数据

    • 例子:你回滚了 create_users_table,那么 users 表里的 1000 个用户数据会瞬间消失,且无法找回。
  3. “最后一次”是指什么?
    Laravel 会记录你每次运行 php artisan migrate 的批次。如果你刚才一次性运行了 3 个迁移文件,那么执行一次 rollback 会把这 3 个文件代表的操作全部撤销

  4. 进阶撤回
    如果你只想撤回最后 3 步,可以使用:
    php artisan migrate:rollback --step=3


🔑 总结口诀:

  • 没表变有表 →\rightarrow migrate
  • 有表变没表 →\rightarrow rollback
  • 想推倒重来 →\rightarrow refresh
2.2.1.4 中途添加/修改字段(标准做法)
1️⃣ 第一步:生成专门的“修改”迁移文件

在终端输入以下命令:

# 语义化命名:add_字段名_to_表名_table
# --table 指定要给哪张表加字段
php artisan make:migration add_phone_to_users_table --table=users
2️⃣ 第二步:编写逻辑

找到新生成的文件(在 database/migrations/ 下最新的一条),你会发现方法从 Schema::create 变成了 Schema::table

public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        // 1. 添加字段(建议加上 nullable,防止旧数据因没有该字段而报错)
        // after('name') 表示把这个新字段放在 'name' 字段后面
        $table->string('phone')->nullable()->after('name'); 
    });
}

public function down(): void
{
    Schema::table('users', function (Blueprint $table) {
        // 回滚时,把这个字段删掉
        $table->dropColumn('phone');
    });
}
3️⃣ 第三步:执行迁移
# 执行迁移
# 1. 【同步】将代码里的表结构真正创建到数据库中
php artisan migrate

# 2. 【撤销】如果发现刚才的迁移写错了(字段名打错等),执行回滚
# 注意:这会删除最后一次迁移所涉及的表和数据!
# php artisan migrate:rollback

# 3. 【重置】推倒重来(慎用!常用于开发初期,快速清空数据并更新所有表)
# php artisan migrate:refresh
💡 核心知识点总结表
场景命令/操作风险
开发初期 (没啥重要数据)直接改原文件,然后跑 php artisan migrate:refresh:会清空表内所有数据。
项目中期 (已有正式数据)新建一个迁移文件,用 Schema::table:只改变结构,保留原数据。
字段放哪?使用 ->after('字段名')无:纯粹为了你在数据库看表时更顺眼。
必填变选填使用 ->nullable()->change()中:需要安装 doctrine/dbal 扩展包才能修改已有字段。
⚠️ 一个“小白”最容易掉的坑

当你添加了新字段(比如 phone)后,你通常会发现:“为什么我用 User::create(...) 存不进手机号?”

原因:你忘记更新 Model(模型) 了!
解决办法
app/Models/User.php 找到 $fillable 数组,把新字段名加进去:

protected $fillable = [
    'name', 
    'email', 
    'password', 
    'phone', // ← 别忘了加它!
];

2.2.2 ORM(模型)

👉 一个 Model 类 = 数据库中的一张表

2.2.2.1 创建模型
# 同时创建模型、迁移文件和控制器 (常用套路)
php artisan make:model Post -mc
2.2.2.2 模型内置属性

文件路径:app/Models/User.php

// 在 Laravel 的标准结构中,所有的模型(Model)默认都存放在 app/Models/ 目录下
class User extends Model
{
    use SoftDeletes; // 开启软删除功能

    // 允许批量赋值的字段(白名单,不写这个 User::create() 会报错)
    protected $fillable = ['name', 'email', 'password'];

    // 隐藏敏感字段(转换为 JSON/数组时自动隐藏,如 API 返回)
    protected $hidden = ['password'];
}
2.2.2.3 增删改查(常用内置“终结者”方法)

注意: get()first() 等方法被称为“执行器”,只有调用它们,Laravel 才会真正去数据库执行 SQL。

操作Eloquent 语法示例
新增数据User::create(['name' => 'Tom', 'email' => 'tom@example.com']);
查询所有$users = User::all(); (静态调用,获取表中全部记录)
执行查询$users = User::where('age', 18)->get(); (获取所有符合条件的结果集合)
只取第一条$user = User::where('name', 'Tom')->first(); (获取符合条件的第一条数据)
根据主键查找$user = User::find(1); (查找 ID 为 1 的数据,找不到返回 null)
严格查找$user = User::findOrFail(1); (找不到直接抛 404 错误,常用于控制器)
分页查询$users = User::paginate(10); (自动分页,每页 10 条,自带分页逻辑)
条件排序查询$user = User::where('age', '>', 18)->orderBy('id', 'desc')->get();
更新数据$user->update(['name' => 'New Name']); (批量更新或单条更新)
保存更改$user->name = 'Jack'; $user->save(); (手动赋值后保存)
删除数据$user->delete(); (根据实例删除,若开启软删除则为逻辑删除)
批量删除User::destroy([1, 2, 3]); (根据主键 ID 直接删除多条)

2.2.3 模型关联(Relationship)

2.2.3.1 定义关联:代码写在哪里?

关联关系必须定义在 app/Models/ 目录下的模型类中。

  • 场景一:一对多(一个用户拥有多篇文章)
    app/Models/User.php 中:

    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class User extends Model {
        // 定义关联:用户可以有多个文章
        public function posts() {
            return $this->hasMany(Post::class); 
        }
    }
    
  • 场景二:多对一/反向关联(这篇文章属于哪个用户)
    app/Models/Post.php 中:

    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class Post extends Model {
        // 定义关联:文章属于一个用户
        public function user() {
            return $this->belongsTo(User::class); 
        }
    }
    
2.2.3.2 关联进阶:如何正确地“拿”数据?

定义好上面的方法后,你就可以在 控制器(Controller) 或者 路由(Route) 中使用了。

💡 核心痛点:N+1 查询问题
如果直接循环读取关联数据,会导致数据库查询次数爆炸。

  • ❌ 错误写法(在 Controller 中)

    // 假设有 100 个用户
    $users = User::all(); // 第 1 次查询:查出所有用户
    
    foreach ($users as $user) {
        // 坑:每次循环都会产生 1 次 SQL 去查当前用户的文章
        // 总共会执行 1 + 100 = 101 次 SQL 查询!
        $userPosts = $user->posts; 
    }
    
  • ✅ 正确写法(使用 with 预加载)

    // 使用 with('方法名')
    // 只有 2 条 SQL:一条查用户,一条用 IN 语句查出所有相关的文章
    $users = User::with('posts')->get(); 
    
    foreach ($users as $user) {
        // 此时数据已经在内存里了,不再产生额外的 SQL 查询
        $userPosts = $user->posts; 
    }
    
2.2.3.3 多对多关联 (Many To Many)

常用于“用户与角色”、“文章与标签”。

  • app/Models/User.php 中定义:

    public function roles() {
        // 一个用户可以拥有多个角色
        return $this->belongsToMany(Role::class);
    }
    
  • 使用方式:

    $user = User::find(1);
    // 获取该用户的所有角色
    $roles = $user->roles; 
    
总结:新手记账秘籍
  1. 定义逻辑:写在 app/Models/ 下的对应的 .php 文件里。
  2. 方法命名:一对多通常用复数(如 posts()),属于关系通常用单数(如 user())。
  3. 取数据:在获取主模型时,记得带上 with('关联方法名') 来保住数据库的命。

2.3 Blade 模板引擎

👉 Blade = Laravel 的前端模板引擎

2.3.1 基础语法

2.3.1.1 输出变量
{{ $name }}

👉 自动防 XSS(安全)

2.3.1.2 条件判断
@if($age > 18)
    成年人
@else
    未成年
@endif
2.3.1.3 循环
@foreach($users as $user)
    <p>{{ $user->name }}</p>
@endforeach

2.3.2 模板继承

2.3.2.1 父模板
<!-- resources/views/layouts/app.blade.php -->

<html>
<head>
    <title>@yield('title')</title>
</head>
<body>

    <h1>网站头部</h1>

    @yield('content')

    <footer>网站底部</footer>

</body>
</html>
2.3.2.2 子模板
@extends('layouts.app')

@section('title', '首页')

@section('content')
    <h2>欢迎来到首页</h2>
@endsection

2.3.3 组件(Component)

👉 更现代的写法(推荐)

2.3.3.1 创建组件
php artisan make:component Alert
2.3.3.2 使用组件
<x-alert type="success" message="操作成功!" />
2.3.3.3 组件模板
<!-- resources/views/components/alert.blade.php -->

<div class="alert alert-{{ $type }}">
    {{ $message }}
</div>

2.4 总结

你可以这样理解 Laravel:

用户请求 → 路由 → 控制器 → 模型 → 数据库
                         ↓
                      返回数据
                         ↓
                     Blade 渲染页面

3. 请求处理与安全性

3.1 表单验证(Validation)

在 Laravel 中,永远不要相信用户输入。验证不仅是为了确保数据格式正确,更是为了防止 SQL 注入等安全问题。

3.1.1 验证的生命周期

当用户点击“提交”按钮后,Laravel 会按以下顺序处理:

  1. 接收数据:获取 $request 中的所有字段。
  2. 匹配规则:对比你定义的 rules
  3. 判定结果
    • 失败:如果是普通网页,自动跳回表单页,并将错误信息存入 Session;如果是 API 请求,直接返回 425 Unprocessable Entity 状态码和 JSON 错误。
    • 成功:继续执行控制器剩下的代码。

3.1.2 三种验证方式对比

方式编写位置适用场景优点
控制器验证Controller 方法内临时、简单的逻辑快速、代码紧凑
Validator 类手动实例化需要手动控制跳转或 Ajax极其灵活,可随时中断
Form Request独立的 Request 类企业级/正式项目推荐! 控制器只负责业务,代码最整洁

3.1.3 方式 3:Form Request 实战(最推荐 ⭐)

3.1.3.1 第一步:生成请求类
php artisan make:request StoreUserRequest
3.1.3.2 第二步:配置验证规则

文件位置:app/Http/Requests/StoreUserRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    // 1. 权限控制:谁能提交这个表单?
    public function authorize(): bool
    {
        // 示例:只有登录用户才能提交。小白练手时先改写为 true。
        return true; 
    }

    // 2. 定义验证规则
    public function rules(): array
    {
        return [
            // 规则之间用 | 隔开,或使用数组 ['required', 'min:3']
            'name'     => 'required|min:3|max:50',
            'email'    => 'required|email|unique:users,email',
            
            // 重要:confirmed 规则要求前端必须有一个字段叫 password_confirmation
            'password' => 'required|min:6|confirmed', 
        ];
    }

    // 3. 自定义属性名称(让错误提示更好看)
    public function attributes(): array
    {
        return [
            'email' => '电子邮箱地址',
        ];
    }

    // 4. 自定义具体错误消息
    public function messages(): array
    {
        return [
            'name.required' => '亲,起个名字吧!',
            'password.min'  => '密码太短了,至少要 6 位数哦。',
        ];
    }
}

3.1.4 常用验证规则分类表

3.1.4.1 一览图
规则写法示例详细说明
必填required字段必须存在且不能为空
可选nullable如果没填就不验证,填了才按后续规则验证
邮箱email必须符合邮箱格式
唯一unique:表名,字段名数据库中不能重复(常用于注册)
长度min:8 / max:20限制字符串长度或数字大小
确认confirmed自动比对 字段名字段名_confirmation
相同/不同same:field / different:field两个字段值必须一致/不一致
正则表达式regex:/^[a-z]+$/i自定义高级匹配逻辑
3.1.4.2 API 请求时怎么用 字段名_confirmation

API 请求体 (JSON):

{
    "name": "Tom",
    "email": "tom@example.com",
    "password": "secret_password",
    "password_confirmation": "secret_password" 
}

注意:这里的 password_confirmation 必须和 password 的值完全一模一样。

返回的错误:

如果前端传的两次密码不一致,前端会收到一个标准的 422 状态码

{
    "message": "The password confirmation does not match.",
    "errors": {
        "password": [
            "两次输入的密码不匹配。"
        ]
    }
}

3.1.5 在 Blade 模板中显示错误

当验证失败回跳时,Laravel 会自动把变量 $errors 注入到所有视图中。

3.1.5.1 顶部集中显示所有错误
@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif
3.1.5.2 字段下方精准提示(更美观)
<input type="text" name="email" value="{{ old('email') }}">
@error('email')
    <div class="text-red-500">{{ $message }}</div>
@enderror

💡 小技巧: 使用 old('字段名') 函数可以回显用户刚才输入的内容,避免用户因为一个错就得重填整个表单。

3.1.6 进阶:自定义验证规则(Rule 类)

如果内置规则不够用,比如你要验证“手机号是否符合特定运营商格式”:

  1. 执行命令php artisan make:rule IsMobile
  2. 编写逻辑 (app/Rules/IsMobile.php):
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    // 正则验证中国大陆手机号
    if (!preg_match('/^1[3-9]\d{9}$/', $value)) {
        $fail('手机号格式不正确。');
    }
}
  1. 在 Request 中使用
'phone' => ['required', new IsMobile()],

3.1.7 三大高频“翻车”现场

  1. confirmed 规则失效

    • 原因:前端 HTML 里的确认框 name 没写对。
    • 解决:如果密码框是 password,确认框必须叫 password_confirmation
  2. unique 规则报错

    • 原因:在“修改资料”时提示 Email 已被占用(其实是被自己占用了)。
    • 解决:需要在规则后排除当前 ID,例如 'email' => 'unique:users,email,'.$this->user()->id
  3. authorize() 返回 false

    • 原因:忘记把默认生成的 return false; 改成 true
    • 现象:提交表单直接报 403 Forbidden

3.2 中间件(Middleware)

3.2.1 基本概念

👉 类似“守门员”

请求流程:

请求 → 中间件 → 控制器 → 响应 → 中间件

3.2.2 中间件类型

类型说明
全局所有请求都会经过
路由指定路由使用
一组中间件

3.2.3 常见内置中间件

'auth'       // 登录验证
'guest'      // 未登录用户
'throttle'   // 限流
'verified'   // 邮箱验证
'csrf'       // CSRF 防护

3.2.4 自定义中间件

php artisan make:middleware CheckAdmin
public function handle($request, Closure $next)
{
    if (auth()->user()?->role !== 'admin') {
        abort(403, '无权限');
    }

    return $next($request);
}

注册(Kernel.php):

protected $routeMiddleware = [
    'admin' => \App\Http\Middleware\CheckAdmin::class,
];

3.2.5 使用方式

路由中使用
Route::get('/admin', function () {
    return '后台';
})->middleware('admin');
控制器中使用
public function __construct()
{
    $this->middleware('auth');
}
分组
Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/dashboard', fn() => '后台');
});

3.2.6 应用场景

  • 登录校验
  • 权限控制
  • API 限流
  • 日志记录

3.3 安全机制(Security)

3.3.1 CSRF 防护

表单必须写:
<form method="POST">
    @csrf
</form>

👉 Laravel 会自动验证 Token

3.3.2 XSS 防护

{{ $name }}   // 自动转义(安全)
{!! $name !!} // 原始输出(危险)

3.3.3 SQL 注入防护

❌ 错误写法:

DB::select("SELECT * FROM users WHERE name = '$name'");

✅ 正确写法:

User::where('name', $name)->get();

3.3.4 文件上传安全

$request->validate([
    'file' => 'required|file|mimes:jpg,png|max:2048'
]);

3.3.5 密码安全

use Illuminate\Support\Facades\Hash;

$password = Hash::make('123456');

验证:

Hash::check('123456', $user->password);

3.3.6 输入数据安全

$name = strip_tags($request->input('name'));

3.4 用户认证(详见第五章)

3.4.1 认证方案

快速安装:
composer require laravel/breeze --dev
php artisan breeze:install
php artisan migrate

3.4.2 认证流程

注册 → 登录 → Session → 访问 → 登出

3.4.3 核心功能

登录
if (Auth::attempt($request->only('email', 'password'))) {
    return redirect()->intended('dashboard');
}
当前用户
$user = auth()->user();
登出
Auth::logout();

3.4.4 扩展认证

API(Sanctum)
composer require laravel/sanctum

3.5 权限控制(详见第五章)

3.5.1 权限方式

方式使用场景
Gate简单判断
Policy复杂权限(推荐)

3.5.2 Gate

Gate::define('edit-post', function ($user, $post) {
    return $user->id === $post->user_id;
});

使用:

if (Gate::allows('edit-post', $post)) {
    // 可以编辑
}

3.5.3 Policy(推荐)

php artisan make:policy PostPolicy --model=Post
public function update(User $user, Post $post)
{
    return $user->id === $post->user_id;
}

控制器:

$this->authorize('update', $post);

3.6 综合最佳实践(重点🔥)

3.6.1 技术选型

功能推荐
验证Form Request
权限Policy
认证Breeze
APISanctum

3.6.2 安全组合

👉 必须同时做:

  • CSRF 防护
  • 后端验证(最重要)
  • 密码 Hash
  • ORM 防注入

3.6.3 常见错误(新手必看❗)

❌ 错误1:只做前端验证

👉 后端必须再验证一遍

❌ 错误2:不做权限控制

👉 用户能随便改别人数据

❌ 错误3:明文密码

👉 必须 Hash

❌ 错误4:接口没加中间件
Route::get('/user', fn() => auth()->user());

👉 应该:

Route::middleware('auth')->get('/user', fn() => auth()->user());

3.7 总结一句话(帮你记住核心)

👉 Laravel 安全三件套:

验证(Validation) + 中间件(Middleware) + 权限(Authorization)

4. FilamentPHP [选学]

👉 一句话理解:

Filament = Laravel 后台管理系统生成器(不用写前端)

你只需要:

  • 写模型(Model)
  • 写一点点配置

👉 就能得到:

  • 后台管理页面(增删改查)
  • 表单
  • 表格
  • 权限控制
  • 仪表盘

4.1 安装与初始化

4.1.1 安装 Filament

composer require filament/filament

4.1.2 初始化后台

php artisan filament:install

👉 会自动做这些事情:

  • 创建后台路由 /admin
  • 安装依赖(Livewire、Tailwind)
  • 配置后台面板

4.1.3 创建管理员用户

php artisan make:filament-user

输入:

Name: admin
Email: admin@test.com
Password: 123456

4.1.4 启动项目

php artisan serve
# 或自定义端口 
# php artisan serve --port=7890

访问默认端口:

http://127.0.0.1:8000/admin

或访问自定义端口:

http://127.0.0.1:7890/admin

👉 登录成功 = 后台就搭好了 🎉

4.2 Resources(资源管理核心🔥)

4.2.1 一键生成 CRUD

假设你有一个模型:

php artisan make:model Product -m

迁移文件:

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');       // 商品名
        $table->decimal('price', 8, 2); // 价格
        $table->text('description')->nullable();
        $table->timestamps();
    });
}

执行:

php artisan migrate

👉 生成 Filament Resource:

php artisan make:filament-resource Product

生成结构:

app/Filament/Resources/ProductResource.php
app/Filament/Resources/ProductResource/Pages/

4.2.2 Form 表单构建(重点🔥)

打开:

ProductResource.php
4.2.2.1 表单示例(带详细注释)
use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            // 输入框
            Forms\Components\TextInput::make('name')
                ->label('商品名称') // 显示名称
                ->required() // 必填
                ->maxLength(100),

            // 数字输入
            Forms\Components\TextInput::make('price')
                ->label('价格')
                ->numeric() // 只能输入数字
                ->required(),

            // 多行文本
            Forms\Components\Textarea::make('description')
                ->label('描述')
                ->rows(4),

            // 日期选择器
            Forms\Components\DatePicker::make('created_at')
                ->label('创建时间'),

        ]);
}
4.2.2.2 常用组件大全(新手必会)
// 输入框
TextInput::make('title')

// 下拉框
Select::make('status')
    ->options([
        'draft' => '草稿',
        'published' => '已发布',
    ])

// 开关
Toggle::make('is_active')

// 文件上传
FileUpload::make('image')

// 富文本编辑器
RichEditor::make('content')

4.2.3 Table 表格构建

use Filament\Tables;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // 文本列
            Tables\Columns\TextColumn::make('name')
                ->label('商品名称')
                ->searchable() // 可搜索
                ->sortable(), // 可排序

            Tables\Columns\TextColumn::make('price')
                ->label('价格'),

            Tables\Columns\TextColumn::make('created_at')
                ->dateTime()
                ->label('创建时间'),
        ])

        ->filters([
            // 筛选器(比如价格筛选)
        ])

        ->actions([
            Tables\Actions\EditAction::make(), // 编辑按钮
            Tables\Actions\DeleteAction::make(), // 删除按钮
        ])

        ->bulkActions([
            Tables\Actions\DeleteBulkAction::make(), // 批量删除
        ]);
}

4.2.4 搜索 & 筛选示例

Tables\Filters\Filter::make('expensive')
    ->query(fn ($query) => $query->where('price', '>', 100))

4.3 Widgets 与自定义

4.3.1 仪表盘统计(Dashboard)

生成 Widget:

php artisan make:filament-widget StatsOverview
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use App\Models\Product;

class StatsOverview extends BaseWidget
{
    protected function getStats(): array
    {
        return [
            // 商品总数
            Stat::make('商品数量', Product::count()),

            // 总价格
            Stat::make('总库存价值', Product::sum('price')),
        ];
    }
}

👉 页面效果:

[ 商品数量: 100 ]
[ 总库存价值: 9999 ]

4.3.2 图表 Widget

php artisan make:filament-widget ProductChart --chart
protected function getData(): array
{
    return [
        'datasets' => [
            [
                'label' => '商品增长',
                'data' => [10, 20, 30, 50],
            ],
        ],
        'labels' => ['1月', '2月', '3月', '4月'],
    ];
}

4.3.3 Action(自定义操作🔥)

👉 比如:导出 PDF、发送通知

4.3.3.1 添加按钮
use Filament\Tables\Actions\Action;

Action::make('download')
    ->label('下载')
    ->action(function ($record) {
        // $record = 当前行数据
        dd($record);
    })
4.3.3.2 带确认弹窗
Action::make('delete')
    ->requiresConfirmation()
    ->action(fn ($record) => $record->delete())
4.3.3.3 通知提示
use Filament\Notifications\Notification;

Notification::make()
    ->title('操作成功')
    ->success()
    ->send();

4.4 实战:完整 CRUD 流程

4.4.1 一步到位流程:

php artisan make:model Post -m
php artisan migrate
php artisan make:filament-resource Post

👉 完成后你已经拥有:

  • 列表页
  • 创建页
  • 编辑页
  • 删除功能
  • 搜索
  • 分页

👉 全部自动生成 😱

4.5 小白避坑指南(非常重要❗)

❌ 错误1:没迁移数据库

php artisan migrate

❌ 错误2:字段名不一致

// 表字段
title

// 表单字段
TextInput::make('title')TextInput::make('name')

❌ 错误3:权限问题(403)

👉 检查:

public static function canViewAny(): bool
{
    return true;
}

❌ 错误4:文件上传失败

👉 必须配置:

php artisan storage:link

4.6 最终总结(帮你建立认知)

4.6.1 Filament 做了什么?

👉 把这些复杂的东西:

前端页面 + 表单 + 表格 + JS + 样式

👉 变成:

PHP 配置

4.6.2 你只需要会:

  • Laravel Model
  • 数据库
  • 一点点配置

4.6.3 一句话理解

Filament = Laravel 后台作弊器

5. Laravel + Flutter 认证与权限

为了让你更系统地掌握 Laravel + Flutter 的认证与权限体系,我为你梳理了一份核心知识点大纲。这套架构不仅适用于登录,也是现代移动端前后端分离开发的通用标准。

5.0 Laravel 自带的用户系统

5.0.1 默认的用户模型

Laravel 自带的用户模型是:App\Models\User

// 在 app/Models/User.php
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    // 默认已经有这些字段:
    // id, name, email, email_verified_at, password, remember_token, created_at, updated_at
}

5.0.2 默认的数据库迁移

// 在database/migrations/xxxx_create_users_table.php

Schema::create('users', function (Blueprint $table) {
    $table->id();                       // 自增ID
    $table->string('name');             // 用户名
    $table->string('email')->unique();  // 邮箱(唯一)
    $table->timestamp('email_verified_at')->nullable();  // 邮箱验证时间
    $table->string('password');         // 加密后的密码
    $table->rememberToken();            // 记住我功能
    $table->timestamps();               // 创建时间和更新时间
});

5.0.3 开箱即用的功能

安装完 Laravel 就有了:

  1. 用户注册/登录/邮箱验证
  2. 密码重置
  3. 记住我功能
  4. CSRF 保护
  5. 加密存储密码

5.0.4 如何添加自定义字段(比如 ‘role’)

5.0.4.1 创建新的迁移文件
php artisan make:migration add_role_to_users_table
// 在迁移文件中
public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('role')->default('user');  // 添加角色字段
        // 默认值是 'user',可以是 'superadmin', 'staff', 'user' 等
    });
}
5.0.4.2 运行迁移
php artisan migrate
// 然后在 User 模型中添加
class User extends Authenticatable
{
    protected $fillable = [
        'name', 'email', 'password', 'role'  // 允许批量赋值的字段
    ];
    
    // 也可以定义角色常量
    const ROLE_SUPERADMIN = 'superadmin'; // 超级管理员
    const ROLE_STAFF = 'staff'; // 管理员
    const ROLE_USER = 'user'; // 普通用户
}
5.0.4.3 定义角色常量

🎯 基本用法对比

❌ 不用常量(硬编码,容易出错)

if ($user->role === 'superuser') {
    // 问题:字符串容易拼错
}

$user->update(['role' => 'supradmin']);  // 拼错了!应该是 'superadmin'

✅ 使用常量(安全可靠)

// 定义在 User 模型中
class User extends Authenticatable
{
    const ROLE_SUPERADMIN = 'superadmin';
    const ROLE_STAFF = 'staff';
    const ROLE_USER = 'user';

    // 判断方法
    public function isSuperAdmin(): bool
    {
        return $this->role === self::ROLE_SUPERADMIN;
    }
}

5.0.5 重要说明

  1. 身份验证(Auth) 和 授权(Gate/Policies) 是分开的:
    • Auth:你是谁?(登录验证)

    • Gate/Policies:你能做什么?(权限检查)

  2. 你之前问的 Gate::define 就是建立在 User 模型基础上的授权系统

  3. Laravel 的认证系统很灵活:
    • 可以用默认的 users 表

    • 也可以自定义表名、字段

    • 还支持 API Token、Socialite 社交登录等

一句话总结:是的,Laravel 自带了 User 模型和全套认证系统,你只需要添加自己的业务字段(如 role)就可以直接用!

5.1 认证机制选择 (Authentication Strategies)

5.1.1 认证机制

5.1.1.1 JWT 是什么(先搞懂这个)

👉 JWT = 登录凭证(token)

用户登录后:

服务器 → 生成 token → 返回给 Flutter
Flutter → 每次请求带 token

📌 服务器不存 session
📌 token 自己携带身份信息

5.1.2 安装 JWT(Laravel)

composer require tymon/jwt-auth
5.1.2.1 发布配置文件(必须做)
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

👉 作用:生成 JWT 配置文件

5.1.2.2 生成密钥(非常重要)
php artisan jwt:secret

👉 作用:

  • 生成加密 token 的 secret
  • 不然 token 会报错

5.1.3 修改 User 模型(核心)

打开:

app/Models/User.php
✨ 改成这样(带超详细注释)
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

/* 继承链条:

User 类继承了 Authenticatable
Authenticatable 是 Model 的子类
这样 User 既是一个“数据库模型”,也是一个“可登录的对象”

*/

class User extends Authenticatable implements JWTSubject
{
    /**
     * 允许批量写入的字段
     * 👉 比如 User::create([...]) 能用这些字段
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * 隐藏字段(不会返回给前端)
     * 👉 防止密码泄露
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * 🧠 JWT 必须方法 1
     * 👉 返回这个用户的唯一 ID(一般就是主键 id)
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * 🧠 JWT 必须方法 2
     * 👉 额外放进 token 的数据(一般不用)
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

5.2 AuthController(登录核心)

创建:

php artisan make:controller Api/AuthController

打开文件:

app/Http/Controllers/Api/AuthController.php

5.2.1 登录接口(重点🔥)

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    /**
     * 🔐 用户登录
     * 👉 email + password → 返回 token
     */
    public function login(Request $request)
    {
        // 1️⃣ 验证前端传来的数据
        $request->validate([
            'email' => 'required|email',   // 必须是邮箱格式
            'password' => 'required'       // 必须有密码
        ]);

        // 2️⃣ 去数据库找用户
        $user = User::where('email', $request->email)->first();

        // 3️⃣ 判断用户是否存在 + 密码是否正确
        if (!$user || !Hash::check($request->password, $user->password)) {

            // ❌ 登录失败
            return response()->json([
                'message' => '账号或密码错误'
            ], 401); // 401 = 没权限
        }

        // 4️⃣ ⭐ 生成 JWT token(核心)
        // 👉 这个 token 就是“登录凭证”
        $token = JWTAuth::fromUser($user);

        // 5️⃣ 返回给 Flutter
        return response()->json([
            'user' => $user,   // 用户信息
            'token' => $token   // 登录凭证
        ]);
    }
}

5.2.2 注册接口

public function register(Request $request)
{
    // 1️⃣ 验证输入
    $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users', // 邮箱不能重复
        'password' => 'required|min:6'
    ]);

    // 2️⃣ 创建用户
    $user = User::create([
        'name' => $request->name,
        'email' => $request->email,

        // 🔐 密码必须加密存储(不能明文!)
        'password' => bcrypt($request->password),
    ]);

    // 3️⃣ 生成 token
    $token = JWTAuth::fromUser($user);

    // 4️⃣ 返回数据
    return response()->json([
        'user' => $user,
        'token' => $token
    ]);
}

5.2.3 获取当前用户(必须登录)

/**
 * 👤 获取当前登录用户
 * 👉 需要 token 才能访问
 */
public function me()
{
    return response()->json(auth()->user());
}

5.2.4 登出

use Tymon\JWTAuth\Facades\JWTAuth;

public function logout()
{
    /**
     * ❌ 让当前 token 失效
     * 👉 等于“退出登录”
     */
    JWTAuth::invalidate(JWTAuth::getToken());

    return response()->json([
        'message' => '退出成功'
    ]);
}

5.2.5 路由保护(重点🔥)

打开 routes/api.php

use App\Http\Controllers\Api\AuthController;
use Illuminate\Http\Request;

/**
 * 🛡 需要登录才能访问的接口
 */
Route::middleware('auth:api')->group(function () {

    // 👤 获取当前用户
    Route::get('/user', function () {
        return auth()->user();
    });

    // 🚪 退出登录
    Route::post('/logout', [AuthController::class, 'logout']);
});

5.2.6 配置 auth

打开:

config/auth.php

找到:

'guards' => [
    'api' => [
        'driver' => 'jwt', 
        'provider' => 'users',
    ],
],

5.3 Flutter 前端

因为 JWT 和 Sanctum 对 Flutter 来说一样:

👉 都是:

Authorization: Bearer token

👉 每次请求自动带 token:

options.headers['Authorization'] = 'Bearer $token';

📌 作用:

  • 自动登录
  • 不用每次手动传 token

5.3.1 安装依赖

dio: ^5.0.0
flutter_secure_storage: ^9.0.0

5.3.2 Token 安全存储

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenStorage {
  final _storage = FlutterSecureStorage();

  // 保存 Token
  Future<void> saveToken(String token) async {
    await _storage.write(key: 'token', value: token);
  }

  // 获取 Token
  Future<String?> getToken() async {
    return await _storage.read(key: 'token');
  }

  // 删除 Token
  Future<void> deleteToken() async {
    await _storage.delete(key: 'token');
  }
}

5.3.3 Dio 封装(非常重要🔥)

import 'package:dio/dio.dart';

class ApiClient {
  late Dio dio;
  final storage = TokenStorage();

  ApiClient() {
    dio = Dio(
      BaseOptions(
        baseUrl: "http://your-api.com/api",
        connectTimeout: Duration(seconds: 5),
      ),
    );

    // 🔥 请求拦截器
  dio.interceptors.add(
    InterceptorsWrapper(
      onRequest: (options, handler) async {

        // 🔐 从本地拿 token
        final token = await storage.getToken();

        // 👉 如果有 token,就加到请求头
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }

       // 🚀 继续发送请求
        return handler.next(options);

        },
      ),
    );
  }
}

5.3.4 登录请求

Future<void> login(String email, String password) async {
  final response = await dio.post('/login', data: {
    'email': email,
    'password': password,
  });

  String token = response.data['token'];

  await storage.saveToken(token);
}

5.3.5 获取用户信息

Future<void> getUser() async {
  final response = await dio.get('/user');

  print(response.data);
}

5.4 权限系统(Authorization / Permission)

在 Laravel 中,权限系统用于控制用户是否可以执行某个操作,例如:

  • 是否允许删除文章
  • 是否允许更新数据
  • 是否允许访问后台

Laravel 提供两种核心方式:

  • Gate(简单权限判断)
  • Policy(基于模型的权限控制,推荐)

5.4.1 Gate 示例

Gate 是 Laravel 提供的一种轻量级权限判断方式,适合简单逻辑,例如“是否管理员”。

5.4.1.1 定义 Gate
use Illuminate\Support\Facades\Gate;

Gate::define('is-admin', function ($user) {
    // 判断用户是否是管理员
    return $user->role === 'admin';
});
5.4.1.2 代码说明
$user->role === 'admin'

含义:

  • 从数据库读取用户角色字段
  • 如果等于 admin → 返回 true
  • 否则返回 false
5.4.1.3 使用 Gate(基础判断)
use Illuminate\Support\Facades\Gate;

if (Gate::allows('is-admin')) {
    // 当前用户是管理员
    echo "欢迎管理员";
}
❌ 反向判断
if (Gate::denies('is-admin')) {
    abort(403, '无权限访问');
}
🚨 推荐写法(自动抛异常)
Gate::authorize('is-admin');

// 如果没有权限,会自动返回 403
echo "通过权限验证";
🎯 Blade 中使用 Gate
@can('is-admin')
    <button>删除用户</button>
@endcan

👉 如果不是管理员,这个按钮不会渲染

🧪 扩展示例:VIP 权限
Gate::define('is-vip', function ($user) {
    // 判断 VIP 是否过期
    return $user->vip_expired_at > now();
});
🧪 扩展示例:多角色权限
// 定义一个名为 "access-dashboard" 的权限规则
// 这个规则用来判断:当前用户能不能访问管理后台
Gate::define('access-dashboard', function ($user) {
    // 检查用户的角色(role)
    // 只有 role 是 "admin" 或 "editor" 的用户才允许访问
    return in_array($user->role, ['admin', 'editor']);
    // 如果是 admin 或 editor → 返回 true → 允许访问
    // 其他角色 → 返回 false → 禁止访问
});
5.4.1.4 在 Controller 中使用
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Gate;
use App\Models\User;

class UserController extends Controller
{
    /**
     * 示例:删除用户(仅限管理员)
     */
    public function destroy($id)
    {
        // 🚨 【最常用】使用 authorize
        // 如果权限验证失败:直接抛出异常,给前端返回 403,且下方的代码永远不会执行。
        // 如果权限验证通过:继续往下运行。
        Gate::authorize('is-admin');

        User::destroy($id);
        
        return response()->json(['message' => '用户已成功删除']);
    }

    /**
     * 示例:获取内部敏感数据
     */
    public function getSensitiveData()
    {
        // ❌ 【反向判断】使用 denies
        if (Gate::denies('is-admin')) {
            // 手动拦截并返回自定义信息
            return response()->json(['message' => '只有管理员能看这个!'], 403);
        }

        return response()->json(['data' => '这是高度机密数据']);
    }

    /**
     * 示例:根据权限返回不同的内容
     */
    public function showProfile()
    {
        // ✅ 【正向判断】使用 allows
        if (Gate::allows('is-admin')) {
            // 管理员看到完整版
            return ["status" => "Admin Full Access", "info" => "所有系统日志..."];
        }

        // 普通用户看到简洁版
        return ["status" => "Regular User", "info" => "基本资料..."];
    }
}
5.4.1.5 Gate 常用方法总结
方法逻辑含义失败后果适用场景
Gate::authorize()必须具备该权限立即中断程序,抛出 403 异常,不执行后续代码。最推荐。用于保护删除、修改等关键操作,简单粗暴。
Gate::allows()是否允许访问?返回 false,程序继续执行用于 if 判断,根据权限显示不同的数据结果。
Gate::denies()是否拒绝访问?返回 true (如果没权限),程序继续执行用于 if 判断,通常用于手动处理拒绝后的逻辑。
5.4.1.6 核心笔记提醒
  1. 自动识别用户:在 Controller 里调用这些方法时,你不需要手动传入当前登录的用户。Laravel 框架会自动从当前的登录状态中抓取 $user 传给 Gate。
  2. 位置建议authorize 建议写在函数的第一行。就像进大门要先刷脸一样,刷脸没过直接赶走,没必要浪费资源跑下面的业务逻辑。
  3. HTTP 状态码:当 Gate 失败时,默认返回的是 403 Forbidden(禁止访问),这能准确告诉前端:我知道你是谁,但你没权力做这件事。

5.4.2 Policy 示例

Policy 是 Laravel 中针对某个模型(Model)的权限控制方式,更适合真实项目。

👉 在 JWT 架构中同样适用,因为 user 来源是 auth()->user()

🧠 补充原理(非常重要)

Policy 的本质是:

Laravel 在你执行某个操作前,自动帮你调用一个“判断函数”

👉 你不用自己调用 Policy
👉 Laravel 会帮你调用

👉 例如流程是:

Flutter 请求 → JWT解析 → auth()->user()
→ Controller → authorize → Policy → 返回 true/false
5.4.2.1 创建 Policy
php artisan make:policy PostPolicy

🧠 作用说明:

👉 创建一个“权限规则文件”
👉 专门写:谁能做什么

5.4.2.2 Policy 文件位置
app/Policies/PostPolicy.php

🧠 理解:

👉 这里就是“权限规则中心”

5.4.2.3 示例:更新文章权限
use App\Models\User;
use App\Models\Post;

class PostPolicy
{
    /**
     * @param User $user  -> 自动注入:当前登录的“人”
     * @param Post $post  -> 显式传入:被操作的“物”(某篇具体的文章)
     */
    public function update(User $user, Post $post)
    {
        // 🧠 核心逻辑:
        // 只有当这篇文章的 user_id 等于当前登录用户的 id 时,才准许修改
        return $user->id === $post->user_id;
    }
}
5.4.2.4 深度拆解:这个 $post 是从哪来的?

很多小白会疑惑:我没在 Policy 里给它赋值,它怎么就有数据了?其实它是从你在 Controller(控制器) 里的调用传过来的。

1. 在 Controller 中,你这样写:

public function update(Post $post) // 假设这是 ID 为 10 的文章
{
    // 你把 $post 丢给了 authorize 方法
    $this->authorize('update', $post); 
}

2. 幕后发生了什么:

  • Laravel 看到你传了一个 Post 实例。
  • 它会去 PostPolicy 里找 update 方法。
  • 它把 当前登录的人 塞进第一个参数 $user
  • 它把你刚才传的 那篇 ID 为 10 的文章 塞进第二个参数 $post
💡 为什么必须要传这个参数?

如果不传 $post,Policy 就成了“瞎子”。

  • 没有 $post 时:你只能做通用判断。比如:“这个人是不是管理员?”(只能决定能不能发帖)。
  • 有了 $post 时:你可以做精准判断。比如:“这个人是不是这篇编号为 99 的文章的作者?”(能决定他能不能改别人的贴)。
🧩 总结:参数的含义
  • 第一个参数 $user:由 Laravel 自动从 JWT/Session 中抓取,不需要你管。
  • 第二个参数 $post:是你想要操作的那条数据库记录。有了它,你才能写出 $user->id === $post->user_id 这种“作者才能改自己文章”的逻辑。

🧠 记住这个公式:

权限 = 谁(user)+做什么(update)+对谁做(user) + 做什么(update) + 对谁做(user+做什么(update+对谁做(post)

5.4.2.5 注册 Policy(如需手动)

👉 📍这个代码要写在:

app/Providers/AuthServiceProvider.php

✏️ 正确写法如下(完整文件位置)

打开:

app/Providers/AuthServiceProvider.php

然后在里面找到 $policies

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Models\Post;
use App\Policies\PostPolicy;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 🔐 模型与 Policy 的绑定关系
     */
    protected $policies = [
        // 👉 告诉 Laravel:
        // Post 这个模型 → 用 PostPolicy 来处理权限
        Post::class => PostPolicy::class,
    ];

    /**
     * 注册权限服务
     */
    public function boot()
    {
        $this->registerPolicies();
    }
}

👉 这一行代码:

Post::class => PostPolicy::class,

翻译成人话就是:

以后只要遇到 Post 相关权限问题
就去找 PostPolicy 这个文件

🧠 解释(很关键):

👉 这一步是在“告诉 Laravel 绑定关系”

Post 这个模型 → 用 PostPolicy 来判断权限
5.4.2.6 控制器中使用 Policy
public function update(Request $request, Post $post)
{
    // 🔥 核心一句:权限安检口
    // 💡 它的逻辑是:[ 没权限 ] → [ 抛异常 ] → [ 自动返回 403 响应 ]
    $this->authorize('update', $post);

    // -----------------------------------------------------------
    // 🛡️ 安全隔离线:
    // 只要代码能执行到这一行,说明上面的 authorize 已经通过了!
    // 如果校验失败,下面的代码(包括数据库操作)“绝对”不会被执行。
    // -----------------------------------------------------------

    // 🧠 内部运行机制:
    // 1. 自动提取:从请求中识别当前登录用户 $user(JWT 解析)。
    // 2. 自动匹配:根据 $post 类型找到对应的 PostPolicy。
    // 3. 自动调用:执行 Policy 里的 update($user, $post)。
    // 4. 自动处理:
    //    - 返回 true  → 继续执行。
    //    - 返回 false → 抛出 AuthorizationException。
    //    - 框架拦截异常 → 自动转换成 HTTP 403 状态码返回给前端。

    $post->update($request->all());

    return response()->json([
        'message' => '更新成功'
    ]);
}
🎯 多种调用方式(按需选择)

除了上面的 $this->authorize,你还会经常看到以下写法,它们都是 Laravel 内置的:

  • 方式 A:模型判断(最直观)
    // 这里的 can() 是 Laravel User 模型内置的方法
    // 语义:当前用户“能不能”对这个“帖子”进行“更新”操作
    if (auth()->user()->can('update', $post)) {
        // 校验通过
    }
    
  • 方式 B:Gate 门面(通用型)
    if (Gate::allows('update', $post)) {
        // 校验通过
    }
    
🧩 Policy 常用内置方法对照表

当你使用命令 php artisan make:policy PostPolicy --model=Post 时,Laravel 会预设以下方法。它们的名称与 Controller 的动作是一一对应的:

Policy 方法名对应场景逻辑参考 (Return)
viewAny文章列表页return true; (通常所有人都能看)
view文章详情页return true;
create发布新文章return $user->role !== 'banned'; (没被封号就能发)
update修改文章return $user->id === $post->user_id; (核心:只能改自己的)
delete删除文章return $user->role === 'admin' || $user->id === $post->user_id;

💡 小白避坑:createviewAny 这种不需要指定某篇特定文章的操作,调用时要传入类名
$this->authorize('create', Post::class);

❌ 对比:传统手动判断方式(不推荐)
// 这种写法虽然能用,但不够“Laravel”
if ($post->user_id !== auth()->id()) {
    abort(403, '你没有权限');
}

🧠 为什么不用这种?

  1. 逻辑分散:权限逻辑写死在 Controller 里,以后改规则要到处找。
  2. 重复劳动:每个方法都要写一遍 if
  3. 不够优雅:无法复用给 Blade 模板或 API 自动过滤。

核心总结(一句话记住)

Policy 是权限说明书,$this->authorize 是拿着说明书去对号入座的检查员。

🧠 核心总结(这一段必须懂)

Policy = 一堆规则函数
Laravel = 自动帮你调用这些规则
JWT = 提供当前 user
5.4.2.7 Blade 中使用 Policy
@can('update', $post)
    <button>编辑</button>
@endcan

@can('delete', $post)
    <button>删除</button>
@endcan

🧠 Blade 在做什么?

渲染页面时:
Laravel 自动调用 Policy 判断
true → 显示按钮
false → 不显示

5.4.3 Flutter 控制 UI

Flutter 端的权限控制主要用于:

👉 控制按钮显示(提升体验)
👉 但不能作为安全控制(必须后端验证)

5.4.3.1 基础示例
if (user.role == 'admin') {
  return ElevatedButton(
    onPressed: () {
      // 调用删除接口
    },
    child: Text("删除"),
  );
}
5.4.3.2 说明

Flutter 判断只是:

  • 控制 UI 是否显示
  • 提升用户体验
  • 防止误操作

⚠️ 但不能防止接口被直接调用

5.4.3.3 推荐写法:封装权限工具
class Permission {
  static bool isAdmin(User user) {
    return user.role == 'admin';
  }

  static bool canDeletePost(User user, Post post) {
    return user.role == 'admin' || user.id == post.userId;
  }
}
5.4.3.4 UI 使用方式
if (Permission.canDeletePost(user, post)) {
  return IconButton(
    icon: Icon(Icons.delete),
    onPressed: () {
      // 调用删除接口
    },
  );
}

5.4.4 正确架构理解(非常重要)

层级作用
Flutter控制 UI 显示
Laravel Gate/Policy权限核心控制
API Controller最终安全验证
⚠️ 常见错误
  • ❌ 只在 Flutter 控制权限
  • ❌ 不在后端做权限判断
  • ❌ 认为隐藏按钮就安全
✅ 正确做法
  • ✔ Flutter 控制显示
  • ✔ Laravel 控制权限
  • ✔ API 做最终拦截

5.5 Token 生命周期管理

5.5.1 过期时间配置

JWT 的安全性核心在于“过期时间”。

  • Sanctum (内置) 修改 config/sanctum.php
  • JWT-Auth (三方) 修改 config/jwt.php
// config/jwt.php 示例
// 以分钟为单位
'ttl' => 60,               // Token 有效期(分钟):建议设短一点
// 如果需要设置 7 天,可写为:60 * 24 * 7 

'refresh_ttl' => 20160,    // 刷新时长(分钟):在此时间内可用旧 Token 换新 Token

🧠 为什么? JWT 一旦发出,服务器无法撤回。设置较短的过期时间能将风险降到最低。

5.5.2 运行机制:后端会自动检查吗?

答案是:肯定的。 但这依赖于 Laravel 的 Middleware(中间件)

  • 自动化拦截:你不需要在 Controller 里写任何判断过期的代码。只要路由被 auth:api 中间件包裹,Laravel 会在请求进入你的代码之前,自动解析 Header 里的 JWT 并检查 exp(过期时间)字段。

  • 什么是“包裹”?(代码演示)
    routes/api.php 中,使用中间件组将需要保护的接口“套”起来:

    // 🛡️ 这里的 middleware('auth:api') 就是安检门
    Route::middleware('auth:api')->group(function () {
        
        // 凡是在这个花括号里的路由,都被“包裹”了
        // 它们都会被自动检查 JWT 是否存在、是否过期、是否合法
        Route::put('/posts/{post}', [PostController::class, 'update']);
        Route::delete('/posts/{post}', [PostController::class, 'destroy']);
        
    });
    
    // 🔓 没被包裹的路由,则不会检查 Token(如文章列表)
    Route::get('/posts', [PostController::class, 'index']);
    
  • 处理结果

    • 未过期:通过安检,正常执行你的控制器代码。
    • 已过期直接中断执行。Laravel 的异常处理器会立即介入,代码运行指针根本无法进入控制器内部,下方的数据库操作(如 update)绝对不会运行。
    • 返回响应:后端会自动向 Flutter 返回 HTTP 401 (Unauthorized) 状态码。

🧠 深度理解:
你可以把中间件想象成**“关卡”**。

  1. Flutter 发起请求 -> 2. 到达 auth:api 关卡 -> 3. 检查过期?
  • ❌ 检查到过期 -> 关卡处直接拦截并返回 401 -> 请求结束
  • ✅ 检查通过 -> 关卡抬杆放行 -> 进入控制器执行逻辑 -> 请求成功

这就是为什么你的 update 方法里不需要写任何 if(token_expired) 的原因——因为能活到那一行的请求,全都是合法的。

💡 小贴士:
在开发调试时,如果发现即使 Token 错了也能访问,请务必检查 routes/api.php,确认该路由是否真的被放进了 middleware('auth:api') 的组里。

5.5.3 自定义失败响应

如果 Laravel 默认的返回格式不符合前端需求(例如 Flutter 需要特定的 JSON 结构,如增加 code 字段),可以通过修改后端的 全局异常处理器 来统一定制。

5.5.3.1 修改文件: app/Exceptions/Handler.php

5.5.3.2 实现代码:

use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;

public function register()
{
    // 🧠 核心逻辑:拦截身份验证异常
    $this->renderable(function (AuthenticationException $e, Request $request) {
        
        // 只有 API 请求(来自 Flutter/Postman 等)才返回自定义 JSON
        if ($request->is('api/*')) {
            return response()->json([
                'code'    => 114514,
                'message' => '登录状态已过期,请重新登录',
                'data'    => null,
            ], 401);
        }
    });
}

5.5.3.3 运行机制:

  • 捕获异常:当路由上的 auth:api 中间件校验 Token 失败时,它会抛出一个 AuthenticationException
  • 重写渲染:Handler 捕获到这个“异常信号”,按照我们定义的 JSON 格式进行包装,而不是返回系统默认的错误文本。

⚠️ 注意: > 尽管自定义了 JSON 的内容,但建议 HTTP 状态码依然保持 401。这样 Flutter 的网络请求库(如 Dio)可以通过 statusCode 第一时间识别出是“权限/认证问题”。

核心总结:
后端就像安检员,你可以决定当安检不通过时,是冷冰冰地关门,还是送上一张前端能读懂的“温馨提示卡”。通过 Handler.php,你可以随心所欲地定制这张“卡片”的内容。

5.5.3.4 $this->renderable 是从哪来的?

很多小白会好奇,这个 renderable 既不是在当前类定义的,也没有看到 import,它是怎么生效的?

1. 家族传承(继承体系)
你的 app/Exceptions/Handler.php 并不是孤立存在的,它继承自 Laravel 框架的核心类:

class Handler extends ExceptionHandler // 这里的 ExceptionHandler 是框架内核提供的

renderable 方法就定义在父类 Illuminate\Foundation\Exceptions\Handler 中。它是 Laravel 8.x 之后引入的一项“黑科技”,专门用于简化 API 的异常处理。

2. 核心原理:类型注入(Type Hinting)
这是 Laravel 最优雅的设计之一。注意闭包函数中的第一个参数:

$this->renderable(function (AuthenticationException $e, Request $request) { ... })
  • 自动匹配:Laravel 会利用 PHP 的反射机制,扫描你闭包里填写的异常类名(如 AuthenticationException)。
  • 精准拦截:当程序抛出异常时,Laravel 会检查该异常是否是你指定的类型。如果是,就执行你的自定义逻辑;如果不是,就跳过,交给下一个处理器。

3. 为什么它是“现代写法”?
在 Laravel 的早期版本中,你必须在一个巨大的 render() 方法里写满 if ($e instanceof ...)。而现在的 renderable 允许你像挂载“插件”一样,为不同的异常单独写处理逻辑:

  • 想处理 Token 过期?挂一个 renderable
  • 想处理数据库找不到记录(404)?再挂一个 renderable
  • 彼此独立,互不干扰,代码极度整洁。

4. 总结:它在 JWT 架构中的角色
Flutter + JWT 的开发中,renderable 充当了“翻译官”的角色:

它把后端冷冰冰的 PHP 异常,翻译成了前端 Flutter 能听懂的 JSON 暗号(如你自定义的 code: 114514)。


💡 提示:如果你在代码里写完发现报错,请检查文件顶部是否正确引入了命名空间:
use Illuminate\Auth\AuthenticationException;


💡 小贴士: 修改 config/jwt.php 后,记得运行 php artisan config:clear 确保配置立即生效!

5.6 安全最佳实践

5.6.1 强制 HTTPS

在生产环境下,必须使用 HTTPS 来防止 JWT 在传输过程中被截获(中间人攻击)。

# .env 文件
APP_URL=https://yourdomain.com

5.6.2 限流(防暴力破解)

防止脚本无限次尝试登录密码或刷新 Token。

// routes/api.php
Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1'); // 每 1 分钟最多尝试 5 次

5.6.3 生产环境不暴露内部错误

严禁将数据库报错或代码行号直接返回给前端,防止泄露服务器路径或表结构。

错误写法(泄露机密):

return response()->json(['error' => $e->getMessage()], 500); 
// 可能会返回 "SQLSTATE[HY000]: Column not found..."

正确写法(模糊化处理):

// 只给前端一个模糊的提示,具体的错误日志写进 storage/logs/laravel.log
return response()->json([
    'message' => '服务器开小差了,请稍后再试'
], 500);

5.7 总结

5.7.1 JWT 登录流程

1. 用户登录
2. Laravel 生成 token
3. Flutter 保存 token
4. 每次请求带 token
5. 后端验证 token

5.7.2 对比 Sanctum

项目SanctumJWT
是否存 session
Flutter适配一般⭐⭐⭐⭐⭐
性能一般
推荐WebApp

6. Laravel + Flutter CRUD

本章节将带你从 0基础 学会:

  • Laravel 后端如何写 CRUD API
  • Flutter 如何调用接口
  • 前后端如何联动

6.1 什么是 CRUD?

CRUD = 四个基本操作:

操作含义
Create创建数据
Read读取数据
Update更新数据
Delete删除数据

我们将实现一个简单的「商品管理系统」。

6.2 Laravel 后端部分

6.2.1 创建项目

6.2.1.1 默认端口
composer create-project laravel/laravel my_project
cd my_project
php artisan serve

访问:

http://127.0.0.1:8000
6.2.1.2 自定义端口
composer create-project laravel/laravel my_project
cd my_project
php artisan serve --port=7890

访问:

http://127.0.0.1:7890

6.2.2 配置数据库

打开 .env

DB_DATABASE=my_db
DB_USERNAME=root
DB_PASSWORD=123456

然后执行迁移:

php artisan migrate

6.2.3 创建模型 + 迁移

php artisan make:model Product -m
修改迁移文件
public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id(); // 主键
        $table->string('name'); // 商品名
        $table->decimal('price', 8, 2); // 价格
        $table->text('description')->nullable(); // 描述
        $table->timestamps(); // 创建时间 + 更新时间
    });
}

执行:

php artisan migrate

6.2.4 创建控制器

php artisan make:controller Api/ProductController

6.2.5 编写 CRUD API

6.2.5.1 代码模板:
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Product;

class ProductController extends Controller
{
    // 获取所有数据 (READ)
    public function index()
    {
        return response()->json(Product::all());
    }

    // 创建数据 (CREATE)
    public function store(Request $request)
    {
        // 验证
        $request->validate([
            'name' => 'required',
            'price' => 'required|numeric'
        ]);

        // 创建
        $product = Product::create($request->all());

        return response()->json($product);
    }

    // 查看单个
    public function show($id)
    {
        return Product::findOrFail($id);
    }

    // 更新 (UPDATE)
    public function update(Request $request, $id)
    {
        $product = Product::findOrFail($id);
        $product->update($request->all());

        return response()->json($product);
    }

    // 删除 (DELETE)
    public function destroy($id)
    {
        Product::destroy($id);

        return response()->json(['message' => 'deleted']);
    }
}
6.2.5.2 详细讲解:
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Product;

class ProductController extends Controller
{
    // ========================
    // 1️⃣ 查询所有数据(READ)
    // ========================
    public function index()
    {
        // Product::all()
        // 👉 查询数据库 products 表里的“所有数据”
        
        // response()->json()
        // 👉 返回 JSON 格式给前端(Flutter)
        return response()->json(Product::all());
    }


    // ========================
    // 2️⃣ 创建数据(CREATE)
    // ========================
    public function store(Request $request)
    {
        // $request = 前端传过来的数据(JSON)

        // ✅ Laravel 内置:数据验证(非常强!)
        $request->validate([
            'name' => 'required',        // 必填
            'price' => 'required|numeric' // 必填 + 必须是数字
        ]);

        // ✅ Laravel ORM(Eloquent)
        // create() = 插入数据库
        // $request->all() = 获取所有字段
        $product = Product::create($request->all());

        // 返回创建后的数据
        return response()->json($product);
    }


    // ========================
    // 3️⃣ 查询单个数据(READ 单个)
    // ========================
    public function show($id)
    {
        // findOrFail()
        // 👉 根据 id 查找
        // 👉 找不到会自动返回 404(超方便!)
        return Product::findOrFail($id);
    }


    // ========================
    // 4️⃣ 更新数据(UPDATE)
    // ========================
    public function update(Request $request, $id)
    {
        // 假设数据库里这条数据 ID 为 5,原始数据是:{"name": "旧手机", "price": 1000}
        $product = Product::findOrFail($id);

        /**
         * 💡 情况 A:前端只改价格 (例如在 Postman 发送了 {"price": 888})
         * $request->all() 拿到的就是 ['price' => 888]
         * update() 后:name 还是 "旧手机",但 price 变成了 888
         */

        /**
         * 💡 情况 B:前端修改全部 (例如在 Flutter 发送了 {"name": "新手机", "price": 2000})
         * $request->all() 拿到的就是 ['name' => '新手机', 'price' => 2000]
         * update() 后:两个字段都会被更新
         */

        /**
         * 💡 情况 C:前端传了干扰字段 (例如 {"name": "手机", "hack": "123"})
         * 如果你在 Model 的 $fillable 里没写 'hack'
         * update() 会自动过滤掉 'hack',只更新 'name',非常安全!
         */

        // 执行更新
        $product->update($request->all());

        // 返回更新后的最新数据给前端
        return response()->json($product);
    }


    // ========================
    // 5️⃣ 删除数据(DELETE)
    // ========================
    public function destroy($id)
    {
        // destroy()
        // 👉 根据 id 删除数据
        Product::destroy($id);

        return response()->json([
            'message' => 'deleted'
        ]);
    }
}
6.2.5.3 Laravel CRUD 核心函数总结
操作类型控制器方法Eloquent 内置函数对应 HTTP 请求方法功能说明
Read (全查)index()Product::all()GET获取表中的所有记录,返回集合。
Create (新增)store()Product::create()POST将请求数据存入数据库(需配置 $fillable)。
Read (单查)show()Product::findOrFail($id)GET查找指定 ID,找不到则直接抛出 404 错误。
Update (更新)update()$model->update()PUT / PATCH找到实例后,根据新数据更新字段。
Delete (删除)destroy()Product::destroy($id)DELETE根据主键 ID 直接删除记录。
6.2.5.4 补充:批量删除
  • Controller 写法:
public function batchDelete(Request $request)
{
    $ids = $request->input('ids');

    if (!$ids || !is_array($ids)) {
        return response()->json([
            'message' => 'Invalid IDs'
        ], 400);
    }

    // 批量删除
    $deleted = Product::whereIn('id', $ids)->delete();

    return response()->json([
        'message' => 'Deleted successfully',
        'deleted_count' => $deleted
    ]);
}
  • 路由配置:
Route::delete('/products/batch-delete', [ProductController::class, 'batchDelete']);
  • 前端请求:Flutter 用 Dio 可以这样传👇
DELETE /api/products/batch-delete

{
  "ids": [1, 2, 3, 4]
}

6.2.6 添加 HTTP 状态码

在基于 Laravel 的 CRUD API 开发过程中,合理设置 HTTP 状态码是接口设计的重要组成部分。状态码不仅用于描述请求处理结果,还能帮助前端(如 Flutter + Dio)快速判断请求是否成功,从而进行相应的业务处理。

6.2.6.1 状态码的作用

HTTP 状态码用于表示服务器对请求的处理结果,主要作用包括:

  • 标识请求是否成功(如 200、201)
  • 标识客户端错误(如 400、404)
  • 标识服务器错误(如 500)
  • 提供统一的前后端交互语义
6.2.6.2 Laravel 中设置状态码的方法

Laravel 提供了统一的响应构造方式,可通过 response() 辅助函数设置状态码:

return response()->json([
    'message' => 'success'
], 200);

其中:

  • 第一个参数为返回数据
  • 第二个参数为 HTTP 状态码
6.2.6.3 常用状态码规范

在本项目中,推荐使用如下状态码约定:

状态码含义使用场景
200OK请求成功(查询、删除等)
201Created创建成功
204No Content删除成功且无返回数据
400Bad Request参数错误
401Unauthorized未登录或认证失败
403Forbidden无权限
404Not Found资源不存在
500Internal Server Error服务器异常
6.2.6.4 批量删除接口示例
(1)返回删除结果
return response()->json([
    'deleted_count' => $deleted
], 200);
(2)无返回内容(推荐 RESTful 风格)
return response()->noContent(); // 状态码 204

6.2.6.5 错误响应示例
return response()->json([
    'message' => 'Invalid IDs'
], 400);
6.2.6.6 前端(Flutter)对状态码的处理

在 Flutter 中使用 Dio 发起请求时,状态码会自动包含在响应对象中:

Response response = await dio.delete('/api/products');

print(response.statusCode);

需要注意:

  • 当状态码为 2xx 时,Dio 默认认为请求成功
  • 当状态码为 非 2xx 时,Dio 会抛出异常(DioException)

示例:

try {
  final response = await dio.delete('/api/products');

  if (response.statusCode == 200) {
    // 处理成功逻辑
  }

} on DioException catch (e) {
  final statusCode = e.response?.statusCode;
  // 处理错误逻辑
}
6.2.6.7 推荐的统一响应结构

为了提高接口可维护性,建议在返回数据中增加业务状态码字段:

return response()->json([
    'code' => 0,
    'message' => 'success',
    'data' => null
], 200);

说明:

  • HTTP 状态码:用于表示请求层级结果
  • code 字段:用于表示业务逻辑结果
  • message:提示信息
  • data:返回数据

6.2.7 设置模型可填充字段和隐藏字段

打开 app/Models/Product.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // ✅ 【白名单】允许批量赋值的字段
    // 只有在这里注册了,create() 和 update() 才能生效
    protected $fillable = [
        'name',
        'price',
        'description'
    ];

    // ✅ 【黑名单】转 JSON 时自动隐藏的字段
    /*
      只要是在这里定义的字段,无论你在控制器里怎么写,都不会暴露给前端。
      常见的返回方式包括:
      1. return Product::all();                 // 返回多个对象的集合
      2. return Product::findOrFail($id);       // 返回单个对象
      3. return response()->json($products);    // 显式转为 JSON 响应
      
      原理:Laravel 在将 Eloquent 对象序列化(转字符串)时,
      底层会通过 toArray() 自动剔除此列表中的字段。
    */
    protected $hidden = [
        'created_at',   
        'updated_at',   
        'internal_code' // 假设你有内部备注或敏感逻辑字段
    ];
}

6.2.8 配置路由

  • 打开 routes/api.php
  • 模板代码:
use App\Http\Controllers\Api\ProductController;

Route::get('/products', [ProductController::class, 'index']);
Route::post('/products', [ProductController::class, 'store']);
Route::get('/products/{id}', [ProductController::class, 'show']);
Route::put('/products/{id}', [ProductController::class, 'update']);
Route::delete('/products/{id}', [ProductController::class, 'destroy']);
  • 详细讲解:
use App\Http\Controllers\Api\ProductController; 
// ↑ 引入控制器(必须写完整命名空间)

// ========================
// 基础 CRUD 路由写法说明
// ========================

// Route::请求方式('URL路径', [控制器类::class, '方法名']);

// 1️⃣ 获取所有商品(READ)
Route::get('/products', [ProductController::class, 'index']);
// GET 请求
// 访问:http://127.0.0.1:8000/api/products
// 调用:ProductController 里的 index() 方法


// 2️⃣ 创建商品(CREATE)
Route::post('/products', [ProductController::class, 'store']);
// POST 请求
// 用于提交数据(新增)
// 数据从 body 传过去(JSON)


// 3️⃣ 获取单个商品(READ 单个)
Route::get('/products/{id}', [ProductController::class, 'show']);
// {id} = 动态参数(变量)
// 例如:/products/1
// Laravel 会自动把 1 传给 show($id)


// 4️⃣ 更新商品(UPDATE)
Route::put('/products/{id}', [ProductController::class, 'update']);
// PUT 请求(也可以用 PATCH)
// 表示修改某一条数据
// 同样通过 {id} 指定是哪一条


// 5️⃣ 删除商品(DELETE)
Route::delete('/products/{id}', [ProductController::class, 'destroy']);
// DELETE 请求
// 删除指定 id 的数据

6.3 使用 Bruno 测试 API

6.3.1 创建

POST /api/products

Body:

{
  "name": "iPhone",
  "price": 9999
}

6.4 Flutter 前端部分

6.4.1 添加依赖

dependencies:
  dio: ^5.0.0

6.4.2 创建 API 类

import 'package:dio/dio.dart';

class ApiService {
  final Dio dio = Dio(BaseOptions(
    baseUrl: 'http://127.0.0.1:8000/api'
  ));

  // 获取列表
  Future getProducts() async {
    return await dio.get('/products');
  }

  // 添加
  Future addProduct(Map data) async {
    return await dio.post('/products', data: data);
  }

  // 更新
  Future updateProduct(int id, Map data) async {
    return await dio.put('/products/$id', data: data);
  }

  // 删除
  Future deleteProduct(int id) async {
    return await dio.delete('/products/$id');
  }
}

6.4.3 显示列表

class ProductPage extends StatefulWidget {
  
  _ProductPageState createState() => _ProductPageState();
}

class _ProductPageState extends State<ProductPage> {
  List products = [];
  final api = ApiService();

  
  void initState() {
    super.initState();
    loadData();
  }

  void loadData() async {
    var res = await api.getProducts();
    setState(() {
      products = res.data;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('商品列表')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (_, i) {
          var item = products[i];
          return ListTile(
            title: Text(item['name']),
            subtitle: Text(item['price'].toString()),
          );
        },
      ),
    );
  }
}

6.4.4 添加数据

api.addProduct({
  "name": "MacBook",
  "price": 15000
});

6.4.5 删除数据

api.deleteProduct(1);

6.5 常见坑(非常重要)

6.5.1 跨域问题

安装:

composer require fruitcake/laravel-cors

6.5.2 访问地址问题

手机不能用:

127.0.0.1

必须改为:

电脑IP

6.5.3 端口问题

确保:

8000 已开放

7. Dcat Admin

明白了!既然你要快点入门 Dcat Admin,而且前端是 Flutter,那我们的策略很明确:

  1. 后端管理 (Web):用 Dcat Admin 快速搭建,用于你(管理员)在电脑上审数据、配置 App 内容。
  2. 前端接口 (App):用 Laravel 写 API(JWT 认证),供 Flutter 调用。

以下是 Dcat Admin 的光速入门指南

7.1 第一步:安装 Dcat Admin

假设你已经有一个安装好的 Laravel 项目:

# 1. 引入 composer 包
composer require dcat/laravel-admin

# 2. 发布资源并运行安装 (它会自动建立管理员表和权限表)
php artisan admin:publish
php artisan admin:install

👉 安装完成后,启动服务器 php artisan serve,然后访问:http://localhost:8000/admin

  • 默认账号: admin
  • 默认密码: admin

7.2 第二步:一键生成 CRUD (核心大招)

你不需要手写代码!Dcat Admin 有一个 “代码生成器”

  1. 登录后台,找到 工具 -> 代码生成器
  2. 填写你的表名(比如 posts),勾选“创建迁移文件”、“创建模型”、“创建控制器”、“创建翻译文件”。
  3. 点击 提交
  4. 运行迁移(如果没点自动运行的话):php artisan migrate
  5. 添加菜单:在后台 系统 -> 菜单 里,把新生成的路由路径(比如 posts)加进去。

现在,你的 Post 管理后台(增删改查)就已经全部跑通了!

7.3 第三步:理解 Dcat 的核心代码结构

生成的控制器在 app/Admin/Controllers/PostController.php。你会看到三个核心方法:

  • grid():控制列表页显示哪些列、哪些搜索框。
  • form():控制新增/编辑页有哪些表单项(文本、图片、日期等)。
  • detail():控制详情页显示。

常用组件示例:

protected function form()
{
    return Form::make(new Post(), function (Form $form) {
        $form->display('id');
        $form->text('title', '标题')->required(); // 文本框
        $form->image('cover', '封面图')->uniqueName(); // 图片上传
        $form->editor('content', '正文'); // 富文本编辑器
        $form->switch('is_public', '是否公开'); // 开关
    });
}

7.4 第四步:同步给 Flutter 提供接口

这就是你现在的双轨并行:

  1. 管理数据:你通过 app/Admin/Controllers/PostController.php 录入新数据。
  2. 展示数据:你在 app/Http/Controllers/Api/PostController.php(之前用 -ma 生成的)里写逻辑,把数据传给 Flutter。

代码逻辑共享:
因为它们都共用同一个模型 app/Models/Post.php。你在后台改了数据,Flutter 那边调 API 看到的就是最新的。

7.5 Flutter 开发者的特别提醒

  1. 文件上传路径
    Dcat Admin 默认把图片上传到 storage/app/public/

    • 记得运行 php artisan storage:link
    • 在给 Flutter 返回 JSON 数据时,图片路径记得加上 url() 函数,变成完整的 http://.../xxx.jpg,否则 Flutter 显示不出来。
  2. 接口测试
    既然你现在有后台了,你可以先在 Dcat Admin 里手动录入几条测试数据,然后再去调你的 API。这样你写 Flutter 的时候,就有真实的 JSON 数据可以解析了。

现在的进度:
你已经安装好 Dcat 并且能看到登录页面了吗?如果有报错(比如数据库连接),记得检查 .env 文件。

8. 大结局?

8.1 流程总结

8.1.1 简单版

Route 定义 (routes/api.php)
    ↓
Route::middleware('auth:api')   // 或在 RouteServiceProvider 中全局加到 api 中间件组
    ↓
JWT 认证(config/jwt.php 配置 ttl、blacklist 等)
    ↓
认证失败 → App\Exceptions\Handler.php(renderable())→ 自定义 JSON 响应 (401/403)
    ↓
认证成功 → 放行
    ↓
路由匹配:Route::get/post/put/delete + Route Model Binding
    ↓
Form Request(如果控制器方法注入了)→ authorize() + rules() + messages() + validated()
    ↓
Controller 方法开始
    ↓
$this->authorize('ability', $modelInstance) // 权限系统:Policy 检查(推荐在这里)
    ↓
业务逻辑(可提取到 Service / Action 类)
    ↓
Model 操作(protected $fillable 保护 mass assignment)
    ↓
Eloquent 查询 / 保存 → 数据库(migration 定义表结构)
    ↓
返回前处理:Model 的 $hidden、$appends、$casts、属性访问器
    ↓
Controller 返回:
    - response()->json(...)
    - 直接返回 Model / Collection(较少用)
    - API Resource / JsonResource(强烈推荐,用于统一格式、隐藏字段、添加 meta 等)

8.1.2 专业版

请求进入 (Request)
    ↓
路由与中间件 (Routing & Middleware)
    - 匹配路由
    - 认证 (JWT / Authentication): 检查 Token 有效性 -> 失败则 401,可重写 renderable() 自定义 JSON 响应
    - 限流 (Throttle): 防止恶意刷接口
    ↓
数据验证 (Form Request Validation)
    - 验证数据格式/字段是否存在
    - 失败则自动返回 422 (Unprocessable Entity)
    ↓
控制器与授权 (Controller & Authorization)
    - Route Model Binding: 自动根据 ID 查出模型实例
    - Policy 检查: $this->authorize() -> 失败则 403 (Forbidden)
    ↓
业务逻辑层 (Service / Action)
    - 复杂的计算、多表联动、第三方 API 调用
    ↓
数据持久化 (Model / Eloquent / migration)
    - 使用 $fillable 过滤字段
    - 触发 Model Events (Observer)
    ↓
响应格式化 (JsonResponse / API Resource)
    - 统一 JSON 结构 (code, data, msg)
    - 隐藏敏感字段,转换字段类型 (Casts)
    ↓
返回响应 (Response 200/201)

8.2 进阶

在 Laravel 中,确实有类似于 Django REST Framework (DRF) 序列化器的功能组件。虽然 Laravel 的实现方式与 DRF 略有不同(DRF 的 Serializer 往往同时负责数据验证数据转换),但 Laravel 将这两个职责进行了拆分。

8.2.1 Laravel 的“双向”替代方案

如果你习惯了 DRF 那种“一个类搞定一切”的风格,在 Laravel 中通常是这样组合的:

  • 处理输入(验证): 使用 Form Request。它负责检查数据是否合法(必填、长度、唯一性)。
  • 处理输出(序列化): 使用 API Resource。它负责把处理后的结果漂亮地展示给用户。

8.3 Form Request

在 Laravel 中,验证数据的合法性是开发者的头等大事。我们通常会经历从“简单粗暴”到“优雅解耦”的过程。

8.3.1 基础:在 Controller 中直接验证

对于刚入门的小白,最直接的方法就是在控制器方法里使用 $request->validate()

代码示例:

public function store(Request $request)
{
    // 直接调用 validate 方法
    // 如果验证失败,Laravel 会自动重定向回上一个页面(网页端)
    // 或者返回 422 状态码和 JSON 错误信息(API端)
    $validated = $request->validate([
        'title'   => 'required|unique:posts|max:255', // 必填 | 在 posts 表中唯一 | 最长 255
        'body'    => 'required',                      // 必填
        'author'  => 'nullable|string',               // 可为空 | 必须是字符串
    ]);
/*
     只有验证通过,才会执行到这里
     $validated 变量包含了所有经过验证的数据
     $request->validate([包含在里面的字段])
    没包含在里面的字段,会被 $validated 彻底丢弃

    例如:request 的内容是 
    {
    "title": "我的第一篇文章",
    "body": "这是内容",
    "is_admin": 1,
    "money": 9999
    }

    那么:
   ❌ {"is_admin": 1,"money": 9999} 会被 $validated 丢弃

*/

    Post::create($validated);

    return response()->json(['message' => '文章发布成功']);
}

注意:这种方式虽然快,但如果你的验证规则有几十条,控制器就会变得非常臃肿。

8.3.2 进阶:使用独立的 Form Request 类

为了保持控制器的整洁(即所谓的“瘦控制器”),我们会把验证规则单独提炼到一个类中。

8.3.2.1 生成 Request 类

在终端运行:

php artisan make:request StorePostRequest
8.3.2.2 配置验证逻辑

打开 app/Http/Requests/StorePostRequest.php,你会发现这个类通过三个主要方法来控制请求的生死存亡

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * 方法一:权限验证 (authorize)
     * 作用:判断“谁”有权发起这个请求。
     * 返回值:必须返回布尔值(true/false)。
     */
    public function authorize(): bool
    {
        // 场景示例:如果是更新文章,可以判断当前用户是不是文章作者
        // return $this->user()->id === $this->post->author_id;
        
        return true; // 小白阶段建议先设为 true,否则会直接报 403 错误
    }

    /**
     * 方法二:验证规则 (rules)
     * 作用:定义具体的验证规则(数据长什么样才合格)。
     * 返回值:返回一个关联数组。
     */
    public function rules(): array
    {
        return [
            'title' => 'required|unique:posts|max:255', // 必填、唯一、限长
            'body'  => 'required',                      // 内容必填
        ];
    }

    /**
     * 方法三:自定义错误提示 (messages) - 【可选】
     * 作用:默认的报错是英文(如 The title field is required.)。
     * 你可以通过这个方法把报错变成温馨的中文。
     */
    public function messages(): array
    {
        return [
            'title.required' => '标题必须填写,不能空着哦!',
            'title.unique'   => '这个标题已经有人用过了,换一个吧。',
            'body.required'  => '正文内容不能为空。',
        ];
    }
}
8.3.2.3 在控制器中使用

现在,你只需要在方法参数里替换掉原来的 Request

// 注意这里改成了 StorePostRequest
public function store(StorePostRequest $request)
{
    // 逻辑非常清爽!
    // 验证逻辑在进入这个方法之前就已经由 Laravel 自动完成了
    $data = $request->validated(); 

/*
   ❌ rules() 里面没有的字段会被丢弃

例如:
    public function rules(): array
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body'  => 'required', 
        ];
    }
而 request 的内容是:
    {
    "title": "我的第一篇文章",
    "body": "这是内容",
    "is_admin": 1,
    "money": 9999
    }

那么:
    $data = $request->validated(); 
    此时 $data 的结果仅为:{"title": "...", "body": "..."}
    ❌ {"is_admin": 1, "money": 9999} 会被彻底丢弃,不会进入数据库!
*/
    
    Post::create($data);

    return response()->json(['message' => '发布成功']);
}

8.3.3 常用验证规则分类表

Laravel 提供了上百种验证规则,作为小白,你只需要先掌握下面这 20% 最常用的,就能应付 80% 的开发场景。

8.3.3.1 基础存在性检查
规则说明示例
required必填。字段不能为空且必须存在。'name' => 'required'
nullable可为空。如果不传或传 null,则通过验证。'bio' => 'nullable'
sometimes存在时验证。只有当字段出现在请求中时才验证。'password' => 'sometimes|min:8'
8.3.3.2 数据类型检查
规则说明示例
string必须是字符串。'title' => 'string'
numeric必须是数字(支持浮点数和整数)。'price' => 'numeric'
integer必须是整数'age' => 'integer'
boolean必须是布尔值(true/false, 1/0)。'is_public' => 'boolean'
array必须是数组。'tags' => 'array'
8.3.3.3 长度与大小限制
规则说明示例
max:value最大值。字符串算字符数,数字算大小,文件算大小 (以KB计)。'title' => 'max:255'
min:value最小值。字符串算字符数,数字算大小,文件算大小 (以KB计)。'password' => 'min:8'
between:min,max范围限制。(同上)'score' => 'between:1,100'
8.3.3.4 格式校验
规则说明示例
email必须符合邮箱格式。'email' => 'email'
url必须是合法的 URL 地址。'website' => 'url'
date必须是合法的日期字符串。'birthday' => 'date'
ip必须是合法的 IP 地址。'last_ip' => 'ip'
regex:pattern正则匹配(终极方案)。'phone' => 'regex:/^1[3-9]\d{9}$/'
8.3.3.5 数据库相关(最常用)
规则说明示例
unique:table,column唯一性。在指定表的某列中不能重复。'email' => 'unique:users,email'
exists:table,column存在性。提交的值必须在表里已存在(常用于外键)。'category_id' => 'exists:categories,id'

👉 注意:column 什么时候可以省略?

当你写 'title' => 'unique:posts' 时,Laravel 会默认认为:

你要校验的数据库字段名,和你请求中的键名(Key)是一模一样的。

  • 你的请求键名title
  • Laravel 默认查找的列名posts 表中的 title 列。

所以,如果两者一致,你完全可以省掉列名。

8.3.3.6 其他逻辑
规则说明示例
confirmed二次确认。会自动检查 字段名_confirmation 字段。'password' => 'confirmed'
in:a,b,c枚举限制。值必须在给定的选项中。'type' => 'in:news,video,image'

8.3.4 规则组合小技巧

rules() 方法中,你可以使用 |(管道符)或者 数组 来组合多个规则。

写法 A(字符串形式):

'email' => 'required|email|unique:users|max:50',

写法 B(数组形式 - 推荐):
当规则中带有正则表达式或变量时,数组写法更安全、更不容易出错。

'phone' => [
    'required',
    'regex:/^1[3-9]\d{9}$/',
    'unique:users,phone'
],

8.3.5 课后小思考

Q:如果我想验证用户上传的头像图片,要求必须是图片格式,且大小不能超过 2MB,该怎么组合规则?

A: 你可以查阅文档使用 imagemax 规则:
'avatar' => 'required|image|max:2048',(注意:max 在处理文件时,单位是 KB)。

8.4 API Resource (数据转换层)

数据存入数据库后,当我们查询并返回给前端时,往往需要对数据进行“加工”。

8.4.1 为什么要用 API Resource?

  • 隐藏敏感隐私:不返回用户密码、手机号。
  • 统一格式:确保所有接口返回的时间格式、字段名是一致的。
  • 重命名键名:数据库字段叫 is_active,前端可能希望叫 status

8.4.2 创建并编写 Resource

生成命令:

php artisan make:resource PostResource

编写 app/Http/Resources/PostResource.php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    /**
     * 将模型数据转换成数组
     */
    public function toArray(Request $request): array
    {
        // $this 指向当前的 Post 模型实例
        return [
            'id'      => $this->id,
            'title'   => $this->title,
            'content' => $this->body, // 把数据库的 body 映射为前端看到的 content
            'date'    => $this->created_at->format('Y-m-d H:i'), // 格式化时间
            
            // 嵌套关联数据:只有当关联的 author 被加载时才显示
            'author'  => new UserResource($this->whenLoaded('author')),
        ];
    }
}

8.4.3 控制器返回数据

使用 Resource 包裹你的模型数据。

use App\Http\Resources\PostResource;
use App\Models\Post;

// 返回单个文章
public function show(Post $post)
{
    return new PostResource($post);
}

// 返回文章列表(带分页)
public function index()
{
    $posts = Post::paginate(10);
    // collection 方法专门处理集合/数组
    return PostResource::collection($posts);
}

8.5 API Resource 是必需的吗?

API Resource 不是必需的,但 AI 强烈建议使用。(但我很少用)

在 Laravel 中,API 响应其实存在一个**“进化阶段”**,你可以根据项目的复杂程度选择不同的写法:

8.5.1 直接返回 Model/Array

这是 Laravel 最基础的特性。当你在 Controller 方法中直接返回一个 Eloquent 模型或数组时,Laravel 的路由组件会自动探测返回值的类型。如果发现是模型或集合,它会调用 toJson() 并自动将 Content-Type 设置为 application/json

// 直接返回模型实例
public function show(User $user) {
    return $user; 
    // 效果等同于 response()->json($user->toArray());
    // 状态码默认为 200
}

// 直接返回集合(Collection)
public function index() {
    return User::all(); 
    // 自动转换为 JSON 数组
}

// 直接返回关联关系
public function posts(User $user) {
    return $user->posts; 
}
  • 优点: 极其快速,零配置,适合快速原型开发。
  • 缺点: 暴露隐私: 除非你在 Model 中定义了 $hidden,否则数据库字段会全部暴露。
    • 结构耦合: 数据库字段改名,API 字段也会跟着碎。
    • 灵活性差: 很难在不修改 Model 的情况下,动态地为某个特定接口增加字段。

8.5.2 基础写法:response()->json()

这种写法手动程度最高,但也最直观。它允许你明确指定状态码和响应头。

public function show($id) {
    $user = User::find($id);
    
    if (!$user) {
        return response()->json(['message' => '用户不存在'], 404);
    }

    return response()->json([
        'data' => [
            'id' => $user->id,
            'name' => $user->name,
            'email' => $user->email
        ],
        'status' => 'success'
    ], 200);
}
  • 优点: 灵活,可以随意构建任何数组结构。
  • 缺点: 代码冗余。如果 10 个接口都要返回用户信息,你就得写 10 遍这个数组转换逻辑。

8.5.3 进阶写法:结合 API Resource + Response

当你使用 API Resource 时,你依然可以使用自定义状态码。Resource 对象本质上会被 Laravel 自动转换成一个 Response 实例。

public function store(Request $request) {
    $user = User::create($request->all());

    // 使用 Resource 格式化数据,并指定 201 Created 状态码
    return (new UserResource($user))
                ->response()
                ->setStatusCode(201);
}

8.5.4 三种方式的对比

你可以把这几种方式看作是处理“数据转换”的不同深度:

写法适用场景特点
直接返回 Model/Array快速原型、内部小工具最快,但无法定制结构,状态码固定为 200。
response()->json()简单的响应、非模型数据你的写法。适合返回简单的错误消息或自定义逻辑。
API Resources复杂项目逻辑复用性最强,像 DRF 一样将“表现层”逻辑抽离。
写法模式备注
直接返回 Model/Array懒人模式Model 即 API。
response()->json()原始模式手动控制每一行数据。
API Resources工业模式像 DRF 一样,把“怎么展示数据”逻辑独立出来

8.5.5 模型中 $hidden 的有效性

在初学 Laravel 时,我们会在 Model(模型)中使用 protected $hidden 来隐藏敏感字段(如 passwordapi_token)。但引入 API Resources 后,这个机制会发生微妙的变化。

8.5.5.1 核心结论:白名单 vs 黑名单
  • Model 的 $hidden 是“黑名单”:它告诉系统“哪些不要显示”。它只在直接序列化模型(直接返回模型或使用 response()->json())时生效。
  • API Resource 是“白名单”:它告诉系统“明确只要显示这些”。它的优先级高于 Model 的 $hidden
8.5.5.2 API Resource 会让 $hidden “失效”

当你使用 API Resource 时,你在 toArray() 方法中手动编写的代码拥有最终决定权

代码演示:

假设你的 User 模型中隐藏了 mobile(手机号):

// app/Models/User.php
class User extends Model {
    // 只有直接返回 Model 或 response()->json() 时,mobile、password 才会隐藏
    protected $hidden = ['mobile', 'password'];
}

如果你在 UserResource 中这样写:

// app/Http/Resources/UserResource.php
public function toArray(Request $request): array
{
    return [
        'id'     => $this->id,
        'name'   => $this->name,
        'mobile' => $this->mobile, // 👈 即使 Model 隐藏了,这里写了就会暴露!
    ];
}

8.5.6 总结

在实际开发中,通常的操作是:

  1. 成功响应: 使用 API Resource(因为它能保证数据结构一致)。
  2. 错误响应/特殊消息: 使用 response()->json(['error' => '...'], 422)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐