StartMVC开发手册

可以快速上手的开发文档

手册目录

模型

模型(Model)用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法,负责项目中的"数据+业务逻辑"。通过模型可以避免代码重复并实现更好的扩展性。

创建模型

模型存放在模块下的model目录下,模型为一个类,一个模型类就是一个php文件。

命名规范

  • 模型类名:模型名称Model(首字母大写)
  • 模型文件名:模型名称Model.php(首字母大写)
  • 命名空间:app\模块\model
  • 所有模型都继承于基础模型类:startmvc\core\Model

基本结构

// app/home/model/UserModel.php
namespace app\home\model;
use startmvc\core\Model;

class UserModel extends Model
{
 // 定义数据表名
 protected $table = 'users';

 // 定义主键(可选,默认为id)
 protected $pk = 'id';

 // 自定义业务方法
 public function getActiveUsers()
 {
 return $this->findAll('status = 1');
 }
}

实例化模型

在控制器中,可以通过多种方式实例化模型:

通过model()方法载入

namespace app\home\controller;
use startmvc\core\Controller;

class IndexController extends Controller
{
 public function indexAction()
 {
 // 实例化当前模块的模型
 $userModel = $this->model('User');

 // 实例化指定模块的模型
 $adminModel = $this->model('User', 'admin');

 // 链式调用
 $users = $this->model('User')->findAll();
 }
}

直接实例化

use app\home\model\UserModel;
$userModel = new UserModel();

使用静态方法

use app\home\model\UserModel;

$userModel = UserModel::model();

// 临时设置表名
$logModel = UserModel::model('logs');

数据查询

查询单条记录

// 通过ID查询
$user = $userModel->find(5);

// 通过字符串条件查询
$user = $userModel->find('name = "张三"');

// 通过数组条件查询
$user = $userModel->find(['status' => 1, 'type' => 'vip']);

// 指定查询字段
$user = $userModel->find(5, 'id,name,email');

查询多条记录

// 查询全部记录
$users = $userModel->findAll();

// 通过字符串条件查询
$users = $userModel->findAll('age > 18');

// 通过数组条件查询
$users = $userModel->findAll(['department' => '技术部']);

// 带排序
$users = $userModel->findAll([], '*', 'created_at DESC');

// 限制返回条数
$users = $userModel->findAll([], '*', '', 10);

// 带偏移量的分页
$users = $userModel->findAll([], '*', 'id DESC', '0,10'); // 从0开始,取10条

条件查询示例

// 基础条件查询
$users = $userModel->findAll('status = 1');

// 使用比较运算符
$users = $userModel->findAll('age > 18 AND score >= 60');

// 使用IN条件
$users = $userModel->findAll('department IN ("技术部", "市场部")');

// 使用LIKE条件
$users = $userModel->findAll('name LIKE "%张%"');

// 使用BETWEEN条件
$users = $userModel->findAll('created_at BETWEEN "2023-01-01" AND "2023-12-31"');

// 复杂条件
$users = $userModel->findAll('(status = 1 AND age > 20) OR is_vip = 1');

// 涉及用户输入时,请使用参数绑定(? 占位符),严禁拼接到字符串条件中
$users = $userModel->where('age > ? AND status = ?', [intval($minAge), 1])->get();

安全提示:字符串条件属于原生SQL直通入口,仅适用于程序内写死的固定条件,严禁拼接用户输入(如 "age > {$min}")。涉及用户数据时请使用数组条件、参数绑定 where('age > ?', [$min]) 或链式 where('age', '>', $min)。

分页查询

use startmvc\core\Request;

// 设置分页参数
$page = intval(Request::get('page')) ?: 1;
$pageSize = 10;

// 执行分页查询
$result = $userModel->paginate($pageSize, $page, ['status' => 1], 'created_at DESC');

// 获取数据和分页信息
$users = $result['data'];
$pagination = $result['pagination'];
// 在视图中使用
// 数据列表:$users
// 总记录数:$pagination['total']
// 当前页:$pagination['current_page']
// 总页数:$pagination['total_pages']
// 是否有下一页:$pagination['has_more']

数据操作

插入数据

// 单条插入(启用 $timestamps 后,created_at/updated_at 自动写入,无需手动赋值)
$userId = $userModel->insert([
 'name' => '张三',
 'email' => 'zhangsan@example.com',
 'status' => 1,
]);

// 通过data方法设置数据并插入
$userId = $userModel->data([
 'name' => '李四',
 'email' => 'lisi@example.com'
])->insert();

// 批量插入(启用 $timestamps 后逐行自动补充时间戳)
$userModel->insert([
 ['name' => '张三', 'status' => 1],
 ['name' => '李四', 'status' => 1]
]);

更新数据

// 通过ID更新
$result = $userModel->update(['name' => '张三(已修改)'], 5);

// 通过条件更新
$result = $userModel->update(
 ['status' => 0],
 'last_login_at < "2023-01-01"'
);

// 通过数组条件更新
$result = $userModel->update(
 ['score' => 100],
 ['department' => '技术部', 'status' => 1]
);

// 链式操作更新
$result = $userModel->data(['name' => '王五', 'email' => 'wangwu@example.com'])
 ->update([], 5);

保存数据(自动判断插入或更新)

// 插入新记录(返回自增ID)
$result = $userModel->save([
 'name' => '张三',
 'email' => 'zhangsan@example.com'
]);

// 更新已有记录(数据中含主键即走更新,主键不参与SET)
$result = $userModel->save([
 'id' => 5,
 'name' => '张三(已更新)',
 'email' => 'zhangsan@example.com'
]);

// 链式调用
$userModel->data(['name' => '李四'])
 ->data(['email' => 'lisi@example.com'])
 ->save();

删除数据

// 通过ID删除
$result = $userModel->delete(5);

// 通过条件删除
$result = $userModel->delete('status = 0 AND created_at < "2022-01-01"');

// 通过数组条件删除
$result = $userModel->delete(['status' => 0, 'is_deleted' => 1]);

安全提示:不带条件的全表 update()/delete() 会被框架安全守卫拦截并抛出异常。清空表请使用 truncate();如确需全表更新/删除,请先调用 allowFullTable() 显式确认。

模型特性(StartMVC 2.9.1+)

以下ORM特性全部按需声明、默认关闭:未声明的模型行为与传统模式完全一致。启用后自动生效于 find()/findAll()/paginate() 及链式代理查询。

自动验证($rules)

在模型中声明 $rules 后,insert() / update() / save() 会在写库前自动校验数据,无需在控制器中手动调用 Validator。规则语法与「数据验证类」完全一致(支持 required|minlen:3 形式、单反引号标签、双反引号自定义消息、无限级嵌套)。

class UserModel extends Model
{
 protected $table = 'users';

 protected $rules = [
 'username' => 'required|minlen:3|maxlen:20`用户名`',
 'email' => 'required|is_email`邮箱`',
 'age' => 'is_numeric|gte:18|lte:120',
 ];
}
$userModel = $this->model('User');

// 验证通过:正常写入
$userId = $userModel->insert(['username' => 'tom', 'email' => 'tom@example.com']);

// 验证失败:返回 false,错误经 getError() 获取
if (!$userModel->insert(Request::post())) {
 $errors = $userModel->getError(); // ['username' => '用户名不能为空']
 $text = $userModel->getErrorString(';'); // 字符串形式,便于直接输出
}
失败处理方式($failFast):默认 true 为快速失败——首个字段校验失败即中止,getError() 只包含第一个失败字段的错误;声明 protected $failFast = false; 则收集全部错误,逐字段继续校验,getError() 一次返回所有失败字段(每个字段只记录首个失败规则),适合表单场景一次点亮全部红框。
局部更新友好:未声明 required 的字段在数据中缺失时会自动跳过校验,因此同一套规则可直接用于 update() 局部更新。

表单场景下希望一次性拿到所有字段的错误时,把 $failFast 置为 false:

class UserModel extends Model
{
 protected $table = 'users';

 protected $rules = [
 'username' => 'required|minlen:3`用户名`',
 'email' => 'required|is_email`邮箱`',
 'age' => 'is_numeric|gte:18|lte:120`年龄`',
 ];

 // 收集全部错误(默认 true 为快速失败)
 protected $failFast = false;
}
if (!$userModel->insert(Request::post())) {
 $errors = $userModel->getError();
 // ['username' => '用户名不能为空', 'email' => '邮箱地址错误', 'age' => '年龄必须大于等于18']

 // 前端可据此一次点亮全部红框
 foreach ($errors as $field => $message) {
 // ...
 }
}
无论 $failFast 取何值,校验失败时都不会写入数据库,insert() / update() 均返回 false。

若更新场景需要独立规则,声明 $rulesUpdate(不声明则沿用 $rules):

protected $rulesUpdate = [
 'email' => 'is_email`邮箱`', // 更新时不强制必填,只校验格式
];

自动处理($auto)

$auto 在写库前自动填充字段或转换值:标量值仅当字段缺失(或为 null)时填充,可调用值(函数名 / 闭包)一律调用并覆盖,返回 null 则放弃写入该字段。

class UserModel extends Model
{
 protected $table = 'users';

 protected $auto = [
 'status' => 1, // 标量:缺失时填充默认值
 'name' => 'trim', // 函数名:强制转换
 'password' => function ($v) { // 闭包:收到当前字段值
 return $v === null ? null : password_hash($v, PASSWORD_DEFAULT);
 },
 'insert' => ['reg_ip' => 'get_ip'], // 场景组:仅插入时生效
 'update' => ['updated_uid' => 'current_uid'],
 ];
}
自动处理的可调用值必须能单参调用(如 trim / md5 / strtoupper / 闭包)。多必参函数(如 password_hash)请用闭包包一层,如上例所示。

自动过滤($fillable)

声明 $fillable 后,写入前会剔除白名单之外的字段,可有效防止批量赋值攻击——例如表单中被注入的 is_admin=1 不会入库。

class UserModel extends Model
{
 protected $table = 'users';

 // 允许写入的字段
 protected $fillable = ['username', 'email', 'password', 'status'];
}
$rules 中声明的字段会自动并入白名单,无需在 $fillable 里重复声明。三个属性均为可选,不声明时写入行为与旧版完全一致。

三者组合成完整的写入前管道,模型声明一次、控制器一行完成:

class UserModel extends Model
{
 protected $table = 'users';
 protected $rules = [
 'username' => 'required|minlen:3`用户名`',
 'email' => 'required|is_email`邮箱`',
 ];
 protected $auto = [
 'status' => 1,
 'insert' => ['reg_ip' => 'get_ip'],
 ];
 protected $fillable = ['status', 'reg_ip'];
}
// 控制器:一次调用完成「过滤 → 填充 → 验证 → 写入」
$userModel = $this->model('User');
$result = $userModel->save(Request::post());

if ($result === false) {
 return json(['errors' => $userModel->getError()]);
}
return json(['success' => true, 'result' => $result]);
生效范围:三大自动仅在 insert() / update() / save() 上生效;where()->update() 等链式查询构建器调用属于 Db 层,不经过模型管道。
批量插入:逐行过管道,任一行验证失败则整体返回 false,错误同样经 getError() 获取。

自动时间戳

class UserModel extends Model
{
 protected $table = 'users';
 protected $timestamps = true; // 开关
 protected $createTime = 'created_at'; // 创建时间字段(设为null可单独禁用)
 protected $updateTime = 'updated_at'; // 更新时间字段(设为null可单独禁用)
}

insert() 自动写入 created_at 与 updated_at;update()/save() 自动刷新 updated_at;字段已显式赋值时不覆盖。批量插入同样逐行补充。

软删除

class UserModel extends Model
{
 protected $table = 'users';
 protected $softDelete = true; // 开关(布尔值)
 protected $deleteTime = 'deleted_at'; // 软删除标记字段
}
// delete() 自动改写为更新 deleted_at 标记,而非真实删除
$userModel->delete(5);

// find/findAll/paginate 及链式查询(where()->get()/count() 等)自动排除已删记录
$users = $userModel->findAll();
$count = $userModel->where('status', 1)->count();

// 查询范围:含已删记录
$userModel->withTrashed()->findAll();

// 查询范围:仅已删记录
$userModel->onlyTrashed()->findAll();

// 恢复软删除记录
$userModel->restore(5);

// 真实删除(绕过软删除)
$userModel->forceDelete(5);

已删除的记录同样会被 update() 排除,防止误更新;restore() 会同时刷新 updated_at(如启用时间戳)。

字段类型转换

class UserModel extends Model
{
 protected $table = 'users';
 protected $casts = [
 'price' => 'float',
 'views' => 'int',
 'is_admin' => 'bool',
 'ext' => 'json', // 读取时自动 json_decode 为数组
 ];
}

casts 在 find()/findAll()/paginate() 返回数据时生效;数组/对象写入时由 Db 层自动 JSON 编码、读出自动还原,双向对称。

关联(hasOne / belongsTo)

class UserModel extends Model
{
 protected $table = 'users';

 // 一对一:[属性名 => [关联模型, 关联表外键, 本表主键]]
 protected $hasOne = [
 'profile' => [ProfileModel::class, 'user_id', 'id'],
 ];

 // 反向关联:[属性名 => [关联模型, 本表外键, 关联表主键]]
 protected $belongsTo = [
 'dept' => [DeptModel::class, 'dept_id', 'id'],
 ];
}
// find/findAll/paginate 自动装载关联数据
$user = $userModel->find(5);
echo $user['profile']['bio']; // hasOne:个人资料
echo $user['dept']['title']; // belongsTo:所属部门

// 无关联记录时对应键为 null,结构始终一致

关联采用批量 IN 查询装载,无 N+1 问题;关联行应用关联模型自身的 casts 转换。

写入事件(模型钩子)

在 insert() / update() / save() / delete() 的前后触发事件,把审计日志、缓存失效、消息推送这类横切逻辑从业务代码里挪出去:控制器仍然只写一次 $model->save($data),附加逻辑全部挂在监听器上,业务代码零改动。自 StartMVC 3.1.4 起提供。

共六个事件,按「动作 + 阶段」组合命名(before_ 前置 / after_ 后置):

事件触发时机可否决
model.before_insert插入前,可改写待写入数据是
model.after_insert插入后,result 为自增ID否
model.before_update更新前,可改写待更新数据是
model.after_update更新后,result 为影响行数否
model.before_delete删除前(软删除同样触发)是
model.after_delete删除后,result 为影响行数否

监听器用 Event::listen() 注册,收到的载荷是一个数组:

键说明
model模型类名(含命名空间)
table表名(不含表前缀)
actioninsert / update / delete
data前置事件=待写入数据(可改写);后置事件=实际写入的数据
whereupdate / delete 的条件;insert 无此键
before写入前的旧记录(数组),仅 $auditBefore = true 时非空
result影响行数或自增ID,仅后置事件有

示例 1:插入前自动生成 slug

use startmvc\core\Event;

// 注册一次即全局生效(常见做法:放在 function/ 目录下的自定义函数文件里)
Event::listen('model.before_insert', function (&$payload) {
 if ($payload['table'] !== 'articles') {
 return;
 }
 if (empty($payload['data']['slug'])) {
 $payload['data']['slug'] = strtolower(str_replace(' ', '-', $payload['data']['title']));
 }
});

// 业务代码照常写,slug 会被自动补齐
$articleModel->insert(['title' => 'Hello World', 'slug' => '']);

示例 2:更新后写审计日志(需要旧记录)

先让模型声明开启旧记录查询:

class ArticleModel extends Model
{
 protected $table = 'articles';

 // 开启后 update / delete 会先查出旧记录放进 $payload['before']
 // 代价:每次写入多一次 SELECT,不需要旧记录就不要声明
 protected $auditBefore = true;
}

再挂监听器:

use startmvc\core\Event;
use startmvc\core\Logger;

Event::listen('model.after_update', function ($payload) {
 (new Logger())->info('模型数据已更新 {table}', [
 'table' => $payload['table'],
 'where' => $payload['where'],
 'before' => $payload['before'], // 写入前的旧记录
 'after' => $payload['data'], // 本次写入的数据
 ]);
});

示例 3:否决写入

前置事件返回 false 即中止本次写入,insert() / update() / delete() 返回 false:

Event::listen('model.before_delete', function (&$payload) {
 // 示例:禁止删除 id 为 1 的超级管理员
 if ($payload['table'] === 'users' && $payload['where'] == 1) {
 return false; // 返回 false 即中止本次写入
 }
});

$userModel->delete(1); // → false,记录未被删除
$userModel->delete(2); // → 正常删除

注意事项

  • 监听器要写成 function (&$payload) 才能改写数据。只写 function ($payload) 拿到的是副本,改动会被静默丢弃(不报错),很容易排查半天。
  • 未注册任何监听器时零开销,行为与旧版完全一致。
  • save() 按主键自动路由到 insert() 或 update(),因此只会触发其中一组事件,不会重复。
  • 软删除的 delete() 同样触发 before_delete / after_delete,forceDelete() 亦然;restore() 不触发写入事件。
  • 批量插入时 before_insert 逐行触发(可逐行改写),after_insert 整批只触发一次:此时 data 是二维数组,result 为首个自增ID。
  • 监听器抛出的异常会直接向上传播(insert() 不捕获),事务内请交由事务回滚。
  • 不再需要时用 Event::forget('model.before_insert') 移除监听器。

为什么用 before_ / after_ 前缀:creating / created 这类命名只差一个字母,读代码和排查问题时极易看错。改用 before_ / after_ 后,成对名字相差整个单词,且动作词(insert / update / delete)在两个阶段完全一致,不需要记「动词变过去式」的变形规则。

注册位置:监听器只需注册一次即全局生效,常见做法是放在 function/ 目录下的自定义函数文件中(Web 请求会自动加载)。注意 CLI 环境下框架不会加载 function/,命令行脚本里请自行注册。

直接使用查询构造器

除了使用预定义的方法外,还可以直接使用底层查询构造器进行更灵活的操作:

// 基本查询
$users = $userModel->where('status', 1)
 ->order('created_at', 'DESC')
 ->limit(10)
 ->get();

// 聚合查询
$count = $userModel->where('department', '技术部')->count('id');
$maxScore = $userModel->where('status', 1)->max('score');
$avgAge = $userModel->where('gender', '男')->avg('age');

// 字段查询
$names = $userModel->where('status', 1)->column('name');
$email = $userModel->where('id', 5)->value('email');

// 原生SQL(query + 参数绑定,返回语句对象后取结果)
$rows = $userModel->query('SELECT * FROM users WHERE score > ? AND status = ?', [60, 1])
 ->fetchAll();
$one = $userModel->query('SELECT * FROM users WHERE id = ?', [5])->fetch();

事务处理

// 手动控制事务(transaction() 开始事务,支持嵌套,内层自动使用SAVEPOINT)
$userModel->transaction();
try {
 $userId = $userModel->insert(['name' => '张三']);
 $logModel->insert(['user_id' => $userId, 'action' => '注册']);
 $userModel->commit();
 return true;
} catch (Exception $e) {
 $userModel->rollback();
 return false;
}

表维护操作

// 优化表
$userModel->optimize();

// 分析表
$userModel->analyze();

// 检查表
$userModel->check();

// 修复表
$userModel->repair();

// 校验表
$userModel->checksum();

// 清空表(不可恢复,慎用)
$userModel->truncate();

// 删除表(不可恢复,慎用)
$userModel->drop();

危险提示:truncate() 清空全表数据、drop() 删除整张表,均无需 WHERE 条件且不可恢复,请谨慎使用。

高级用法

自定义模型示例

namespace app\home\model;
use startmvc\core\Model;
use startmvc\core\Loader;

class UserModel extends Model
{
 protected $table = 'users';
 protected $pk = 'id';

 // 启用软删除(布尔开关,标记字段用 $deleteTime 指定)
 protected $softDelete = true;
 protected $deleteTime = 'deleted_at';

 // 启用时间戳
 protected $timestamps = true;
 protected $createTime = 'created_at';
 protected $updateTime = 'updated_at';

 // 获取激活用户
 public function getActiveUsers()
 {
 return $this->findAll('status = 1');
 }

 // 获取用户信息与订单(模型内调用其他模型请用 Loader 或 new 实例化)
 public function getUserWithOrders($userId)
 {
 $user = $this->find($userId);
 if ($user) {
 $orderModel = Loader::getInstance(OrderModel::class);
 $user['orders'] = $orderModel->findAll(['user_id' => $userId]);
 }
 return $user;
 }

 // 修改用户状态
 public function changeStatus($userId, $status)
 {
 return $this->update(['status' => $status], $userId);
 }
}

软删除的恢复无需自行实现,直接使用模型原生方法:$userModel->restore($userId)。

在控制器中使用

namespace app\home\controller;
use startmvc\core\Controller;
use startmvc\core\Request;

class UserController extends Controller
{
 public function indexAction()
 {
 $userModel = $this->model('User');
 $users = $userModel->findAll();
 $this->display('index', ['users' => $users]);
 }

 public function viewAction($id)
 {
 $userModel = $this->model('User');
 $user = $userModel->getUserWithOrders($id);
 $this->display('view', ['user' => $user]);
 }

 public function createAction()
 {
 if (Request::isPost()) {
 $data = Request::post();
 $userModel = $this->model('User');
 $userId = $userModel->insert($data);
 if ($userId) {
 $this->redirect('index');
 }
 }
 $this->display('create');
 }

 public function updateAction($id)
 {
 $userModel = $this->model('User');
 if (Request::isPost()) {
 $data = Request::post();
 $result = $userModel->update($data, $id);
 if ($result) {
 $this->redirect('index');
 }
 }
 $user = $userModel->find($id);
 $this->display('update', ['user' => $user]);
 }

 public function deleteAction($id)
 {
 $userModel = $this->model('User');
 $userModel->delete($id);
 $this->redirect('index');
 }
}

总结

StartMVC的模型系统提供了丰富而灵活的数据操作方法:从基本的CRUD操作、链式查询构建、事务处理,到自动时间戳、软删除、字段类型转换、关联装载与写入事件等ORM特性(2.9.1+),都有简洁直观的API接口。通过合理使用模型,可以有效组织业务逻辑,提高代码复用率,使应用程序更加易于维护和扩展。