| name | php-runtime-func-test |
| description | Ensure that when implementing or modifying PHP runtime or std/php functions in the Origami project, the agent always creates a minimal PHP test script under tests/php/ and executes it via the Origami runner (go run ./zy.go ...) to verify behavior end-to-end. |
PHP Runtime Function + Test Script Workflow
使用场景
在本项目中,当代理新增或修改 PHP 运行时相关函数时(尤其是:
std/php/ 下的内置函数封装(如 set_exception_handler、trigger_error 等)
std/php/core/* 中的辅助函数
- 与 PHP 内置函数/关键字语义强相关的包装函数
必须按本 Skill 的流程同时补一份可直接运行的 PHP 测试脚本,并实际执行一次验证。
操作步骤
1. 补充或修改函数实现
- 在合适位置实现或修改函数:
- 通常在
std/php/core/*.go 或 std/php/*.go 中新增 XXXFunction 结构体并实现:
Call(ctx data.Context) (data.GetValue, data.Control)
GetName() string
GetParams() []data.GetValue
GetVariables() []data.Variable
- 在
std/php/load.go 的 Load(vm data.VM) 中注册该函数:
- 将
NewYourFunction() 加入 for _, fun := range []data.FuncStmt{ ... } 列表。
这一步是 Origami 中让函数“被 PHP 代码真正看到”的前提。
2. 在 tests/php/ 下新建最小可复现脚本
- 在
tests/php/ 目录下创建一个新的 .php 文件,文件名建议遵循:
<function_name>_test.php,例如:set_exception_handler_test.php
- 测试脚本格式规范(参考同目录下
json_serializable_test.php、ksort_test.php 等):
- 首行
<?php 后空一行,接着必须写 namespace tests\php;
- 文件顶部用 docblock
/** ... */ 简要说明本测试目的(一两行即可)
- 失败时用 Log::fatal('...') 报错并终止,成功时用 Log::info('...测试通过') 收尾
- 不要用
throw new \Exception() 或 echo "ok\n" 表示通过/失败,统一用 Log
- 脚本内容要尽量最小化但可见效果,典型结构:
<?php
namespace tests\php;
use Exception;
set_exception_handler(function ($e) {
Log::info('set_exception_handler 捕获到未处理异常: ' . $e->getMessage());
});
function throw_unhandled_exception() {
throw new Exception('这是一个测试异常');
}
throw_unhandled_exception();
关键点:脚本必须只依赖已经在项目里存在的类/函数,并能通过日志或输出看到行为差异。
类命名约定:tests/php/ 下所有测试共用命名空间 tests\php,因此类名不要起得太通用,以免与其他测试或将来新增测试冲突。应为测试中定义的类使用唯一前缀(例如与测试主题相关:MagicMethods_CallTester、MagicMethods_BaseCallParent),而不是泛用名(如 CallTester、BaseCallParent)。
3. 通过 Origami 运行脚本进行端到端验证
- 在项目根目录下运行(代理应使用 Shell 工具执行):
go run ./zy.go tests/php/set_exception_handler_test.php
- 预期:
- 进程正常退出(
exit code 0 或符合预期的非 0 码)
- 终端输出中包含脚本中预期的日志/信息,例如:
[INFO] set_exception_handler 捕获到未处理异常: 这是一个测试异常
- 若行为与预期不符:
- 回到函数实现和测试脚本,查明差异(参数传递、上下文
Context/VM、异常处理路径等)
- 修正后再次运行同一脚本,直至输出符合 PHP 语义预期。
4. 记录兼容性语义(可选但推荐)
当函数是与 PHP 标准行为强相关时(如 set_exception_handler、Closure::bind、call_user_func 等),在对应的 .go 文件顶部用简短注释说明:
- 当前实现是否完全对齐 PHP 行为
- 如有差异,在哪些场景做了简化或不支持
示例(已存在的 set_exception_handler 实现):
使用本 Skill 的要点
当你在本项目中:
- “新增一个 PHP 内置函数”
- “修复某个 std/php 函数逻辑”
- “补充 runtime 里与 PHP 关键字/异常系统相关的行为”
请务必:
- 先实现/修改函数并注册到 VM
- 再在
tests/php/ 下新建一个可直接运行的 .php 脚本
- 最后通过
go run ./zy.go tests/php/xxx_test.php 实际运行验证
- 测试脚本中的类名使用唯一前缀:因所有测试共用命名空间
tests\php,类名不要写太通用的(如 CallTester、BaseParent),应加与测试主题相关的前缀(如 MagicMethods_CallTester),避免与其他测试冲突。
- 永远不要忽略
data.Control 返回值:当你调用任何返回 (X, data.Control) 的函数(如 vm.GetOrLoadClass / vm.GetOrLoadInterface / node.NewXXX().GetValue 等),必须检查 acl != nil 并及时向上返回或处理,禁止写成 _, _ = fn(...)、_ , _ := fn(...) 这种丢弃控制流的用法,否则会吞掉运行时错误、抑制 throw/return/continue/break 等控制信号。
只有当以上步骤都完成且脚本行为符合预期时,这次函数补充才算完成。
测试脚本与问题定位(避免“测试文件总是错”)
- 不要默认怪测试文件:当用户说“测试错了”或“输出不对”时,先认定是运行时/解析器的问题,除非能明确证明是测试用例写错(例如断言与 PHP 官方文档不符)。
- 用真实代码定位:若问题来自真实项目(如 Symfony、vendor 下的代码),以用户给出的具体文件与行号为准(例如
vendor/symfony/console/Descriptor/TextDescriptor.php:233),在该行写的是“应该输出 $content 却输出了 true”时,优先考虑:
- 运算符优先级:如
a && b ? c : d 在 PHP 中为 (a && b) ? c : d;若解析成 a && (b ? c : d) 会得到错误结果。应在 parser/expression_parser.go 中保证 &&/|| 的右侧不吞掉其后的 ? :(例如 parseLogicalAnd 的右侧用 parseBitwiseOr 而非 parseAssignment)。
- 其它解析/求值顺序:对照 PHP 官方运算符优先级表检查解析层级。
- 测试脚本的用途:
tests/php/ 下的脚本用于回归/最小复现。若 bug 已在真实文件中定位,可先在该真实文件上验证修复,再在 tests/php/ 里补一个最小用例(例如只包含有问题的表达式)防止回退。