mtphp 文档

本次迭代新增能力速览(零依赖、独立实现)

下述组件均为本次迭代补齐,延续“使用简单、配置简单、关键词尽量短”的取向;命名空间统一 Mtphp\,无任何 Illuminate / Symfony / Think 依赖,模板与思想借鉴主流框架但独立实现,规避版权风险。

Mtphp\Encryption\Crypt:AES-256-CBC + HMAC-SHA256 签名,自动从 config('app.key')(即 .env 的 APP_KEY)取主密钥并派生 enc/mac 两把钥匙;会话与加密 Cookie 默认启用,对开发完全透明(失败自动回退明文,旧文件仍可读取)。

use Mtphp\Encryption\Crypt;

$c = Crypt::enc('敏感串');          // 密文(enc: 前缀)
$s = Crypt::dec($c);                 // 还原;被篡改会抛异常
Crypt::setKey('base64 密钥');        // 运行时注入密钥(测试 / 特殊场景)

要点:Session 落盘与 Response::eCookie() 输出均为密文,Request::cookie() 自动解密;短方法 enc() / dec()。

24.2 集合 Collection

Mtphp\Support\Collection(全局助手 collect()):链式集合操作,零依赖实现常用方法。

collect([1,2,3])->map(fn($x)=>$x*2)->filter(fn($x)=>$x>3)->toArray();   // [4,6]
collect($users)->pluck('id')->implode(',');                            // "1,2,3"
collect($rows)->where('type','a')->sortBy('id')->groupBy('cat');

方法覆盖:map / filter / reduce / each / first / last / pluck / contains / keys / values / sort / sortBy / implode / push / merge / unique / groupBy / chunk / pipe / sum / avg / max / min / isEmpty / toArray 等。

24.3 软删除 SoftDeletes

Mtphp\Database\SoftDeletes(trait):自动给模型加 deleted_at 全局作用域,删除只置时间、不真删。

class Post extends Model { use \Mtphp\Database\SoftDeletes; }

Post::find(1)->delete();              // 软删(置 deleted_at)
Post::withTrashed()->find(1);         // 含已删
Post::onlyTrashed()->where('id',1)->get();
Post::find(1)->restore();             // 恢复
Post::find(1)->forceDelete();         // 真删
Post::baseQuery()->where(...);        // 绕过作用域的裸查询

24.4 Query 增强

Mtphp\Database\Query 新增:whereNull / whereNotNull / orWhereNull、having()(含 HAVING 编译,SQLite 数值参数自动 CAST(? AS INTEGER) 避坑)、havingRaw()(原生 HAVING 片段)、bulkInsert()(批量插入)、upsert()(MySQL ON DUPLICATE KEY UPDATE;SQLite INSERT OR REPLACE)、raw()(逃生舱,直接写 SELECT / 写语句)、whereJson / orWhereJson / whereJsonIsNull / whereJsonNotNull(JSON 文档路径查询,跨库可移植,数值比较自动 CAST(... AS REAL) 修正 SQLite 亲和性)、whereFullText / orWhereFullText(MySQL MATCH...AGAINST)、whereExists / orWhereExists / whereNotExists / orWhereNotExists(EXISTS 子查询,支持闭包 / Query / 原生 SQL)、union() / unionAll()(集合操作,分支可为闭包 / Query / 原生 SQL,绑定按序合并);ModelBuilder 覆写 find($id) 接收主键链式可用。

底层修正:连接层(Connection / 测试用 SqliteConnector)改为按 PHP 类型绑定参数(int / bool → PARAM_INT,null → PARAM_NULL),根治 SQLite / MySQL“整型值被当字符串绑定导致数值比较恒错”的亲和性陷阱(INTEGER 存储类恒小于 TEXT 存储类,故 30 > '15' 会被判 false)。此前 having() 的 CAST(? AS INTEGER) 与 whereJson 的 CAST(... AS REAL) 属于同一问题的局部绕行,现由底层统一解决。

DB::table('order')->whereNull('paid_at')->having('cnt','>',1)->get();
DB::table('log')->bulkInsert([ [...],[...] ]);
DB::table('kv')->upsert(['k'=>1,'v'=>2]);     // 存在则更新,否则插入
DB::table('u')->raw('SELECT * FROM u WHERE id=?',[1]);

24.5 限流 Throttle 中间件

Mtphp\Http\Middleware\Throttle:固定窗口限流,默认读 config/throttle.php(max / decay)。超限返回 429 + Retry-After 并带 X-RateLimit-Limit / Remaining 头。

// 路由 / 全局中间件里加上即可
'throttle' => [new \Mtphp\Http\Middleware\Throttle(60, 60)];   // 60 次 / 60 秒

24.6 API Token 鉴权

Mtphp\Auth\Token + Mtphp\Http\Middleware\AuthApi:HMAC 签名令牌,mk($uid,$ttl) 签发、chk($token) 校验、支持吊销名单与按用户吊销;中间件校验 Authorization: Bearer <token>,失败 401,成功写入 Request::setAuthUser()。

$token = (new \Mtphp\Auth\Token())->mk(1, 3600);
(new \Mtphp\Http\Middleware\AuthApi())->handle($req, $next);   // 校验 Bearer
$req->authUser();                                              // 取当前用户
// 路由里:'middleware' => [\Mtphp\Http\Middleware\AuthApi::class]

24.7 邮件 Mail(SMTP / Array / 门面)

Mtphp\Mail\Mail(门面)+ SmtpTransport(纯 PHP socket,零扩展,支持 TLS / AUTH LOGIN)+ ArrayTransport(测试收集);配置见 config/mail.php(driver=smtp|array、host/port/encryption/username/password/from)。

use Mtphp\Mail\Mail;

Mail::to('a@b.com')->subject('Hi')->text('hello')->send();   // 流式发送
Mail::raw('a@b.com', 'Hi', 'hello');                        // 一行快速发送
Mail::to('a@b.com')->subject('S')->html('<b>x</b>')->send();// HTML 正文
// 主题含中文自动 MIME base64 编码;正文行首孤点自动转义(SMTP 安全)

无 App 上下文时 Mail::transport() 回退到 ArrayTransport,便于脱离配置做单元测试。

24.8 数据填充 Seeders + seed 命令

Mtphp\Database\Seeder(抽象基类,run() 必实现,call() 调用子填充);命令 php bin/mtphp seed(默认 App\Seeders\DatabaseSeeder,可用 --class= 指定)。示例见 app/Seeders/DatabaseSeeder.php、UserSeeder.php。

// app/Seeders/DatabaseSeeder.php
class DatabaseSeeder extends \Mtphp\Database\Seeder {
    public function run(): void {
        \Mtphp\Database\DB::table('user')->insert(['name'=>'演示用户','email'=>'demo@example.com']);
        $this->call(\App\Seeders\UserSeeder::class);
    }
}

24.9 日志增强(PSR-3 八级 / 按日切分 / 多通道)

// config/log.php
return [
    'default'  => 'single',
    'channels' => [
        'single' => ['driver'=>'single','path'=>'runtime/logs/app.log','level'=>'info'],
        'daily'  => ['driver'=>'daily', 'path'=>'runtime/logs/app',    'level'=>'debug'],
    ],
];

24.10 路由增强(命名路由 route() / resource())

Route::get($uri,$action,$mw=[],$name=null) 支持命名;route($name,$params) 生成 URL;Route::resource($name,$controller) 一键注册七条 RESTful 路由;分组支持 as(名称前缀)。

use Mtphp\Route;
Route::get('/users/{id}', 'User@show', [], 'users.show');
Route::resource('posts', 'Post@');          // index/create/store/show/edit/update/destroy + 对应命名

route('users.show', ['id'=>5]);             // → /users/5
route('users.index', ['page'=>2]);          // → /users?page=2

24.11 数据库短别名(关键词尽量短)

Mtphp\Database\DB 在既有 beginTransaction / commit / rollBack / transaction 之上,新增更短的入口:

DB::startTrans();          // 开事务(= beginTransaction)
DB::trans(fn()=>{...});    // 事务闭包(= transaction)
DB::commit();              // 提交
DB::rollBack();            // 回滚

24.12 HTTP 客户端重试与超时

Mtphp\Http\Client 新增 retry($attempts,$delayMs) 与 connectTimeout($sec);仅对“传输层失败”(curl 状态码 0 / 抛异常)重试,HTTP 业务码(如 500)不重试。

http('https://api.example.com')
    ->timeout(30)->connectTimeout(7)        // 超时 / 连接超时(秒)
    ->retry(3, 200)                          // 最多 3 次,每次间隔 200ms
    ->get('/x');

24.13 TestResponse 测试客户端

Mtphp\Testing\TestResponse:包装一次 Http\Response,提供可读断言,让 HTTP / 控制器功能测试更直观。

use Mtphp\Testing\TestResponse;

$r = new TestResponse($response);
$r->assertOk()->assertStatus(200)
  ->assertJson(['ok'=>true,'data'=>['id'=>1]])
  ->assertSee('hello')
  ->assertHeader('Content-Type','application/json; charset=utf-8');
// 另有 assertNotFound / assertForbidden / assertUnauthorized

24.14 事件优先级与通配

Mtphp\Events\Dispatcher::listen($events,$listener,$priority=0):数值越大越先执行;支持通配键 * (监听全部事件)与 App\Events\* (命名空间前缀匹配)。

event()->listen(OrderPaid::class, $h, 10);   // 高优先级先执行
event()->listen('*', $globalFallback);        // 全局兜底
event()->listen('App\Events\*', $nsHandler);  // 命名空间通配

24.15 队列失败重试退避

Mtphp\Queue\DatabaseQueue 的失败重试由固定延迟升级为指数退避:delay = retryDelay × 2^(attempts−1)(封顶 3600s),避免重试风暴;达到 max_attempts 后写入 failed_jobs。文件驱动保持“at-most-once → 失败目录兜底”的设计不变。

// config/queue.php(database 驱动)
'retry_delay' => 5,    // 首次重试延迟;其后 10s → 20s → 40s … 封顶 3600s
'max_attempts' => 3,   // 超过则进失败表

以上全部能力均配套零依赖单测(见 tests/,php tests/run.php 一键跑全量,全绿)。

24.16 ORM 关联与模型事件增强

配套零依赖单测:tests/BelongsToManyTest.php(懒加载 / 预加载 / 反向 / 嵌套)、tests/ModelEventsTest.php(事件触发 / 观察者 / 写前拦截 / 按模型隔离 / 软删除事件),全量 php tests/run.php 一并绿。

24.17 关联存在性约束 + 便捷写

配套零依赖单测:tests/WhereHasTest.php(hasMany / belongsTo / belongsToMany 的 EXISTS、计数、约束闭包、NOT EXISTS、orHas)、tests/FirstOrCreateTest.php(三种便捷写 + 不落库验证),全量 php tests/run.php 一并绿。

24.18 Query 构造器补全(核心功能缺口)

经核查,以下为 Query/ModelBuilder 真实缺失的构造器能力(非文档/别名类装饰),本次补齐:

配套零依赖单测:tests/QueryBuilderCompletenessTest.php(列间比较 / addSelect / orderByRaw 含绑定 / 左连接缺匹配补 NULL / 右连接与交叉连接 / pluck 列表与映射 / when / tap),全量 php tests/run.php 一并绿。

24.19 Query 条件构造补全(嵌套 WHERE / whereIn 子查询 / 批量删除)

配套零依赖单测:tests/QueryConditionsTest.php(嵌套 AND 分组 / 嵌套 OR 分组 / whereIn 闭包与 Query 实例两种子查询 / whereNotIn 子查询 / destroy 数组·单值·逗号串 / 写前事件拦截),全量 php tests/run.php 一并绿。

24.20 FROM 派生表 / orHaving / from 别名 + 列名反引号修复

配套零依赖单测:tests/QueryFromSubTest.php(fromSub 闭包 / Query 实例 + where / fromRaw 含绑定 / SQL 渲染 / fromSub + orderBy 非聚合派生表 / orHaving),全量 php tests/run.php 一并绿。

24.21 模型作用域(本地 / 全局)+ 多态关联 + 远程一对多

  class Post extends Model {
      public function scopePublished($q) { $q->where('published', 1); }
      public function scopePopular($q, int $views) { $q->where('views', '>=', $views); }
  }
  Post::published()->get();          // 静态调用本地作用域
  Post::popular(100)->get();         // 带参数
  Post::query()->published()->where('views', '>=', 50)->get();
  

  Post::addGlobalScope('adult', fn ($q) => $q->where('age', '>=', 18));
  Post::addGlobalScope(fn ($q) => $q->where('active', 1));   // 匿名
  Post::addGlobalScope(new PublishedScope());                // Scope 对象
  Post::withoutGlobalScope('adult')->get();   // 移除单个
  Post::withoutGlobalScopes()->get();         // 移除全部(如后台管理需要看到全部记录)
  

配套零依赖单测:tests/ModelFeaturesTest.php(本地作用域静态 / 构造器调用、全局作用域自动套用 / 对象形式 / withoutGlobalScope(s)、morphMany/morphOne/morphTo 懒加载与预加载 + withCount、morph map、morphToMany、hasManyThrough 懒加载 / 预加载 / whereHas),全量 php tests/run.php 一并绿。

24.22 Query 便捷谓词 + ORM 计数 / 失败变体 / 批量赋值保护

补齐此前遗漏的「真实框架功能」缺口(均零依赖、纯新增,并已通过单测):

配套零依赖单测:tests/QueryGapTest.php(whereNotBetween / orWhereNotBetween / whereBetweenColumns / orWhereBetweenColumns / havingBetween / orHavingBetween / latest / oldest / inRandomOrder / take / skip / offset / forPage / firstOrFail 的 SQL 渲染与结果校验)、tests/OrmExtraTest.php(morphToMany withCount + loadCount、hasMany loadCount、morphMany withCount、fillable / guarded / 默认宽松、firstOrFail / updateOrFail / deleteOrFail),全量 php tests/run.php 一并绿。


24.23 Query 便捷查询 + 模型便捷读 + 预加载约束闭包 + 软删判定

补齐此前遗漏的「真实框架功能」缺口(均零依赖、纯新增,并已通过单测):

配套零依赖单测:tests/QueryConvenienceTest.php(doesntExist / sole 的 0/1/多 三种分支 / each 全量+小批次 / reorder 清排序+重设 / findMany 去重+空数组+指定列 / Model::sole / SoftDeletes::trashed 增删恢复三态)、tests/EagerConstraintTest.php(hasMany / hasOne / belongsTo / belongsToMany / morphMany / morphOne / morphToMany / hasManyThrough 的 with 约束、withCount 约束、嵌套路径 posts.comments 约束),全量 php tests/run.php 一并绿。


24.24 校验 exists 规则 + Collection 高阶筛选

补齐此前遗漏的真实框架功能缺口(均零依赖、纯新增,已通过单测):

配套零依赖单测:tests/ValidatorTest.php::testExistsRule(存在通过/不存在失败/列缺省取字段名/空值放行/unique 反向)、tests/CollectionTest.php(keyBy / where 默认与操作符与回调 / whereIn / firstWhere),全量 php tests/run.php 一并绿。


24.25 缓存便捷方法(pull / add / forever / rememberForever / increment / decrement)

补齐此前遗漏的真实框架功能缺口(均零依赖、纯新增,已通过单测):

以上写入 CacheInterface 契约,并在 AbstractCache trait 给出通用实现(文件驱动 FileCache 与 Redis 驱动 RedisStore 共用)。注:通用版 add/increment 为「has+set」「get+set」两步(单进程安全),高并发强一致场景可由驱动覆盖为原子指令(SET NX / INCRBY)。

配套零依赖单测:tests/FileCacheTest.php(pull / add 已存在不覆盖 / forever+rememberForever / increment+decrement)、tests/ThrottleTest.php 与 tests/TokenTest.php 的 CacheInterface 测试替身同步补齐 6 方法,全量 php tests/run.php 一并绿。


24.26 简单分页 simplePaginate(免 COUNT 大表)

补齐此前遗漏的真实框架功能缺口(均零依赖、纯新增,已通过单测):

配套零依赖单测:tests/PaginationTest.php(simplePaginate 首/末页、items 数、hasMorePages/nextPageUrl/previousPageUrl、total/lastPage=-1、Model 调用、toArray 与 links 简单模式),全量 php tests/run.php 一并绿。

24.27 缓存原子锁 lock(分布式锁)

$lock = cache()->lock('order:' . $id, 10);   // 持锁 10 秒
if ($lock->get()) {
    try {
        // 临界区
    } finally {
        $lock->release();
    }
}
// 或:阻塞等待并在拿到锁后执行回调(结束自动释放)
cache()->lock('order:' . $id, 10)->block(5, fn () => /* 临界区 */ null);

24.28 事件增强:subscribe / until / flush

24.29 HTTP 客户端并发 concurrent(curl_multi)

$resps = http('https://api.test')->withToken($t)->concurrent([
    '/a',
    ['url' => '/b', 'method' => 'POST', 'data' => ['x' => 9]],
    ['url' => 'https://other.test/c'],
]);

24.30 i18n 复数 choice / trans_choice

__('apples', ['count' => 5]);   // 假设文案 '{0} 没有苹果|{1} 一个苹果|[2,*] :count 个苹果' → "5 个苹果"
trans_choice('apples', 5);

24.31 Query::getBindings()(导出 toSql 绑定)

$q = DB::table('users')->where('age', '>', 18)->where('city', '=', 'WH');
$sql   = $q->toSql();        // SELECT * FROM `users` WHERE `age` > ? AND `city` = ?
$binds = $q->getBindings();  // [18, 'WH']

24.32 Collection 集合操作:only / except / partition / zip / collapse

配套零依赖单测:tests/CacheLockTest.php(独占/释放/owner/block 回调与超时/forceRelease/过期接管/永久锁 + 条件 Redis 锁)、tests/EventsSubscribeTest.php(subscribe 数组与订阅者对象 / until 短路 / flush)、tests/HttpClientConcurrentTest.php(并发请求构建 + 同序响应 + Client 形态)、tests/TranslationChoiceTest.php(精确/区间/上界/无匹配/catch-all/单复数/:count 自动替换)、tests/QueryBindingsTest.php(条件/whereIn/whereRaw/空绑定与 SQL 对齐)、tests/CollectionTest.php(only/except/partition/zip/collapse),全量 php tests/run.php 一并绿。

24.33 附件上传自动压缩(ImageCompressor + UploadedFile)

框架的附件上传此前只有 UploadedFile::move() 原样搬文件、无压缩。本次补齐可选、GD 守卫的图片自动压缩,作为 UploadedFile::move() 的开关 + 参数,并通过 config/upload.php 提供全局默认。

引擎:Mtphp\Image\ImageCompressor(零依赖,GD 守卫)

接入:UploadedFile::move() 第三参

$file = request()->file('avatar');
// 第三参:null(走全局开关) | false(强制关闭) | 数组(逐次覆盖)
$dest = $file->move('uploads/avatar', null, [
    'max_width'   => 1920,
    'max_height'  => 1920,
    'quality'     => 82,
    'format'      => 'keep',
    'auto_rotate' => true,
]);
// 压缩失败不丢原文件,仅记录:
if ($file->getLastError() !== null) { /* GD 缺失 / 非图片等 */ }

配套零依赖单测:tests/ImageCompressTest.php(默认不压缩 / 显式压缩缩放 / false 覆盖全局 / 全局配置生效 / 动画 GIF 跳过 / 非图片穿透 / 直调 WebP / 缺文件抛异常 / 非法类型抛异常),全量 php tests/run.php 一并绿。