xiasf / think-orm
从 ThinkPHP 5.0.24 抽离的独立 ORM 包,保留 think\ 命名空间,提供 Db / Model / Validate / Paginator / Collection / Loader / Config 等完整模型层能力,可在非 ThinkPHP 项目(脚本、微服务、其他框架)中无缝复用 ThinkPHP 5.0 风格 ORM 用法。
Requires
- php: >=7.2
- ext-pdo: *
- ext-pdo_mysql: *
Requires (Dev)
- phpunit/phpunit: ^8.5 || ^9.5
Suggests
- psr/log: 如需 SQL 日志输出,可注入 PSR-3 LoggerInterface(\think\Log::setLogger());否则默认静默或写文件(Orm::boot(['log'=>['file'=>...]]))
This package is auto-updated.
Last update: 2026-07-09 09:36:53 UTC
README
ThinkPHP 5.0.24 ORM 的独立 composer 包移植。保留原 think\ 命名空间,零改动拷贝核心源码,配套 8 个桩文件替换框架依赖。
官方 topthink/think-orm 从 TP 5.1+ 抽出,API 与 5.0.24 不兼容,因此必须直接 fork 5.0.24 实际源码。
安装
composer require xiasf/think-orm
需要 PHP >= 7.2 + ext-pdo。MySQL 默认 ext-pdo_mysql。
三步上手
1. 启动
require __DIR__ . '/vendor/autoload.php'; \ThinkOrm\Orm::boot([ 'database' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'hostport' => 3306, 'database' => 'app', 'username' => 'root', 'password' => 'secret', 'charset' => 'utf8mb4', 'prefix' => '', 'debug' => false, ], ]);
2. 创建模型(默认解析到 app\<module>\model\<Name>)
最常见的方式是写一个继承 think\Model 的子类:
// 文件: app/model/User.php namespace app\model; use think\Model; use think\traits\model\SoftDelete; class User extends Model { use SoftDelete; protected $table = 'users'; protected $autoWriteTimestamp = 'datetime'; // 自动管理 create_time / update_time protected $deleteTime = 'delete_time'; // 软删字段 protected $hidden = ['password']; // 序列化时隐藏 protected $readonly = ['name']; // 只读字段 // 关联 public function posts() { return $this->hasMany(Post::class); } public function profile() { return $this->hasOne(Profile::class); } public function roles() { return $this->belongsToMany(Role::class, 'user_roles'); } // 读写器 public function getNameAttr($v) { return ucfirst($v); } // 命名范围 public function scopeActive($q) { return $q->where('is_active', 1); } }
yf 项目用户:可以直接继承本包提供的
app\common\BaseModel(位于example/app/common/BaseModel.php,可拷到自己的项目里)。它把 yf 项目的 BaseModel 完整移植过来,提供add / adds / upd / updBy / updAttr / del / info / infoBy / lists / listBy / listByIds / listPageBy / search / countBy / maxBy / minBy / avgBy / sumBy / valueBy / inc / dec / upSert等 yf 风格方法。完整用法见example/目录(含 Notice/Smartpark 模型与验证器、字段格式化、自动时间戳、JSON 字段、关联、PSR-3 SQL 日志、run.php端到端示例)。
3. 用 model() / validate() 操作
// 取模型实例(单例) $User = model('User'); // CRUD $user = $User->find(1); $users = $User->where('age', '>', 18)->order('id desc')->select(); $newId = $User->insertGetId(['name' => 'a', 'email' => 'a@x']); $User->where('id', $newId)->update(['age' => 20]); // 或走静态 $user = \app\model\User::get(1); $user->age = 21; $user->save(); \app\model\User::destroy([2, 3]); // 关联 foreach (\app\model\User::get(1)->posts as $p) { /* ... */ } // 验证 $v = validate('User'); if (!$v->check($data)) { echo $v->getError(); }
创建验证器(默认解析到 app\validate\<Name>)
// 文件: app/validate/User.php namespace app\validate; use think\Validate; class User extends Validate { protected $rule = [ 'name' => 'require|max:30', 'email' => 'require|email', 'age' => 'integer|>=:0|<=:150', ]; protected $message = [ 'name.require' => '名字必须填', 'name.max' => '名字不能超过 30 字符', 'email' => '邮箱格式错误', ]; protected $scene = [ 'create' => ['name', 'email', 'age'], 'update' => ['name', 'age'], 'login' => ['email'], ]; }
validate('User')->scene('login')->check(['email' => 'a@b.com']);
命名空间与 model()/validate() 解析
model() / validate() 解析器有 4 种输入形式,所有形式都自动把下划线命名转成大驼峰:
| 调用 | 解析结果 | 目录 |
|---|---|---|
model('user') |
app\model\User |
app/model/User.php |
model('user_order') |
app\model\UserOrder |
app/model/UserOrder.php(下划线 → 大驼峰) |
model('iot/xxx') |
app\iot\model\Xxx |
app/iot/model/Xxx.php(模块/名字) |
model('iot/user_order') |
app\iot\model\UserOrder |
app/iot/model/UserOrder.php |
model('iot/v1/xxx') |
app\iot\model\v1\Xxx |
app/iot/model/v1/Xxx.php(多层路径) |
model('iot/v1/user_order') |
app\iot\model\v1\UserOrder |
app/iot/model/v1/UserOrder.php |
model('\\App\\Special\\User') |
原样使用 | 直接 new 这个类 |
转换规则(Loader::parseName)
- 正向(小写下划线 → 大驼峰):
user_order→UserOrder、car_parking_log→CarParkingLog - 调用方用小写下划线(更符合 URL/参数习惯),实际类名是大驼峰
- 反向:
UserOrder→user_order(仅在某些自动场景用,比如with('userOrder')也可解析到user_order关联)
验证器同规则
validate('iot/Car') → app\iot\validate\Car、validate('iot/v1/CarOrder') → app\iot\validate\v1\CarOrder。
自定义命名空间根
默认根命名空间是 app(由 App::$namespace 控制)。如果你的项目不叫 app:
\think\App::$namespace = 'My\\App'; // model('User') → My\App\model\User
common 模块 fallback
如果 model('iot/NotExists') 在 iot 模块下找不到,Loader 会自动尝试 app\common\model\NotExists。这是 yf 的多模块共享模型机制。
实际例子(yf 项目风格)
namespace app\parkinglot\model\v1; use app\parkinglot\model\BModel; use app\parkinglot\model\v1\Smartpark; class Car extends BModel { // 关联:直接用类常量 Smartpark::class —— 比 model('xxx/xxx')->class 更直接, // IDE 可跳转、PHPStan 可静态分析。 public function smartparkInfo() { return $this->belongsTo(Smartpark::class, 'smartpark_id', 'id') ->where(['status' => 1, 'is_del' => 0]); } }
历史写法
model('smartpark/smartpark')->class也能用(model()返回实例,->class取其 FQCN),但不推荐——多一次实例化、绕一道字符串解析、IDE 无法跟踪。新代码统一用TargetClass::class。
文件结构:
example/app/
├── di/
│ ├── model/
│ │ └── v1/
│ │ ├── Notice.php ← model('di/v1/Notice') 或 model('di/v1/notice')
│ │ └── Smartpark.php
│ └── validate/
│ └── Notice.php
├── parkinglot/
│ ├── model/
│ │ ├── BModel.php ← 不通过 model() 解析(直接 use)
│ │ └── v1/
│ │ ├── Car.php ← model('parkinglot/v1/Car')
│ │ ├── CarOwner.php
│ │ └── ...
│ └── validate/
│ ├── BaseValidator.php
│ └── Car.php ← validate('parkinglot/Car')
└── common/
└── BaseModel.php
yf 风格业务模块参考:parkinglot example
example/app/parkinglot/ 是从 yf 真实业务抽出来的最小可运行参考,覆盖以下高级模式(每个都有对应测试):
1. BModel:双 readonly + 默认关联
// example/app/parkinglot/model/BModel.php class BModel extends BaseModel { public $model = null; // 必须为 public(否则被 __set 拦截) protected $readonly = ['smartpark_id', 'parkinglot_id']; public function smartparkInfo() { return $this->belongsTo(Smartpark::class, 'smartpark_id', 'id') ->where(['status' => 1, 'is_del' => 0]); // 条件关联 } public function useWithSp() // 链式预加载快捷方法 { return $this->useWith(['smartpark_info', 'parkinglot_info']); } }
2. $insert 自动字段 + 修改器(默认值兜底)
class Car extends BModel { protected $insert = ['is_temp_number', 'is_new_energy']; // 显式传值则尊重,否则按车牌号推断 protected function setIsTempNumberAttr($value, $data) { if (is_null($value) || $value === '') { return stripos($data['number'] ?? '', '临') === false ? 0 : 1; } return $value; } }
3. belongsTo + ->bind() 字段绑定
把关联表字段"绑"成本模型字段,访问 $owner->name 实际取自 user 表:
public function userInfo() { return $this->belongsTo(User::class, 'user_id', 'id')->bind([ 'name', 'face', 'email', 'mobile', 'nick_name', 'real_name', ]); }
4. belongsToMany + pivot 条件过滤
通过 Request::instance()->param('smartpark_id/d') 在运行时给 pivot 加条件:
public function carOwnerList() { $rel = $this->belongsToMany(CarOwner::class, 'pt_car_car_owner', 'car_owner_id', 'car_id'); $sp = Request::instance()->param('smartpark_id/d', 0); if ($sp) { $rel->getQuery()->where(['pivot.smartpark_id' => $sp]); } return $rel; }
5. 多层嵌套 with
public function useWithFull() { return $this->useWith([ 'smartpark_info', 'fixcar_list' => ['smartpark_info', 'parkinglot_info'], // 二级嵌套 'car_owner_list', ]); }
6. 7 个 yf 风格验证规则(BaseValidator)
| 规则 | 语义 | 示例 |
|---|---|---|
sometimes |
字段存在时才校验(TP 默认行为已涵盖,这里仅作声明) | 'mobile' => 'sometimes|regex:^1\d{10}$' |
conflict:a,b |
当前字段存在时,a/b 都不能存在 | 'email' => 'conflict:mobile,name' |
r_if:field,v1,v2 |
field 等于 v1 或 v2 时当前字段必填 | 'mobile' => 'r_if:contact_type,phone,sms' |
r_with:a,b |
a 或 b 存在时当前字段必填 | 'nick_name' => 'r_with:name,email' |
r_with_all:a,b |
a 和 b 都存在时当前字段必填 | 同上 |
r_without:a,b |
a 或 b 不存在时当前字段必填 | 同上 |
r_without_all:a,b |
a 和 b 都不存在时当前字段必填 | 同上 |
本包的
Validate::checkItem已扩展:所有r_*规则即使在字段为空时也会触发(默认 TP 行为只对require*规则如此)。这是与上游 TP 5.0.24 的唯一行为差异,但完全是 yf 业务必需的。
7. BaseModel::validateData 错误转异常
规则写错(如 'require|integeregt:0' 拼错)时,原 TP 5.0.24 会触发 PHP Warning 然后静默通过。本包的 BaseModel 用 set_error_handler 包裹,转成 ValidateException 便于排查:
protected function validateData($data, $rule = null, $batch = null) { // ...build $validate... set_error_handler(function ($code, $msg, $file, $line) { // iconv 转 GBK→utf-8 后抛 ValidateException throw new ValidateException("Validate ERROR: [{$code}] {$msg} in file: {$file} on line: {$line}"); }); try { $ok = $validate->batch($batch)->check($data); } finally { restore_error_handler(); } // ... }
配置
数据库(完整选项)
\ThinkOrm\Orm::boot([ 'database' => [ 'type' => 'mysql', // 本包仅支持 mysql 'hostname' => '127.0.0.1', 'hostport' => 3306, 'database' => 'app', 'username' => 'root', 'password' => '', 'dsn' => '', // 显式 dsn 优先 'socket' => '', // Unix socket(非空时优先于 hostname/hostport) 'charset' => 'utf8mb4', 'prefix' => '', 'params' => [], // PDO 构造参数,例:[PDO::ATTR_PERSISTENT => true] 开持久连接 'debug' => false, 'deploy' => 0, // 0=集中式(单库),1=分布式(主从) 'rw_separate' => false, // 分布式部署时是否读写分离 'master_num' => 1, // 主服务器数量(rw_separate=true 时有效) 'slave_no' => '', // 指定从服务器序号(不指定则随机) 'read_master' => false, // 写后强制读主库(业务有"写完立即读"场景时启用) 'fields_strict' => true, // 严格字段检查 'resultset_type' => 'array', // 或 'collection' 'auto_timestamp' => false, // 全局自动时间戳 'datetime_format' => 'Y-m-d H:i:s', 'sql_explain' => false, // EXPLAIN 调试(debug=true 时生效) 'use_schema' => false, // 读取 RUNTIME_PATH/schema 字段缓存 'builder' => '', // 自定义 Builder 类 'query' => '\\think\\db\\Query', // 自定义 Query 类 'break_reconnect' => false, // 断线重连 ], 'paginate' => [ 'type' => 'bootstrap', 'var_page' => 'page', 'list_rows' => 15, ], ]);
与 yf 项目
database.php对齐:上面列出的配置项与 TP 5.0.24 / yf 项目惯例完全一致。从 yf 切到本包时,把 yf 的application/database.php返回数组直接传给Orm::boot(['database' => $yfConfig])即可。
自定义常量
// boot 之前先定义,否则自动 fallback 到 sys_get_temp_dir()/think-orm/ define('RUNTIME_PATH', '/var/log/myapp/'); \ThinkOrm\Orm::boot([...]);
守护进程使用
适用场景:workerman / swoole / while(true) loop / cron 等 CLI 常驻进程。
核心问题:常驻进程持有 PDO 连接数小时,MySQL wait_timeout(默认 8h,DBA 可能调到 1h 甚至更短)会主动切断空闲连接,下次 query 时炸 MySQL server has gone away。
关键配置:phpfpm vs cli
| 配置项 | phpfpm / mod_php | cli 守护进程 |
|---|---|---|
params[PDO::ATTR_PERSISTENT] |
true(多请求间复用 PDO,减握手) |
不设 / [](守护进程已常驻,persistent 反而阻止 break_reconnect 关闭僵尸连接) |
break_reconnect |
false(请求短,靠重建连接即可) |
true(必须开,遇到 gone away 关键词自动 close → 重连) |
read_master |
按需 | true(守护进程有"写后立即读"场景的几乎必然) |
phpfpm 启动
\ThinkOrm\Orm::boot([ 'database' => [ // ... 'params' => [\PDO::ATTR_PERSISTENT => true], // ★ 开 'break_reconnect' => false, // ★ 不依赖 'read_master' => true, // 业务有写后立即读就开 ], ]);
cli 守护进程启动
\ThinkOrm\Orm::boot([ 'database' => [ // ... 'params' => [], // ★ 关(不能开 persistent) 'break_reconnect' => true, // ★ 必须开 'read_master' => true, ], ]);
为什么 cli 不能开 ATTR_PERSISTENT?
- 守护进程本来就常驻,PDO 已天然复用,persistent 不带来额外收益
- persistent PDO 由 PHP 内部缓存,
Connection::close()只是$this->linkID = null,不真关闭底层 PDO。break_reconnect 失效,僵尸连接永远占着 - persistent 连接跨脚本共享,多个 worker 进程(如 workerman 多子进程)共用同一个 PHP 持久化池容易撞 transaction 状态污染
完整守护进程 demo
参考 example/daemon/BaseWorker.php —— 单文件、零外部依赖、不依赖 pcntl(Windows 也能跑)。三个核心机制:
1. initDb():启动时预连接
protected function initDb(): void { Db::query('SELECT 1'); // 强制建立连接(避免 lazy 引发首次业务请求即失败) $this->lastHeartbeat = microtime(true); }
2. heartbeat():定期探活
while (!$this->stopRequested) { try { if (microtime(true) - $this->lastHeartbeat >= $this->heartbeatInterval) { $this->heartbeat(); // SELECT 1,失败抛 PDOException $this->lastHeartbeat = microtime(true); } $this->onTick(); // 业务 } catch (\Throwable $e) { if ($this->checkDbBreak($e)) { // 关键词识别 $this->reconnectDb(); } } }
3. reconnectDb():清两道缓存
踩过的坑:break_reconnect = true 时 Connection::close() 只清自己内部的 linkID,但 Model::$links 是另一个静态缓存,按 model 类名缓存了 Query 实例(持有 Connection)。Connection 重建后,Model 仍会拿到老的僵尸 Connection。
protected function reconnectDb(): void { // 1) 清全局连接池 Db::clear(); // 2) 清 Model 类级别 Query 缓存(protected,反射) $prop = new \ReflectionProperty(Model::class, 'links'); $prop->setAccessible(true); $prop->setValue(null, []); // 3) 强制重建一次连接(失败立即抛) Db::query('SELECT 1'); }
checkDbBreak 关键词列表
参考 Connection::isBreak():
$keywords = [ 'server has gone away', 'no connection to the server', 'Lost connection', 'is dead or not enabled', 'Error while sending', 'decryption failed or bad record mac', 'server closed the connection unexpectedly', 'SSL connection has been closed unexpectedly', 'Error writing data to the connection', 'Resource deadlock avoided', 'failed with errno', 'Broken pipe', ];
业务异常(如 validate failed)不应触发重连 —— checkDbBreak 用 stripos 匹配关键词,无匹配返回 false。
完整启动示例
# demo 模式(限 20 个 tick 后退出) php example/run_daemon.php --max-tick=20 # 生产模式(无限循环;pcntl 可用时 SIGTERM/SIGINT 优雅退出) php example/run_daemon.php
参考文件:
example/daemon/BaseWorker.php—— 抽象基类(initDb / heartbeat / checkDbBreak / reconnectDb / 信号处理)example/daemon/QueueWorker.php—— 队列消费具体实现(单条事务、失败回滚、自动建表 + seed)example/run_daemon.php—— 启动脚本(cli 配置演示)
注入(可选)
// PSR-3 SQL 日志 \think\Log::setLogger($monolog); // PSR-16 让 Query::cache() 真正生效 \think\Cache::setInstance($psr16Cache); // 自定义 Request(用于 Paginator 解析 page 参数、Validate::method) \think\Request::setInstance(new MyRequest());
未注入时所有调用都走 NoOp 桩,不报错。
API 速查
| 用法 | 例子 |
|---|---|
| 模型实例 | model('User') |
| 验证器实例 | validate('User') |
| 单条 | User::get($id) / User::where('x',1)->find() |
| 多条 | User::all() / User::where(...)->select() |
| 单值 | User::where('id',1)->value('name') |
| 列值 | User::column('name','id') |
| 插入 | User::create($data) / (new User)->save($data) |
| 更新 | User::update($data) / $u->save() |
| 删除 | User::destroy($ids) / $u->delete() |
| 物理删除(含软删模型) | User::destroy($ids, true) / $u->delete(true) —— 注意 不是 force()->delete()(见下方差异说明) |
| 自增自减 | User::where('id',1)->inc('hits')->update() |
| 关联 | User::with('posts,profile')->select() |
| 分页 | User::where(...)->paginate(15) |
| 事务 | Db::transaction(fn() => ...) |
| 分批 | User::chunk(100, function($rows){...}) |
| 软删恢复 | User::onlyTrashed()->find()->restore() |
测试
# 准备 MySQL 测试库 mysql -u root -p123456 -e "CREATE DATABASE think_orm_test CHARACTER SET utf8mb4" # 全套(默认 127.0.0.1:3306 root/123456,可用环境变量覆盖) composer test # 不需 DB 的单元测试 composer test:unit # 过滤 composer test:filter -- RelationHasMany
跑 example
mysql -u root -p123456 -e "CREATE DATABASE think_orm_example CHARSET utf8mb4"
php example/run.php
example/run.php 端到端演示:model() 解析 → 创建 → 字段格式化(JSON/数组/decimal→float/append)→ 关联预加载 → readonly → 验证器场景 → 聚合。包含 13 个 section:di 模块(Notice/Smartpark)+ parkinglot 模块(Car/CarOwner/Parkinglot/Smartpark/User 的 BModel + 条件关联 + bind + 多层嵌套 + pivot 过滤 + readonly)。所有 SQL 通过 PSR-3 logger 打到 stdout。
跑守护进程示例
# 复用 example 数据库 php example/run_daemon.php --max-tick=20 # demo 模式:跑 20 个 tick 后退出 php example/run_daemon.php # 生产模式:无限循环(Ctrl+C 退出)
example/run_daemon.php 演示守护进程下安全使用 ORM 的完整模式:队列消费 worker,每 2 秒轮询一批任务、单条事务处理、失败回滚、定期心跳、断线重连。详见下方 守护进程使用 章节。
测试覆盖(431 tests / 841 assertions):
| 范围 | 测试文件 |
|---|---|
| 桩文件 | SupportStubTest |
| 配置 | ConfigTest |
| Loader 类 | LoaderTest(parseName / parseClass / addNamespaceAlias / addClassMap / model() / validate() 解析、缓存、common fallback、FQCN passthrough、异常) |
| 验证规则 | ValidateRulesTest(全部内置规则 + 中文消息) |
| 集合 | CollectionTest |
| CRUD | QueryCrudTest、InsertAllTest |
| 事务 | TransactionTest(commit/rollback/嵌套) |
| Query Builder | QueryBuilderTest(where/join/group/having/order/limit/page/inc/dec) |
| Query 高级 API | AdvancedQueryTest(whereRaw/whereOrRaw/whereExists/whereNotExists/whereExp/whereTime/whereNotNull/whereNotBetween/whereNotLike/useSoftDelete/fetchSql/getPk/getTableFields) |
| 子查询 | SubqueryTest |
| 分批 | ChunkCursorTest |
| Model CRUD | ModelCrudTest、ModelAccessorMutatorTest、ModelAutoTimestampTest |
| Model 高级 | ModelWorkflowTest(model()/validate() helper、hidden/append、scope、事件、readonly) |
| Model 高级 API | AdvancedApiTest(Paginator URL 辅助:appends/fragment/getUrlRange/getCurrentPage/getCurrentPath/render;Model::has / hasWhere / together) |
| 验证器 | ModelValidationTest(Model 内嵌规则 + 失败回滚) |
| 软删 | SoftDeleteTest |
| 关联 | RelationHasOneTest、RelationHasManyTest、RelationBelongsToTest、RelationBelongsToManyTest、RelationHasManyThroughTest、RelationMorphTest |
| 复合主键 | ComplexPkTest |
| JSON 字段 | JsonFieldTest |
| 分页 | PaginatorTest |
| 闭包 where | ClosureWhereTest |
| yf 风格 BaseModel | YfBaseModelTest(77 个测试:validatorName、useWith、自动时间戳、JSON、读写器、append、关联、readonly、CRUD add/adds/upd/upds/updBy/updAttr/del/delBy、info/infoBy、lists/listBy/listByIds/listPageBy、search/search_or 分页、聚合 countBy/maxBy/minBy/avgBy/sumBy/valueBy/inc/dec、upSert、resultSet/resultListSet、trait spd/sca/listIndexBy/listIndexByIds/fieldWhere/withModel/withScope/get_ExtendField/rollbackQuery、validatorName 显式覆盖、validateData 错误转异常、PSR-3 SQL 日志、validate/model helper、端到端) |
| yf parkinglot 模块 | ParkinglotIntegrationTest(20 个测试:BModel 双 readonly + 条件关联 + helper,$insert 自动字段 + 修改器,belongsTo+bind,hasMany,belongsToMany+pivot 过滤,多层嵌套 with,search_or,7 个 BaseValidator 自定义规则,validateData 错误转异常) |
| 守护进程 | DaemonWorkerTest(15 个测试:initDb/heartbeat 探活、checkDbBreak 关键词识别 gone away/lost connection/broken pipe、业务异常不触发重连、reconnectDb 双缓存清空 Db::$instance+Model::$links、重建 PDO(CONNECTION_ID 变化)、cli 默认关 persistent / phpfpm 通过 params 开 persistent、限次迭代生命周期、心跳触发、模拟断线自动重连恢复) |
与原 TP 5.0.24 的差异
-
helper.php精简:保留exception / config / dump / debug / model / validate / db / import / trace / load_relation / collection,删除 web 相关助手(lang/input/widget/controller/action/url/session/cookie/cache/request/response/view/json/jsonp/xml/redirect/abort/halt/token/load_trait/vendor)。 -
Config.php:移除依赖Request::module()的动态 extra-config 加载分支。 -
Validate.php:$typeMsg改为硬编码中文;移除{%xxx%}多语言包装与Lang::get/has调用。 -
behavior规则:本包未移植think\Hook类。Validate::behavior()直接返回 true(规则视为通过);如需自定义行为验证,覆盖该方法即可。 -
token规则:依赖 Session,桩默认返回 null,token 规则将失败;可注入 Session 实现启用。 -
Paginator:默认Request::param()桩返回 null,页码默认为 1;要支持 HTTP 上下文需注入自定义 Request。 -
Validate::checkItem扩展:所有以r_开头的规则(r_if/r_with/r_with_all/r_without/r_without_all)即使在字段为空时也会触发——这是 yf 业务必需的条件必填语义。原 TP 5.0.24 只对require*规则做此处理,本包扩展至r_*。 -
Loader.php:移除了register()/loadComposerAutoloadFiles()/registerComposerLoader()等 SPL autoload 注册逻辑——Composer 已处理自动加载。Loader::model()在跨模块查找时的EXTEND_PATH/APP_PATH改为读项目根的extend/与app/目录。 -
trait 命名空间迁移:原 TP 5.0.24 的
traits\model\SoftDelete与traits\think\Instance占用顶层traits\命名空间,不适合作为公开 composer 包发布。本包改为:traits\model\SoftDelete→think\traits\model\SoftDeletetraits\think\Instance→think\traits\Instance(同时整理目录结构)
从 yf 迁移过来的代码需把
use traits\model\SoftDelete;改为use think\traits\model\SoftDelete;。 -
Query::setInc / setDec实时写入:移除了$lazyTime延迟累积更新分支(依赖 Cache 的 inc/dec),inc/dec 永远实时写入 DB。$lazyTime参数保留以维持签名兼容但被忽略。 -
不依赖缓存:
think\Cache桩默认所有操作返回 false / null / 空。Query::cache()API 保留(每次仍走 DB),PSR-16 缓存可选注入但不推荐。 -
⚠️
Orm::boot()只能初始化一次:第二次调用仅合并Config,不会刷新这些已建立的单例:Log::$logger(已注入的 PSR-3 logger)Db::$instance(已建立的 PDO 连接)Model::$links(按类名缓存的 Query 实例)
Log::record的"按优先级"行为(重要):- 未注入 PSR-3 logger → 走文件日志(如已通过
log.file启用) - 已注入 PSR-3 logger → 直接调用
logger->log()后return,文件日志永远不写(即使log.file也配置了)
想运行时切换:
场景 正确做法 切日志文件路径(未注入 PSR-3 logger) Orm::refreshLog($newPath)或直接Log::setLogFile($newPath)切 PSR-3 logger 实例 Log::setLogger($newLogger)从 PSR-3 logger 切回文件日志 Log::setLogger(null)+Log::setLogFile($path)多套 DB 配置 用 Db::connect($configKey)显式连接(不要重复 boot 切库)守护进程断线重连 见 守护进程使用 章节 测试场景完全重置 Orm::reset()+Db::clear()+ 反射清空Model::$links -
⚠️
force()是 update 标志,不是物理删除(Laravel 用户最容易踩的坑):$model->force(true)->save()—— 跳过字段比较,强制 update 写入(TP 5.0.24 原语义)$model->force()->delete()—— 仍是软删,force 标志对 delete 无影响- 物理删除(含软删模型)的真实 API:
User::destroy($ids, true); // 第二参数 $force=true 走 SoftDelete::destroy 第二参 $user->delete(true); // SoftDelete::delete($force=true) 跳过软删字段 User::onlyTrashed()->select(); // 反例:查软删记录用 onlyTrashed()
License
Apache-2.0(同 ThinkPHP 5.0.24)。