📝 表单助手

FormHelper 是 ThinkAdmin 提供的快捷表单助手,能够根据提交的表单数据自动处理保存与更新操作,大幅提升开发效率。

📚 基础概念

🤔 基本介绍

FormHelper(表单助手)是 ThinkAdmin 框架提供的一个工具类,用于简化表单数据的处理。

简单理解:就像表单的"自动处理机",帮你自动判断是新增还是更新,自动验证和保存数据,无需手动编写大量重复代码。

传统方式(不推荐)

// ❌ 传统方式:需要手动处理新增、更新、验证等
public function save()
{
    $data = input('post.');
    $id = input('id');
    
    // 1. 验证数据
    if (empty($data['username'])) {
        $this->error('用户名不能为空');
    }
    
    // 2. 判断是新增还是更新
    if ($id) {
        // 更新操作
        $user = SystemUser::find($id);
        if (empty($user)) {
            $this->error('用户不存在');
        }
        $user->save($data);
    } else {
        // 新增操作
        SystemUser::create($data);
    }
    
    $this->success('保存成功');
}

使用 FormHelper(推荐)

// ✅ 使用 FormHelper:一行代码完成所有操作
public function save()
{
    SystemUser::mForm('form');
}

🤔 使用优势

问题1:代码重复

不使用 FormHelper 时,每个表单都需要重复编写相同的逻辑:

// ❌ 每个表单都要写这些代码
$data = input('post.');
$id = input('id');
if ($id) {
    // 更新
} else {
    // 新增
}
// ... 大量重复代码

问题2:容易出错

手动处理新增、更新等逻辑容易出错:

// ❌ 容易出错:忘记验证、忘记处理数据等
$data = input('post.');
SystemUser::create($data);  // 没有验证,可能出错

问题3:维护困难

当需要修改表单逻辑时,需要在多个地方修改:

// ❌ 需要在多个控制器中修改相同的逻辑
// UserController.php
if (empty($data['username'])) {
    $this->error('用户名不能为空');
}

// OrderController.php
if (empty($data['order_no'])) {
    $this->error('订单号不能为空');
}  // 重复代码

使用 FormHelper 的优势

// ✅ 统一接口,代码简洁
SystemUser::mForm('form');
SystemOrder::mForm('form');

🎯 核心功能

功能1:自动判断操作类型

根据是否有主键 ID 自动判断是新增还是更新:

// 添加用户:表单中没有 id 字段
// POST 数据:['username' => 'admin', 'nickname' => '管理员']
// 结果:执行 INSERT 操作

// 编辑用户:表单中有 id 字段
// POST 数据:['id' => 1, 'username' => 'admin', 'nickname' => '管理员']
// 结果:执行 UPDATE 操作,更新 id=1 的记录

功能2:数据验证

自动验证表单数据:

// 在回调方法中验证
protected function _form_filter(array &$data)
{
    if ($this->request->isPost()) {
        if (empty($data['username'])) {
            $this->error('用户名不能为空');
        }
    }
}

功能3:数据预处理

支持数据转换、设置默认值等:

protected function _form_filter(array &$data)
{
    if ($this->request->isPost()) {
        // 设置默认值
        $data['create_at'] = date('Y-m-d H:i:s');
        // 数据转换
        $data['status'] = intval($data['status']);
    }
}

功能4:模板渲染

自动渲染表单模板:

// GET 请求时,自动加载数据并渲染模板
SystemUser::mForm('form');
// 自动查找模板:view/user/form.html

功能5:回调处理

支持前置和后置回调,灵活处理业务逻辑:

// 前置回调:数据保存前处理
protected function _form_filter(array &$data)
{
    // 验证和处理数据
}

// 后置回调:数据保存后处理
protected function _form_result(bool $result, array $data)
{
    // 处理保存结果
}

📖 相关概念

GET 请求

  • 用于获取数据,显示表单页面
  • 例如:访问 /admin/user/add 显示添加用户表单

POST 请求

  • 用于提交数据,保存表单数据
  • 例如:提交表单保存用户信息

主键(Primary Key)

  • 数据库表中唯一标识一条记录的字段
  • 通常是 id
  • 例如:id = 1 表示第一条记录

回调函数(Callback)

  • 在特定时机自动调用的函数
  • 用于处理自定义逻辑
  • 例如:_form_filter() 在数据保存前调用

模板(Template)

  • 用于显示表单的 HTML 文件
  • 例如:view/user/form.html

⚙️ 工作流程

步骤1:GET 请求(显示表单)

用户访问:/admin/user/add

FormHelper 检查是否有 ID 参数

没有 ID → 显示空表单(新增模式)
有 ID → 查询数据库获取数据 → 显示表单(编辑模式)

渲染表单模板,显示数据

步骤2:POST 请求(保存数据)

用户提交表单

FormHelper 接收表单数据

执行 _form_filter() 回调(数据验证和处理)

判断是新增还是更新(检查是否有 ID)

保存到数据库(INSERT 或 UPDATE)

执行 _form_result() 回调(结果处理)

返回成功或失败响应

完整流程

GET 请求 → 检查 ID → 查询数据 → 渲染模板
POST 请求 → 接收数据 → 验证数据 → 保存数据 → 返回结果

    _form_filter() 回调

    判断新增/更新

    保存到数据库

    _form_result() 回调

🚀 主要功能

  • 自动处理: 根据表单数据自动处理保存与更新
  • 简化操作: 减少手动编写代码的工作量
  • 错误减少: 降低因手动编写代码而产生的错误
  • 提高效率: 显著提升数据操作的处理效率
  • 统一接口: 提供统一的表单处理接口
  • 灵活配置: 支持多种参数配置和自定义

📋 使用场景

  • 数据保存: 表单数据的自动保存
  • 数据更新: 现有数据的自动更新
  • 批量操作: 批量表单数据处理
  • 数据验证: 表单数据验证和处理
  • 模板渲染: 表单模板的自动渲染

⚙️ 使用要求

模型继承

  • 模型必须继承自 \think\admin\Model
  • 确保模型具备与 FormHelper 交互的能力
  • 支持自动处理表单数据的保存与更新

控制器继承

  • 控制器必须继承自 \think\admin\Controller
  • 方便调用 FormHelper 提供的功能
  • 简化数据处理的流程

🔧 工作原理

自动处理流程

  1. 接收数据: 接收表单提交的数据
  2. 数据验证: 自动进行数据验证
  3. 保存更新: 根据条件自动保存或更新
  4. 结果返回: 返回处理结果和状态
  5. 回调处理: 执行前置和后置回调函数

参数配置

  • 模板文件: 指定表单模板文件
  • 主键字段: 设置主键字段名称
  • 查询条件: 定义查询条件
  • 额外数据: 传递额外的数据参数
  • 验证规则: 支持自定义验证规则
  • 事务支持: 支持数据库事务操作

数据处理机制

  • 智能判断: 根据主键自动判断新增或更新
  • 数据过滤: 支持数据过滤和预处理
  • 错误处理: 完善的错误处理机制
  • 日志记录: 自动记录操作日志

调用快捷表单

// 1.0 模型用法(推荐方式)
// 参数 $template 模板文件
// 参数 $field 主键字段
// 参数 $where 查询条件
// 参数 $data 额外数据
SystemUser::mForm($template, $field, $where, $data);

// 1.1 模型通用表单(最常用)
SystemUser::mForm('form');

// 2.0 控制器用法(不推荐,建议使用模型方法)
// 参数 $dbQuery 为模型名称
// 参数 $template 模板文件
// 参数 $field 主键字段
// 参数 $where 查询条件
// 参数 $data 额外数据
$this->_form('SystemUser', $template, $field, $where, $data);

// 2.1 通用修改器(不推荐,建议使用模型方法)
$this->_form('SystemUser');

工作原理详解

系统进一步简化了控制器中的数据添加与更新操作。现在,控制器仅需通过一行代码 SystemUser::mForm('form'); 即可实现数据的快速添加或更新。

自动判断逻辑:

这一行代码会自动根据提交的表单数据判断是执行添加操作还是更新操作:

  • 如果有主键 ID:执行更新操作(UPDATE)
  • 如果没有主键 ID:执行添加操作(INSERT)

示例说明:

// 添加用户:表单中没有 id 字段
// POST 数据:['username' => 'admin', 'nickname' => '管理员']
// 结果:执行 INSERT 操作

// 编辑用户:表单中有 id 字段
// POST 数据:['id' => 1, 'username' => 'admin', 'nickname' => '管理员']
// 结果:执行 UPDATE 操作,更新 id=1 的记录

自动输出响应:

使用了 HttpResponseException 来直接输出响应,这意味着在使用 SystemUser::mForm('form'); 时,控制器无需返回任何内容,系统会自动处理输出。

模板变量传递:

为了满足更多场景的需求,系统还允许在控制器中给模板额外赋值。通过在控制器中设置 $this->username = '变量值';,你可以将变量值传递给模板,并在模板中直接使用 $username 变量。

数据回调处理:

为了增强数据的灵活性和安全性,系统引入了回调操作(参数使用引用)。开发者可以定义 protected function _form_filter(&$data) 方法,在数据添加到数据库之前对数据进行自定义处理或验证。这一功能使得数据在入库前能够进行额外的过滤和校验,提高了数据的质量和安全性。

数据回调处理

对于表单操作,Controller 内置了两个回调方法,如:

// 表单前置操作,允许使用引用更改 data 值
// 参数 $data :会返回待处理的数组,分显示模型和保存模型,也就是 get|post 请求
[_ACTION]_form_filter(array &$data)

// 表单后置操作,返回的 data 为提交的数据
// 参数 $result :为返回保存结果,成功为 true,失败为 false
// 参数 $data :为返回后的数据,默认为带上数据ID,也就是主键,这个与模型定义有关
[_ACTION]_form_result(bool $result, array $data)

以上案例回调函数如果返回 false 时,Controller 默认行为将不会再执行。

回调方法详解

1. _form_filter() 方法

_form_filter() 方法在表单数据保存前调用,用于数据验证和处理:

  • GET 请求:用于准备表单显示数据,如加载关联数据、设置默认值等
  • POST 请求:用于验证和处理提交的数据,如数据转换、业务逻辑验证等
  • 参数引用:使用 &$data 引用传递,可以直接修改数据
  • 返回 false:如果返回 false,将阻止默认的保存操作

2. _form_result() 方法

_form_result() 方法在表单数据保存后调用,用于结果处理:

  • 参数 $result:保存结果,true 表示成功,false 表示失败
  • 参数 $data:保存后的数据,包含主键 ID
  • 返回 false:如果返回 false,将阻止默认的成功/失败响应

实际应用案例

案例1:完整的用户管理表单(深入应用)

<?php
declare(strict_types=1);

namespace app\admin\controller;

use think\admin\Controller;
use think\admin\model\SystemUser;

/**
 * 系统用户管理
 * @class User
 * @package app\admin\controller
 */
class User extends Controller
{
    /**
     * 添加系统用户
     * @auth true
     */
    public function add()
    {
        SystemUser::mForm('form');
    }

    /**
     * 编辑系统用户
     * @auth true
     */
    public function edit()
    {
        SystemUser::mForm('form');
    }

    /**
     * 表单数据处理
     * @param array $data
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    protected function _form_filter(array &$data)
    {
        if ($this->request->isPost()) {
            // 检查资料是否完整
            empty($data['username']) && $this->error('登录账号不能为空!');
            
            // 处理权限配置(实际项目中的权限处理)
            $data['authorize'] = arr2str($data['authorize'] ?? []);
            
            if (empty($data['id'])) {
                // 新增:检查账号是否重复
                $map = ['username' => $data['username'], 'is_deleted' => 0];
                if (SystemUser::mk()->where($map)->count() > 0) {
                    $this->error("账号已经存在,请使用其它账号!");
            }
                // 新添加的用户密码与账号相同
                $data['password'] = md5($data['username']);
            } else {
                // 编辑:不允许修改用户名
                unset($data['username']);
            }
        } else {
            // GET 请求:准备表单数据
            // 权限绑定处理(将字符串转换为数组)
            $data['authorize'] = str2arr($data['authorize'] ?? '');
            // 获取权限列表和基础数据
            $this->auths = SystemAuth::items();
            $this->bases = SystemBase::items('身份权限');
        }
    }

    /**
     * 表单结果处理
     * @param bool $state 保存结果
     * @param array $post 提交的数据
     */
    protected function _form_result(bool $state, array $post)
    {
        if ($state && $this->request->isPost()) {
            // 记录操作日志
            sysoplog('系统用户管理', "保存用户[{$post['id']}]成功");
            
            // 触发用户保存事件
            $this->app->event->trigger('PluginAdminUserSave', [
                'id' => $post['id'],
                'username' => $post['username'] ?? '',
            ]);
            
            // 自定义成功响应
            $this->success('用户保存成功!', 'javascript:history.back()');
        } else {
            // 保存失败时的处理
            $this->error('用户保存失败,请稍后重试!');
    }
}
}

深入应用技巧:

1. GET 和 POST 请求的区别处理

protected function _form_filter(array &$data)
{
    if ($this->request->isGet()) {
        // GET 请求:准备表单显示数据
        // 加载关联数据、设置默认值、权限检查等
        $data['authorize'] = str2arr($data['authorize'] ?? '');
        $this->auths = SystemAuth::items();
        $this->bases = SystemBase::items('身份权限');
        
        // 设置默认值
        if (empty($data['id'])) {
            $data['status'] = 1;
            $data['create_at'] = date('Y-m-d H:i:s');
        }
    } else {
        // POST 请求:处理表单提交
        // 数据验证、转换、业务逻辑处理等
        empty($data['username']) && $this->error('登录账号不能为空!');
        
        // 数据转换
        $data['authorize'] = arr2str($data['authorize'] ?? []);
        
        // 业务逻辑验证
        if (empty($data['id'])) {
            // 新增时的特殊处理
            $data['password'] = md5($data['username']);
        } else {
            // 编辑时的特殊处理
            unset($data['username']);  // 不允许修改用户名
        }
    }
}

2. 复杂业务逻辑处理

protected function _form_filter(array &$data)
{
    if ($this->request->isPost()) {
        // 复杂验证:检查多个条件
        if (empty($data['username']) || empty($data['email'])) {
            $this->error('用户名和邮箱不能为空!');
        }
        
        // 正则验证
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $data['username'])) {
            $this->error('用户名只能包含字母、数字和下划线!');
        }
        
        // 业务规则验证
        if (!empty($data['id'])) {
            // 编辑时:检查是否允许修改
            $user = SystemUser::mk()->find($data['id']);
            if ($user['status'] == 0) {
                $this->error('已禁用的用户不允许修改!');
            }
        }
        
        // 数据预处理
        $data['update_at'] = date('Y-m-d H:i:s');
        if (empty($data['id'])) {
            $data['create_at'] = date('Y-m-d H:i:s');
        }
    }
}

3. 关联数据处理

protected function _form_filter(array &$data)
{
    if ($this->request->isPost()) {
        // 处理一对多关联数据
        $images = $data['images'] ?? [];
        unset($data['images']);  // 从主表数据中移除
        
        // 保存主表数据后,在 _form_result 中处理关联数据
        $this->images = $images;  // 保存到属性中,供 _form_result 使用
    } else {
        // GET 请求:加载关联数据
        if (!empty($data['id'])) {
            $data['images'] = SystemGoodsImage::mk()
                ->where('goods_id', $data['id'])
                ->select()
                ->toArray();
        }
    }
}

protected function _form_result(bool $result, array $data)
{
    if ($result && $this->request->isPost()) {
        // 处理关联数据
        if (!empty($this->images)) {
            $goodsId = $data['id'];
            // 删除旧图片
            SystemGoodsImage::mk()->where('goods_id', $goodsId)->delete();
            // 保存新图片
            foreach ($this->images as $image) {
                SystemGoodsImage::mk()->save([
                    'goods_id' => $goodsId,
                    'image_url' => $image,
                ]);
            }
        }
    }
}

4. 条件查询和默认值

public function edit()
{
    // 使用条件查询,确保只能编辑自己的数据
    $where = ['id' => input('id')];
    if (!AdminService::isSuper()) {
        $where['uuid'] = AdminService::getUserId();
    }
    
    // 传递额外数据到模板
    $data = [
        'canEdit' => AdminService::isSuper(),
    ];
    
    SystemUser::mForm('form', 'id', $where, $data);
}

5. 表单令牌和安全验证

public function save()
{
    // 应用表单令牌(防止重复提交)
    $this->_applyFormToken();
    
    SystemUser::mForm('form');
}

protected function _form_filter(array &$data)
{
    if ($this->request->isPost()) {
        // 安全验证:防止越权操作
        if (!empty($data['id'])) {
            $user = SystemUser::mk()->find($data['id']);
            if (empty($user)) {
                $this->error('用户不存在!');
            }
            
            // 非超级管理员只能修改自己的数据
            if (!AdminService::isSuper() && $user['uuid'] != AdminService::getUserId()) {
                $this->error('只能修改自己的数据!');
            }
        }
    }
}

常见问题和解决方案:

  1. 问题:表单提交后数据没有保存

    • 检查 _form_filter() 是否返回了 false
    • 检查是否有 $this->error() 阻止了保存
    • 检查数据验证是否通过
  2. 问题:编辑时数据没有加载

    • 检查主键字段是否正确
    • 检查 _form_filter() 中 GET 请求的数据准备逻辑
    • 检查查询条件是否正确
  3. 问题:关联数据保存失败

    • _form_result() 中处理关联数据
    • 使用数据库事务确保数据一致性
    • 检查关联表的外键约束
  4. 问题:权限验证不生效

    • 确保方法注释中有 @auth true
    • 检查权限节点是否正确生成
    • 检查用户是否拥有相应权限

#### 案例2:复杂的商品管理表单(带关联数据)

```php
<?php
declare(strict_types=1);

namespace app\admin\controller;

use think\admin\Controller;
use think\facade\Db;

/**
 * 商品管理
 * @class Goods
 * @package app\admin\controller
 */
class Goods extends Controller
{
    /**
     * 添加商品
     * @auth true
     */
    public function add()
    {
        StoreGoods::mForm('form');
    }

    /**
     * 编辑商品
     * @auth true
     */
    public function edit()
    {
        StoreGoods::mForm('form');
    }

    /**
     * 商品表单数据处理
     * @param array $data
     */
    protected function _form_filter(array &$data)
    {
        if ($this->request->isGet()) {
            // GET请求:准备表单数据
            if (!empty($data['id'])) {
                // 编辑模式:加载商品详情
                $data['goods'] = Db::name('StoreGoods')->where('id', $data['id'])->find();
                $data['images'] = Db::name('StoreGoodsImages')->where('goods_id', $data['id'])->select();
                $data['specs'] = Db::name('StoreGoodsSpecs')->where('goods_id', $data['id'])->select();
            } else {
                // 新增模式:设置默认值
                $data['status'] = 1;
                $data['sort'] = 0;
                $data['images'] = [];
                $data['specs'] = [];
            }
        } else {
            // POST请求:处理表单提交
            // 验证必填字段
            if (empty($data['title'])) {
                $this->error('商品标题不能为空');
            }
            if (empty($data['price'])) {
                $this->error('商品价格不能为空');
            }
            if (empty($data['images'])) {
                $this->error('请上传商品图片');
            }
            
            // 生成商品编号
            if (empty($data['code'])) {
                $data['code'] = 'G' . date('YmdHis') . rand(1000, 9999);
            }
            
            // 处理商品图片
            $images = $data['images'] ?? [];
            $data['image'] = $images[0] ?? ''; // 主图
            unset($data['images']); // 移除临时字段
            
            // 处理商品规格
            $specs = $data['specs'] ?? [];
            unset($data['specs']); // 移除临时字段
            
            // 使用事务处理复杂数据
            try {
                $this->app->db->transaction(function () use ($data, $images, $specs) {
                    // 保存商品基本信息
                    if (empty($data['id'])) {
                        $goodsId = $this->app->db->name('StoreGoods')->insertGetId($data);
                    } else {
                        $goodsId = $data['id'];
                        $this->app->db->name('StoreGoods')->where('id', $goodsId)->update($data);
                    }
                    
                    // 保存商品图片
                    $this->app->db->name('StoreGoodsImages')->where('goods_id', $goodsId)->delete();
                    if (!empty($images)) {
                        $imageData = [];
                        foreach ($images as $index => $image) {
                            $imageData[] = [
                                'goods_id' => $goodsId,
                                'image_url' => $image,
                                'sort' => $index,
                                'create_at' => date('Y-m-d H:i:s')
                            ];
                        }
                        $this->app->db->name('StoreGoodsImages')->insertAll($imageData);
                    }
                    
                    // 保存商品规格
                    $this->app->db->name('StoreGoodsSpecs')->where('goods_id', $goodsId)->delete();
                    if (!empty($specs)) {
                        $specData = [];
                        foreach ($specs as $spec) {
                            if (!empty($spec['name']) && !empty($spec['value'])) {
                                $specData[] = [
                                    'goods_id' => $goodsId,
                                    'spec_name' => $spec['name'],
                                    'spec_value' => $spec['value'],
                                    'price' => $spec['price'] ?? $data['price'],
                                    'stock' => $spec['stock'] ?? 0,
                                    'create_at' => date('Y-m-d H:i:s')
                                ];
                            }
                        }
                        if (!empty($specData)) {
                            $this->app->db->name('StoreGoodsSpecs')->insertAll($specData);
                        }
                    }
                });
            } catch (\Exception $e) {
                $this->error("商品保存失败:{$e->getMessage()}");
            }
        }
    }

    /**
     * 表单结果处理
     * @param bool $result 保存结果
     * @param array $data 提交的数据
     */
    protected function _form_result(bool $result, array $data)
    {
        if ($result && $this->request->isPost()) {
            $this->success('商品保存成功!', 'javascript:history.back()');
        }
    }
}

案例3:表单验证和数据处理

/**
 * 表单数据处理 - 通用验证规则
 * @param array $data 表单数据(引用传递,可修改)
 */
protected function _form_filter(array &$data)
{
    if ($this->request->isPost()) {
        // 使用 _vali 方法进行数据验证
        $this->_vali([
            'username.require' => '登录账号不能为空!',
            'username.length' => '账号长度为3-20个字符!',
            'email.require' => '邮箱不能为空!',
            'email.email' => '邮箱格式不正确!',
            'phone.mobile' => '手机号格式不正确!',
            'password.require' => '密码不能为空!',
            'password.length' => '密码长度为6-20个字符!'
        ]);
        
        // 自定义业务验证
        if (!empty($data['username'])) {
            $map = [['username', '=', $data['username']], ['is_deleted', '=', 0]];
            if (!empty($data['id'])) {
                $map[] = ['id', '<>', $data['id']];
            }
            if (SystemUser::mk()->where($map)->count() > 0) {
                $this->error('账号已经存在,请使用其它账号!');
            }
        }
        
        // 数据预处理
        $data['username'] = trim($data['username']);
        $data['email'] = strtolower(trim($data['email']));
        $data['phone'] = preg_replace('/[^0-9]/', '', $data['phone']);
        
        // 密码加密
        if (!empty($data['password'])) {
            $data['password'] = md5($data['password']);
        } else {
            unset($data['password']);
        }
        
        // 设置默认值
        $data['status'] = $data['status'] ?? 1;
        $data['create_at'] = date('Y-m-d H:i:s');
        $data['update_at'] = date('Y-m-d H:i:s');
    }
}

如果是在 ThinkAdmin 后台基于 admin.js 的情况下,可使用 form[data-auto] 来与 $this->_form 配合使用。

🔧 前端配合使用

自动表单提交

<!-- 使用 data-auto 属性自动提交表单 -->
<form data-auto="{:url('form')}" method="post">
    <input type="text" name="name" placeholder="名称">
    <input type="email" name="email" placeholder="邮箱">
    <button type="submit">提交</button>
</form>

手动表单处理

<!-- 手动处理表单提交 -->
<form id="myForm" method="post">
    <input type="text" name="name" placeholder="名称">
    <input type="email" name="email" placeholder="邮箱">
    <button type="button" onclick="submitForm()">提交</button>
</form>

<script>
function submitForm() {
    $.post('{:url("form")}', $('#myForm').serialize(), function(res) {
        if (res.code === 1) {
            layer.msg(res.info);
            // 处理成功后的逻辑
        } else {
            layer.msg(res.info);
        }
    });
}
</script>
最近更新:
Contributors: 邹景立, Anyon