mtphp 文档

进阶组件(补齐主流框架常用能力)

本章汇总框架“开箱即用”的一批进阶组件:控制台、数据库迁移、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):

列修饰(链式):->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();          // 反向同样支持

默认约定(均可显式传参覆盖):

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();

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

要点:

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

要点:

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 每分钟调用)

要点:

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 构造器参数化查重,防注入)。此外还有一组更细的规则:

自定义规则: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\,模板仅借鉴主流框架思想、独立实现,规避版权风险。