进阶组件(补齐主流框架常用能力)
本章汇总框架“开箱即用”的一批进阶组件:控制台、数据库迁移、ORM 关联、分页、API 资源、队列、校验、鉴权、会话、CORS、事件、HTTP 客户端、本地化。它们零依赖 / 零配置即可用,全部位于 Mtphp\ 命名空间(模板引擎仅借鉴主流框架思想、独立实现,规避版权风险;详见第 22 节及框架版权红线)。
23.1 控制台(Console)
内核 Mtphp\Console\Application + 命令基类 Mtphp\Console\Command(思想借鉴主流框架的 Artisan 内核)。
编写命令:
namespace App\Console\Commands;
use Mtphp\Console\Command;
class Greet extends Command
{
public function configure(): void
{
$this->name = 'greet';
$this->description = '问好';
}
public function handle(): int
{
$this->info('Hello ' . $this->option('name', 'world'));
return 0; // 返回进程退出码
}
}
入口 bin/mtphp 已内置登记若干命令:
php bin/mtphp list :: 列出命令
php bin/mtphp key:generate :: 生成 APP_KEY 写入 .env
php bin/mtphp routes:list :: 列出已注册路由
php bin/mtphp migrate :: 执行尚未运行的迁移
php bin/mtphp migrate:rollback :: 回滚最近一批迁移
php bin/mtphp queue:work --queue=default --sleep=2 :: 启动队列消费者
php bin/mtphp serve --host=127.0.0.1 --port=8000 :: 启动内置开发服务器
php bin/mtphp schedule:list :: 列出全部定时/重复任务与下次运行时间
php bin/mtphp schedule:run :: 执行到期任务(配合系统 cron 每分钟一次)
php bin/mtphp schedule:work :: 常驻守护,每分钟自动触发(免系统 cron)
Command 提供 option($k,$default) / argument($i,$default) 解析 --k=v 与位置参数;line() / info() / warn() / error() 输出。新增命令后用 $console->add(new XxxCommand()) 登记即可。
23.2 数据库迁移与 Schema 构造器
文件 mtphp/Database/Schema/* + mtphp/Database/Migrator.php。迁移文件放在 database/migrations/,文件名带时间戳前缀(如 2024_01_01_000001_create_users.php),return 一个 Mtphp\Database\Migration 的匿名子类:
<?php
use Mtphp\Database\Migration;
use Mtphp\Database\Schema\Blueprint;
use Mtphp\Database\Schema\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('users', function (Blueprint $t): void {
$t->id(); // 主键自增
$t->string('name', 64);
$t->string('email')->unique();
$t->integer('age')->nullable();
$t->timestamps(); // created_at / updated_at(可空)
});
}
public function down(): void
{
Schema::dropIfExists('users');
}
};
常用列 / 约束(Blueprint):
- 列:
id()、increments()、string($col,$len)、char()、text()、integer()、tinyInteger()、bigInteger()、boolean()、float()、decimal($col,$precision,$scale)、date()、dateTime()、time()、timestamp()、json()、enum($col,$values)、timestamps()(created_at/updated_at)、softDeletes()(deleted_at)。 - 约束 / 索引:
primary($cols)、unique($cols)、index($cols)、foreign($col)->references('id')->on('other')->onDelete('CASCADE')。 - 变更:
dropColumn()、dropIndex()、dropForeign()、dropIfExists()、rename($to)(均为当前表操作)。
列修饰(链式):->nullable()、->default($v)、->unsigned()、->comment('...')、->unique() 等。
执行:php bin/mtphp migrate(按文件名升序跑未执行过的迁移,记录到 migrations 仓储表并分批);php bin/mtphp migrate:rollback 回滚最近一批。Schema 构造器自动按 getDriver() 在 mysql / sqlite 方言间分叉(自增主键、布尔 / JSON / 枚举映射、外键、索引均已适配),因此迁移可在 SQLite 内存库下完整回归(见第 20 节测试基座)。
23.3 ORM 关联与预加载
在 Model 上定义关联方法,并用 with() 做预加载(避免 N+1):
namespace App\Models;
use Mtphp\Database\Model;
class User extends Model
{
protected static ?string $table = 'users';
public function posts(): \Mtphp\Database\Relations\HasMany
{
return $this->hasMany(Post::class, 'user_id'); // 外键默认 user_id
}
public function profile(): \Mtphp\Database\Relations\HasOne
{
return $this->hasOne(Profile::class, 'user_id');
}
}
class Post extends Model
{
protected static ?string $table = 'posts';
public function author(): \Mtphp\Database\Relations\BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
$users = User::with('posts')->get(); // 预加载 posts,避免 N+1
echo $users[0]['posts'][0]['title'];
$posts = Post::with('author')->get();
echo $posts[0]['author']['name'];
// 嵌套预加载支持 'a.b' 写法
$users = User::with('posts.comments')->get();
要点:hasMany(...) / hasOne(...) / belongsTo(...) 第二个参数为外键(默认 短类名_singular + _id,如 User→user_id);with('rel.sub') 支持点号嵌套预加载。关联结果以数组形式挂到父记录上(与 ORM-lite 返回数组的风格一致)。
23.3.1 多对多(belongsToMany)
经中间表(pivot)连接两个模型,用于「一个用户拥有多个角色、一个角色属于多个用户」这类关系:
class User extends Model
{
public function roles(): \Mtphp\Database\Relations\BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
class Role extends Model
{
public function users(): \Mtphp\Database\Relations\BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
// 预加载(含嵌套 belongsToMany.belongsToMany)
$users = User::with('roles')->get();
echo $users[0]['roles'][0]['name'];
$roles = Role::with('users')->get(); // 反向同样支持
默认约定(均可显式传参覆盖):
- 中间表名:两模型「单数类名 snake」按字母序拼接,如
User+Role⇒role_user;User+Post⇒post_user。 - 中间表两列:指向本模型主键的外键(默认
本表单数名_id,如user_id)、指向关联模型主键的外键(默认关联表单数名_id,如role_id)。 - 可传参定制:
belongsToMany(Role::class, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey)。
belongsToMany 与 hasMany/hasOne/belongsTo 共用同一套 with() 预加载与 Model::load() 懒加载机制,支持点号嵌套(如 users.roles.permissions)。
23.3.2 关联存在性约束(whereHas / has / doesntHave)
在「父模型」上按「关联是否存在 / 计数」过滤结果,底层生成 EXISTS / NOT EXISTS(计数比较则生成 (SELECT COUNT(*) ...) $op $n 标量子查询),支持 hasMany / hasOne / belongsTo / belongsToMany 四种关联;belongsToMany 经「中间表 + 嵌套 EXISTS」连接关联表。
// 有文章的用户(EXISTS)
$users = User::has('posts')->get();
// 文章数 > 1 的用户(计数比较)
$users = User::has('posts', '>', 1)->get();
// 拥有“已发布”文章的用户(带约束闭包,闭包作用于关联模型构造器)
$users = User::whereHas('posts', function ($q) {
$q->where('published', 1);
})->get();
// 没有评论的帖子(NOT EXISTS)
$posts = Post::doesntHave('comments')->get();
// 反向 / 多对多同样可用
$posts = Post::whereHas('author', fn ($q) => $q->where('name', 'alice'))->get();
$users = User::whereHas('roles', fn ($q) => $q->where('name', 'admin'))->get();
$orphans = User::doesntHave('roles')->get();
- 静态入口(
User::has(...)/User::whereHas(...)/User::doesntHave(...))与构造器入口(User::query()->has(...))等价,均返回可继续链式的ModelBuilder。 - 逻辑或:
orHas()/orWhereHas()/orDoesntHave()/orWhereDoesntHave()。
23.3.3 便捷写(firstOrCreate / updateOrCreate / firstOrNew)
按一组属性查找;命中则返回该行,未命中则创建(或按需更新)。三者均复用已接通模型事件的 create / update,返回「行数组」(含主键)。
// 查得到返回,查不到按 [email + name] 创建并返回
$user = User::firstOrCreate(['email' => 'a@b.com'], ['name' => 'A']);
// 查得到就更新 name,查不到就按 [email + name] 创建
$user = User::updateOrCreate(['email' => 'a@b.com'], ['name' => 'A']);
// 查得到返回行;查不到返回“未落库”的属性数组(数组风格 ORM 下等价于未保存模型)
$row = User::firstOrNew(['email' => 'a@b.com'], ['name' => 'A']);
23.4 分页器(Pagination)
Mtphp\Pagination\Paginator。查询构造器与 paginate() 助手均可产出:
use function Mtphp\paginate;
use Mtphp\Database\DB;
$page = DB::table('users')->paginate(15, 1); // 第 1 页,每页 15
// 或:paginate('users', 15, $page)
$page->items(); // 当前页数据(数组)
$page->total(); // 总记录数
$page->perPage(); // 每页条数
$page->currentPage(); // 当前页
$page->lastPage(); // 最后一页
$page->hasMorePages(); // 是否还有下一页
$page->onFirstPage(); // 是否首页
$page->previousPageUrl(); // 上一页 URL(首页为 null)
$page->nextPageUrl(); // 下一页 URL(末页为 null)
$page->setPath('/users'); // 设置分页链接基路径
$page->links(); // 生成分页 HTML(<a> 链接)
$page->toArray(); // ['data' => [...], 'meta' => [...]],便于 JSON 接口
23.5 API 资源转换器(API Resource)
Mtphp\Http\Resources\JsonResource:把“模型 / 数组 → 对外 JSON”的字段裁剪与结构封装集中到一处,避免把内部字段(密码、secret 等)泄露到接口。
namespace App\Http\Resources;
use Mtphp\Http\Resources\JsonResource;
class UserResource extends JsonResource
{
public function toArray($item): array
{
return [
'id' => $item['id'],
'name' => $item['name'],
// 故意裁剪 password 等内部字段
];
}
}
// 使用
$user = User::find(1);
(new UserResource($user))->resolve(); // 单条:直接返回字段数组
UserResource::collection(User::all())->resolve(); // 集合:返回数组列表
UserResource::collection($page)->resolve(); // 分页:自动包成 ['data'=>..., 'meta'=>...]
(new UserResource($user))->response(200); // 直接生成 JSON 响应(Response)
要点:toArray($item) 定义单条转换规则;resolve() 自动识别 单条 / 列表 / 分页器 三种输入,分页输入自动包裹 data + meta(含 total / perPage / current_page / last_page / has_more 等);make($resource) 与 collection($resource) 为静态工厂。
23.6 队列(Queue,文件驱动)
Mtphp\Queue\Queue:零依赖、无需 Redis 等外部服务,任务以 JSON 文件落地到 runtime/queue/<队列名>/ 下,适合中小规模、开发环境与边缘部署。
use Mtphp\Queue\Queue;
$q = new Queue('runtime/queue');
// 或直接用全局助手(应用上下文下自动套用 config('queue.path')):queue()
$q->push(['class' => App\Jobs\SendMail::class, 'data' => ['to' => 'a@b.c']]);
$q->push('纯文本 / 数组任务', 'emails', 60); // 第三个参数 = 延迟秒数
$job = $q->pop(); // 取出一个“当前可消费”的任务(取出即删)
$q->size(); // 待消费数量(不含延迟未到点)
$q->clear(); // 清空
任务载荷为 ['class' => 类名, 'data' => 任意] 时,消费者会自动经容器解析实例并调用其 handle($data):
namespace App\Jobs;
class SendMail
{
public function handle(array $data): void
{
// 发送邮件……
}
}
启动消费者:php bin/mtphp queue:work --queue=default --sleep=2。命令内部循环调用 $q->worker($handler, $queue, $opts):
$q->worker(function (array $job): void {
// 处理 $job['payload']
}, 'default', ['max' => 0, 'once' => false, 'sleep' => 2]);
// max : 最多处理几个(0=不限)
// once : 只取一个就退(适合 cron 单次触发)
// sleep: 空轮询间隔秒
说明:文件驱动为 at-most-once(取出即删);处理抛异常的任务会被 worker() 兜底移入 <队列>/failed/ 目录,可用 failedJobs() 检视,便于排查(无外部中间件时也具备基础容错)。
数据库驱动(Mtphp\Queue\DatabaseQueue,零依赖,推荐生产使用)
queue:work 会按 config('queue.driver') 自动选择驱动;切到 database 即获得可靠投递:原子认领(事务 + SELECT ... FOR UPDATE / 原子 UPDATE 子查询)、崩溃可恢复、内置重试与失败表,满足严格可靠场景。表结构由 DatabaseQueue 自动建(CREATE TABLE IF NOT EXISTS,跨 mysql/sqlite 方言,无需手动迁移)。
// config/queue.php
return [
'driver' => 'database', // file(默认)| database
'connection' => 'mysql', // config/database.php 中的连接名
'table' => 'jobs', // 任务表
'failed_table' => 'failed_jobs', // 失败表
'max_attempts' => 3, // 最大尝试次数(达上限移入失败表)
'retry_delay' => 5, // 重试延迟秒数
'retry_after' => 90, // 认领后超过该秒数仍 reserved(进程崩溃/卡死)即超时回收重试;须大于单任务最大正常处理耗时
];
// 业务代码:与文件驱动完全同接口,无需改调用
queue()->push(['class' => App\Jobs\SendMail::class, 'data' => ['to' => 'a@b.c']]);
queue()->push('纯文本 / 数组任务', 'emails', 60);
// 消费者行为与文件驱动一致;失败任务会按 attempts 自动重试,超过 max_attempts 进入 failed_jobs
php bin/mtphp queue:work --queue=default --sleep=2
要点:
- 文件驱动与数据库驱动都实现
Mtphp\Queue\QueueInterface,上层经QueueManager::resolve()(即queue()助手)透明切换;新增驱动只需实现该接口并扩展QueueManager。 - 数据库驱动
pop()在事务内原子认领并自增attempts;worker()成功后删除,失败则未达上限时释放(延迟后重试)、达上限时写入failed_jobs并删除。 database驱动同样支持延迟任务(available_at)与多队列(queue字段 + 索引)。
23.6.1 延迟任务(精确调度)
两种驱动都支持“到点才消费”:push($payload, $queue, $delay) 的第三个参数 $delay 是延迟秒数,任务在 now + delay 之前不会被 pop 出来,也不计入 size()(待消费数)。可用于定时发信、重试退避等:
// 30 秒后才可被消费
queue()->push(['class' => App\Jobs\SendMail::class, 'data' => ['to' => 'a@b.c']], 'default', 30);
注意:延迟是“最早可消费时间”而非“精确触发”;消费者轮询(
--sleep)决定实际触发精度,生产环境建议将--sleep设小(如 1~2 秒)。
23.6.2 常驻部署(Supervisor / systemd)
queue:work 是阻塞常驻进程,生产环境需要进程守护(崩溃自动拉起、开机自启)。下面给出两份开箱配置。
Supervisor(/etc/supervisor/conf.d/mtphp-queue.conf):
[program:mtphp-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/mtphp/bin/mtphp queue:work --queue=default --sleep=2
autostart=true
autorestart=true ; 崩溃自动重启
numprocs=2 ; 多进程并发消费(按 CPU/任务量调整)
redirect_stderr=true
stdout_logfile=/var/www/mtphp/runtime/logs/queue.log
supervisorctl reread && supervisorctl update && supervisorctl start mtphp-queue:*
systemd(单实例可换成 --once + timer 做 cron 式单次触发):
# /etc/systemd/system/mtphp-queue.service
[Unit]
Description=mtphp queue worker
After=network.target
[Service]
ExecStart=php /var/www/mtphp/bin/mtphp queue:work --queue=default --sleep=2
Restart=always
User=www-data
[Install]
WantedBy=multi-user.target
systemctl enable --now mtphp-queue.service
要点:
- 消费进程无状态、可水平扩展(多
numprocs或多机),数据库驱动靠事务原子认领保证并发不重复消费。 - 失败任务统一在
failed_jobs(数据库驱动)或<队列>/failed/(文件驱动),排查后重放即可;不要手工删任务表。 - 上线前把
config/app.php的debug设为false、确认config/queue.php的retry_after大于单任务最大正常耗时(否则处理中任务会被误回收导致重复消费)。
23.6.3 定时与重复任务(任务调度器)
框架内置零依赖的任务调度器(Mtphp\Schedule\Schedule + Event + CronExpression,思想借鉴主流框架的 Task Scheduler,独立实现),用标准 5 段 cron 表达“定时”与“重复(周期)”触发,并把任务体路由到闭包 / 控制台命令 / 队列 / 事件四类执行通道——其中 job() 通道即“定时 + 重复任务 × 队列”的集成点:用 cron 描述周期,用队列承载实际执行(异步、可重试、可分布式消费)。
四种任务体:
use Mtphp\Schedule\Schedule;
$schedule->call(function (): void {
cache()->clear(); // ① 闭包:清理缓存
})->dailyAt('02:00')->name('clear-cache');
$schedule->command('queue:work --once') // ② 控制台命令(用 --once 单次触发,或另跑常驻 queue:work)
->everyFiveMinutes()->name('queue-tick');
$schedule->job( // ③ 入队(到点把任务推入队列,交给 queue:work 消费)
['class' => App\Jobs\DailyReport::class, 'data' => []],
'default', 0
)->dailyAt('03:00')->name('daily-report');
$schedule->event(App\Events\Heartbeat::class) // ④ 派发框架事件(类名或已构造对象)
->everyMinute();
调度定义文件:默认读取 app/Routes/schedule.php(与 routes.php 同级),$schedule 变量在该文件作用域内可用,由 schedule:run / schedule:work 自动加载。
时间表达式(cron 流式 API):
| 方法 | 等价于 cron | 含义 |
|---|---|---|
cron('15 10 * * 1-5') | 15 10 * * 1-5 | 周一至周五 10:15 |
everyMinute() | * * * * * | 每分钟 |
everyFiveMinutes() | */5 * * * * | 每 5 分钟 |
everyTenMinutes() / everyFifteenMinutes() / everyThirtyMinutes() | */10 / */15 / */30 | 对应步长 |
hourly() / hourlyAt(30) | 0 * * * * / 30 * * * * | 每小时 / 每小时第 30 分 |
daily() / dailyAt('02:30') / at('02:30') | 0 0 * * * / 30 2 * * * | 每天 / 每天固定时刻 |
twiceDaily(1, 13) | 0 1,13 * * * | 每天 1 点、13 点 |
weekly() | 0 0 * * 0 | 每周日 |
monthly() / monthlyOn(15, '04:00') | 0 0 1 * * / 0 4 15 * * | 每月 1 号 / 每月 15 号 4 点 |
yearly() | 0 0 1 1 * | 每年 1 月 1 日 |
段内支持 *(任意)、*/n(步长)、a-b(范围)、a,b,c(列表)与单值,月/周支持英文缩写(JAN…DEC、MON…SUN);日与周同时限定时为“或(OR)”关系(Vixie cron 语义)。
条件与互斥:
$schedule->call($task)
->weekdays() // 仅工作日(便捷封装,等价于 cron 1-5)
->when(fn () => someFlag()) // 仅当回调返回 true 才跑
->skip(fn () => Maintenance::on()) // 回调返回 true 则本次跳过
->withoutOverlapping(10); // 互斥:窗口内(分钟)只允许跑一次,防重叠(基于文件锁,跨进程安全)
说明:
withoutOverlapping用文件锁(runtime/schedule/*.lock,默认)实现跨进程互斥,无需 Redis;锁目录随App根目录自动确定,或在测试/特殊部署下用Event::setMutexPath()覆盖。
运行方式(二选一):
# 方式一:系统 crontab 每分钟触发一次(推荐生产,最稳)
* * * * * php /var/www/mtphp/bin/mtphp schedule:run >> /var/www/mtphp/runtime/logs/schedule.log 2>&1
# 方式二:框架内置守护(免系统 cron,适合无 cron 权限的托管/容器,用 Supervisor 守护本进程)
php /var/www/mtphp/bin/mtphp schedule:work --interval=60
php bin/mtphp schedule:list # 列出全部任务、cron、下次运行时间(核对定义是否如预期)
php bin/mtphp schedule:run # 执行所有到期任务(单发,由 cron 每分钟调用)
要点:
- 单个任务抛异常会被
runDue()捕获并记日志(logger()->error()),不影响其余任务;命令类任务经同一个Console\Application执行,与手写 CLI 完全等价。 job()通道把“重复任务”落到队列:调度器按 cron 到点把 payload 推入队列,真正的重活由queue:work异步消费——天然获得队列的可靠投递/重试/水平扩展能力;这也是“队列的定时与重复任务”推荐写法。- 若只需“跑一次且精确某个时刻”,用
->cron('0 2 1 1 *')之类一次性表达式即可;调度器无状态、不依赖外部存储(互斥锁除外),重启不影响周期定义。
23.7 校验器(Validator)
Mtphp\Validation\Validator:用最简单的方式完成最常见的入参校验;全局助手 validate($data, $rules) 校验失败直接抛 ValidationException。
use Mtphp\Validation\Validator;
$v = Validator::make($input, [
'name' => 'required|max:50',
'email' => 'required|email',
'age' => 'nullable|integer|min:0|max:150',
'status' => 'required|in:0,1,2',
'password' => 'required|confirmed', // 需有 password_confirmation 字段
]);
if ($v->fails()) {
return $v->errors(); // ['name' => ['name 为必填项', ...], ...]
}
$data = $v->validated(); // 通过校验的字段集合
// 便捷写法(校验不过直接抛异常)
$data = validate($input, ['email' => 'required|email']);
规则写法:字符串 'required|email|max:255'(竖线分隔、冒号带参)、数组 ['required','email']、闭包(自定义)。内置规则:required / required_if / email / numeric / integer / min / max / between / length / in / not_in / regex / confirmed / same / alpha / alpha_num / url / ip / date / unique:table,column(unique 借助 DB 构造器参数化查重,防注入)。此外还有一组更细的规则:
- 布尔 / 数字:
boolean、digits:n(恰好 n 位数字)、digits_between:min,max。 - 类型:
array(值必须是数组)、json(合法 JSON 字符串)。 - 字符串形态:
starts_with:xxx,yyy、ends_with:.png,.jpg、different:field(与另一字段不同)。 - 时间:
before:时间/字段、after:时间/字段(参数为日期字符串或同名字段名)。 - 尺寸:
size:n(字符串按长度、数值按值、数组按个数相等)。 - 勾选:
accepted(值为 yes/on/1 等)。
自定义规则:Validator::extend('rule', fn ($value, $params) => bool)。错误信息支持 :field / :param / :value 占位符,可在第三个参数自定义文案。
23.8 鉴权(Auth)
Mtphp\Auth\Auth(Session Guard,零依赖):以“会话 + 用户提供者”方式管理登录态,与具体存储解耦。
use Mtphp\Auth\Auth;
auth()->setUserProvider(fn ($id) => User::find($id)); // 注册“按 id 取用户”
auth()->attempt(fn ($c) => User::findBy('email', $c['email']), $input); // 尝试登录
if (auth()->check()) {
$id = auth()->id();
$user = auth()->user(); // 经提供者解析出的用户数组(带缓存)
}
auth()->logout();
要点:login(array $user) 直接以用户数组登录;attempt($resolver, $creds) 用解析器取用户、成功则写入会话;check() / guest() / id() / user() / logout() 为常用方法。守卫绑定当前会话(同 Session::current()),CLI / 测试无运行中会话时回落到内存会话。
多守卫(Multi-Guard):auth() 现由 Mtphp\Auth\AuthManager 统一管理,按名隔离——auth('admin') 取 admin 守卫,auth() 取默认守卫(见 config/auth.php 的 defaults.guard,默认 web)。各守卫拥有独立的会话键(mtphp_auth_id_{guard})与记住我 Cookie(remember_{guard}),互不串味;同一请求内按名复用同一实例。
auth('web')->setUserProvider(fn ($id) => WebUser::find($id));
auth('admin')->setUserProvider(fn ($id) => AdminUser::find($id));
// web 与 admin 登录态完全隔离
记住我(Remember Me):login($user, $remember = true) 会生成并下发加密的长期 Cookie(用户id|remember_token,默认 30 天),下次访问若会话已失效则凭该 Cookie 自动重新登录。配合用户表的 remember_token 列使用:
auth()->setRememberResolver(fn (int $id, string $token) => User::findByRemember($id, $token)); // 校验 id + token
auth()->setRememberPersister(fn (int $id, string $token) => User::update($id, ['remember_token' => $token])); // 持久化(可选)
auth()->attempt($resolver, $input, $remember = true); // 登录时勾选记住我
// 用户表需含 remember_token 列;若登录时用户数据未带该值,框架自动生成并经 persister 落库
提示:Web 场景把
Request/Response注入守卫(auth('web')->setRequest($req)->setResponse($resp))即可自动收发记住我 Cookie;无状态 API 令牌守卫由Mtphp\Http\Middleware\AuthApi(配合Mtphp\Auth\Token)提供,可视为“api”守卫实现。
23.9 会话(Session)
Mtphp\Http\Session:提供 get / set / forget / flush、一次性 flash 闪存与会话重生成;支持 file(落地磁盘、跨请求持久)与 array(内存,适合 CLI / 测试 / 无状态)两种驱动。Web 场景配合 StartSession 中间件自动生效。
session()->set('cart', $items);
$items = session()->get('cart');
session()->flash('status', '保存成功'); // 仅下一轮请求可读
session()->getFlash('status'); // 本轮读 flash
session()->reflash(); // 再保留一轮
session()->regenerate(); // 登录/提权时重生成 id(降固定攻击风险)
session()->forget('cart'); // 删除单项
session()->flush(); // 清空
说明:本类只管“id + 数据”的存取与持久化;Cookie 收发由 StartSession 中间件负责,使会话与传输解耦。
23.10 CORS 中间件
Mtphp\Http\Middleware\Cors:框架内置,默认关闭,可由 config/app.php 的 security.cors 开启,或仅在某组 API 路由上单独挂载。
// config/app.php
'security' => [
'cors' => true, // 开启 CORS
'cors_origins' => ['https://app.example.com'], // 或 ['*'] 放行全部
'cors_methods' => ['GET','POST','PUT','DELETE','PATCH','OPTIONS'],
'cors_headers' => ['Content-Type','X-CSRF-TOKEN','Authorization'],
'cors_max_age' => 86400,
],
能力:按 cors_origins 回写 Access-Control-Allow-Origin(非 * 时仅反射白名单来源,避免泄露);回写 Allow-Methods / Allow-Headers / Max-Age;对预检 OPTIONS 请求直接返回 204 短路。
23.11 事件分发(Events)
Mtphp\Events\Dispatcher(思想借鉴主流框架的事件系统):支持闭包、类监听器([类,方法] 经容器解析)与类名字符串监听器。
use Mtphp\Events\Dispatcher;
event()->listen(OrderPaid::class, [App\Listeners\Notify::class, 'handle']);
event()->listen(OrderPaid::class, fn (OrderPaid $e) => /* ... */);
event(new OrderPaid($order)); // 派发;监听器可按父类/接口匹配,并可就地修改事件对象
说明:event() 无参返回全局分发器;派发时既匹配具体类名,也匹配其全部父类 / 接口,便于基类事件统一监听。类监听器经容器解析,支持依赖注入。
23.12 HTTP 客户端(HTTP Client)
Mtphp\Http\Client:fluent 链式,底层 curl,零依赖。
use function Mtphp\http;
$resp = http('https://api.example.com')
->withToken($token)
->asJson()
->get('/users', ['page' => 1]);
$resp->status(); // HTTP 状态码
$resp->ok(); // 是否 2xx
$resp->successful(); // 是否 2xx/3xx
$resp->body(); // 原始响应体
$resp->json(); // 解析 JSON(失败返回 null)
$resp->header('X-Rate');// 单个响应头
$resp->headers(); // 全部响应头
// 其它方法:post/put/delete/patch/send;withHeaders/withHeader/asForm/timeout/withOptions
要点:GET 的数据转 query,其余默认按 JSON 编码(也可 asForm());setTransport($fn) 可注入假传输层,便于离线测试请求构建与响应解析。
23.13 本地化(i18n)
Mtphp\Translation\Translator:多语言取词,占位符 :key 替换;全局助手 __($key, $replace)。
use Mtphp\Translation\Translator;
$t = Translator::instance();
$t->setLocale('en');
$t->addLines(['welcome' => 'Hello, :name'], 'en');
echo __('welcome', ['name' => '张三']); // Hello, 张三
$t->setFallback('zh'); // 取不到时回落到的语言
要点:setLocale() 切换语言、addLines() 批量注入词条(键支持点号层级,自动展平)、get($key,$replace) 取词并替换 :key;setFallback() 设置回落语言。
小结:以上组件均零依赖、零配置即可用,并与既有核心(容器 / 路由 / 模板 / DB)风格一致;命名空间统一为
Mtphp\,模板仅借鉴主流框架思想、独立实现,规避版权风险。