PHP 如何让同一个地方创建的匿名类不同?(含代码)
2025-03-30 14:50:10
PHP 里怎么让同一个地方创建的匿名类不一样?
写 PHP 时,你可能碰到过这么个事儿:用同一个方法创建的匿名类,它们居然是同一个“类”(class)。
看下面这段代码:
<?php
// 定义一个函数,返回一个匿名类的实例
function test($test) {
return new class($test) {
public function __construct(public $test) {}
};
}
// 定义另一个结构几乎一样的函数
function test2($test) {
return new class($test) {
public function __construct(public $test) {}
};
}
// 比较函数
function compare($a, $b) {
var_dump([
'$a == $b' => $a == $b, // 比较值 (松散比较)
'$a === $b' => $a === $b, // 比较引用和类型 (严格比较)
'get_class($a) == get_class($b)' => get_class($a) == get_class($b), // 比较类名
]);
echo "Class A: " . get_class($a) . "\n";
echo "Class B: " . get_class($b) . "\n\n";
}
echo "比较 test(1) 和 test(1):\n";
compare(test(1), test(1));
/* 输出类似:
array(3) {
'$a == $b' => bool(true) // 值相同,松散比较为 true
'$a === $b' => bool(false) // 不同实例,严格比较为 false
'get_class($a) == get_class($b)' => bool(true) // 类名相同!
}
Class A: class@anonymous/path/to/your/script.php:6$a6b...
Class B: class@anonymous/path/to/your/script.php:6$a6b...
*/
echo "比较 test(['abc']) 和 test(1):\n";
compare(test(['abc']), test(1));
/* 输出类似:
array(3) {
'$a == $b' => bool(false) // 值不同
'$a === $b' => bool(false) // 不同实例
'get_class($a) == get_class($b)' => bool(true) // 类名还是相同!
}
Class A: class@anonymous/path/to/your/script.php:6$a6b...
Class B: class@anonymous/path/to/your/script.php:6$a6b...
*/
echo "比较 test(1) 和 test2('xx'):\n";
compare(test(1), test2('xx'));
/* 输出类似:
array(3) {
'$a == $b' => bool(false)
'$a === $b' => bool(false)
'get_class($a) == get_class($b)' => bool(false) // 类名不同了!
}
Class A: class@anonymous/path/to/your/script.php:6$a6b...
Class B: class@anonymous/path/to/your/script.php:12$cde... // 注意行号和后面的哈希可能不同
*/
echo "比较 test('xx') 和 test2('xx'):\n";
compare(test('xx'), test2('xx'));
/* 输出类似:
array(3) {
'$a == $b' => bool(true) // 注意:这里值可能相同
'$a === $b' => bool(false)
'get_class($a) == get_class($b)' => bool(false) // 类名还是不同
}
Class A: class@anonymous/path/to/your/script.php:6$a6b...
Class B: class@anonymous/path/to/your/script.php:12$cde...
*/
从结果能看出来:
test(1)
和test(1)
创建了两个不同的对象实例(===
是false
),但它们属于同一个匿名类 (get_class()
返回值相同)。对象的值可能相同 (==
是true
)。test(1)
和test2('xx')
创建的对象不仅实例不同,连类也不同 (get_class()
返回值不同)。这是因为匿名类的定义在不同的函数 (test
和test2
) 里。
这就引出了问题:如果我需要用 同一段代码逻辑(比如同一个函数)生成很多实例,但又希望 PHP 把它们看作是 不同的类,该怎么办?总不能像上面那样,手动复制粘贴搞出 test3()
, test4()
, ...
, test50()
吧?这也太蠢了。
就像提问者遇到的场景:在写测试时,需要给一个方法传入实现了某个接口的对象。为了隔离测试,他用了匿名类来实现这个接口,接口方法返回一个动态生成的值。但测试框架或被测代码内部依赖 ::class
来区分不同的输入类型。结果创建了 50 个对象,虽然返回值都不同,但因为它们来自同一个 test()
函数,所以类名都一样,导致测试达不到预期效果。
为啥会这样?匿名类的“身份”是怎么定的?
简单说,PHP 解释器在碰到 new class(...) { ... }
这种语法时,会偷偷给这个匿名类生成一个内部名字。这个名字通常和它在哪个文件、哪一行被定义的有关系。你可以从 get_class()
的输出看到类似 class@anonymous/path/to/your/script.php:6$a6b...
的格式。
关键在于,PHP 认为在 同一个地方(同一个文件、同一个函数或方法内部、同一行代码)定义的匿名类,就是 同一个类定义。所以,你每次调用 test()
函数,虽然都 new
了一个新对象,但这个 new
操作是基于 同一个 匿名类的定义的。PHP 不会因为你调用了多次函数,就为 test()
函数内部第 6 行的那个 new class
生成不同的类定义。
而 test()
和 test2()
里的 new class
,虽然长得一模一样,但它们位于 不同的函数(不同的代码上下文)里。所以 PHP 会给它们生成不同的内部类名,自然就被当作不同的类了。
怎么破?让同一个“模子”刻出不同的“类”
既然问题的根源在于 PHP 依据匿名类定义的“位置”来确定其身份,那我们想办法让每次创建时,这个“位置”看起来不一样不就行了?
下面介绍两种主要方法。
方法一:试试 eval()
,简单粗暴但有效
eval()
函数能把一个字符串当作 PHP 代码来执行。我们可以利用它来动态生成包含匿名类定义的代码字符串,然后执行。
原理:
每次调用 eval()
执行含有 new class(...)
的代码时,PHP 可能会认为这是一个新的、动态的代码执行上下文。在这个上下文里定义的匿名类,即使结构和之前 eval()
调用里的完全一样,PHP 也可能给它分配一个新的内部类名。这就达到了我们想要的效果。
代码示例:
<?php
interface MyInterface {
public function getValue(): string;
}
// 使用 eval 创建实现了 MyInterface 的匿名类实例
function create_unique_anonymous_class_via_eval(string $value): MyInterface {
// 构造包含匿名类定义的 PHP 代码字符串
// 注意:这里简单拼接字符串。如果 $value 来自不可信来源,会有安全风险!
// 这里 $value 仅用于构造函数,相对安全些,但仍需谨慎。
$classDefinition = '
return new class("' . addslashes($value) . '") implements MyInterface {
private string $internalValue;
public function __construct(string $val) {
$this->internalValue = $val;
}
public function getValue(): string {
return $this->internalValue;
}
}; // 注意最后的分号
';
// 执行字符串代码,eval 会返回 return 语句的值
// 使用 @ 抑制可能的解析错误或其他警告,生产环境应有更健壮的错误处理
$instance = @eval($classDefinition);
if ($instance === false || !($instance instanceof MyInterface)) {
// eval 失败或返回的不是期望的类型
// 在实际应用中应该抛出异常或记录错误
throw new \RuntimeException("Failed to create anonymous class via eval for value: " . $value);
}
return $instance;
}
// 测试一下
$obj1 = create_unique_anonymous_class_via_eval("hello");
$obj2 = create_unique_anonymous_class_via_eval("world");
$obj3 = create_unique_anonymous_class_via_eval("world"); // 再创建一个值相同的
echo "比较 obj1 和 obj2:\n";
var_dump([
'$obj1 == $obj2' => $obj1 == $obj2,
'$obj1 === $obj2' => $obj1 === $obj2,
'get_class($obj1) == get_class($obj2)' => get_class($obj1) == get_class($obj2),
]);
echo "Class Obj1: " . get_class($obj1) . "\n";
echo "Class Obj2: " . get_class($obj2) . "\n\n";
/* 输出类似:
array(3) {
'$obj1 == $obj2' => bool(false)
'$obj1 === $obj2' => bool(false)
'get_class($obj1) == get_class($obj2)' => bool(false) // 类名不同了!
}
Class Obj1: class@anonymous/.../script.php(32) : eval()'d code:2$XXX...
Class Obj2: class@anonymous/.../script.php(32) : eval()'d code:2$YYY...
*/
echo "比较 obj2 和 obj3:\n";
var_dump([
'$obj2 == $obj3' => $obj2 == $obj3, // 值相同,松散比较可能为 true
'$obj2 === $obj3' => $obj2 === $obj3, // 不同实例
'get_class($obj2) == get_class($obj3)' => get_class($obj2) == get_class($obj3), // 类名还是不同!
]);
echo "Class Obj2: " . get_class($obj2) . "\n";
echo "Class Obj3: " . get_class($obj3) . "\n\n";
/* 输出类似:
array(3) {
'$obj2 == $obj3' => bool(true)
'$obj2 === $obj3' => bool(false)
'get_class($obj2) == get_class($obj3)' => bool(false) // 类名依旧不同!
}
Class Obj2: class@anonymous/.../script.php(32) : eval()'d code:2$YYY...
Class Obj3: class@anonymous/.../script.php(32) : eval()'d code:2$ZZZ...
*/
echo $obj1->getValue() . "\n"; // 输出 "hello"
echo $obj2->getValue() . "\n"; // 输出 "world"
注意事项和安全建议:
eval()
是魔鬼! 这是最重要的提醒。eval()
执行任意字符串代码,如果这个字符串的内容受到外部输入(比如用户请求、数据库内容)的影响,而且没有经过极其严格的过滤和转义,就可能导致 严重的安全漏洞 (代码注入)。攻击者可能执行任意 PHP 代码。- 仅在绝对信任代码来源时使用。 在上面例子里,我们构造的
$classDefinition
字符串只包含我们自己写的、固定的类结构,以及一个经过addslashes
处理的$value
。相对可控,但仍需警惕。如果类结构本身需要根据外部输入动态生成,风险会指数级增加。 - 性能开销。 每次调用
eval()
,PHP 都需要解析、编译、执行这段代码,比直接调用函数或方法开销大。如果需要大量生成,性能影响可能比较明显。 - 调试困难。
eval()
内部如果出错,错误信息可能指向eval()
调用的那一行,而不是eval
代码内部真正出错的位置,给调试带来麻烦。上面示例用了@
抑制符,实际应用中最好不用,或者配合try...catch
和错误处理机制。
进阶使用:
你可以让 create_unique_anonymous_class_via_eval
函数接受更多参数,动态构建更复杂的匿名类结构字符串。比如,根据参数决定实现哪些接口、包含哪些方法等。但这会进一步增加构造代码字符串的复杂度和潜在风险。
方法二:生成临时 PHP 文件再 include
如果你对 eval()
的安全性非常担心(这是应该的),可以考虑另一种稍微麻烦但更安全的方式:动态生成一个包含匿名类定义的临时 PHP 文件,然后 include
这个文件。
原理:
include
或 require
一个 PHP 文件时,PHP 会执行这个文件里的代码。每个临时文件都相当于一个独立的“代码位置”。我们在每个临时文件里定义那个匿名类结构,并让文件执行后返回 new
出来的实例。这样,每次 include
一个新生成的临时文件,PHP 都会认为是从一个新位置加载类定义,从而得到不同的匿名类。
代码示例:
<?php
interface MyInterface {
public function getValue(): string;
}
// 使用临时文件创建实现了 MyInterface 的匿名类实例
function create_unique_anonymous_class_via_file(string $value): MyInterface {
// 定义匿名类代码模板
// 使用 var_export 确保 $value 被安全地嵌入到 PHP 代码字符串中
$classCode = '<?php
// 文件被 include 时执行这里
interface MyInterface { // 重新定义接口以确保文件独立性,或者确保已加载
public function getValue(): string;
}
return new class(' . var_export($value, true) . ') implements MyInterface {
private string $internalValue;
public function __construct(string $val) {
$this->internalValue = $val;
}
public function getValue(): string {
return $this->internalValue;
}
};';
// 创建一个唯一的临时文件名
$tempFilePath = tempnam(sys_get_temp_dir(), 'anon_class_');
if ($tempFilePath === false) {
throw new \RuntimeException("无法创建临时文件");
}
// 将代码写入临时文件
if (file_put_contents($tempFilePath, $classCode) === false) {
// 尝试删除已创建但写入失败的文件
unlink($tempFilePath);
throw new \RuntimeException("无法写入临时文件: " . $tempFilePath);
}
// 使用 try...finally 确保临时文件总能被删除
try {
// include 临时文件,它会返回匿名类的实例
$instance = include $tempFilePath;
if (!($instance instanceof MyInterface)) {
throw new \RuntimeException("临时文件未返回预期的 MyInterface 实例");
}
return $instance;
} finally {
// 无论 include 是否成功或抛出异常,都尝试删除临时文件
if (file_exists($tempFilePath)) {
unlink($tempFilePath);
}
}
}
// 测试一下
$obj4 = create_unique_anonymous_class_via_file("apple");
$obj5 = create_unique_anonymous_class_via_file("banana");
$obj6 = create_unique_anonymous_class_via_file("banana"); // 再创建一个值相同的
echo "比较 obj4 和 obj5:\n";
var_dump([
'$obj4 == $obj5' => $obj4 == $obj5,
'$obj4 === $obj5' => $obj4 === $obj5,
'get_class($obj4) == get_class($obj5)' => get_class($obj4) == get_class($obj5),
]);
echo "Class Obj4: " . get_class($obj4) . "\n";
echo "Class Obj5: " . get_class($obj5) . "\n\n";
/* 输出类似:
array(3) {
'$obj4 == $obj5' => bool(false)
'$obj4 === $obj5' => bool(false)
'get_class($obj4) == get_class($obj5)' => bool(false) // 类名不同!
}
// 类名会包含临时文件的路径,每次都不同
Class Obj4: class@anonymous/.../tmp/anon_class_XXXXXX:8$...
Class Obj5: class@anonymous/.../tmp/anon_class_YYYYYY:8$...
*/
echo "比较 obj5 和 obj6:\n";
var_dump([
'$obj5 == $obj6' => $obj5 == $obj6, // 值相同,松散比较可能为 true
'$obj5 === $obj6' => $obj5 === $obj6, // 不同实例
'get_class($obj5) == get_class($obj6)' => get_class($obj5) == get_class($obj6), // 类名还是不同!
]);
echo "Class Obj5: " . get_class($obj5) . "\n";
echo "Class Obj6: " . get_class($obj6) . "\n\n";
/* 输出类似:
array(3) {
'$obj5 == $obj6' => bool(true)
'$obj5 === $obj6' => bool(false)
'get_class($obj5) == get_class($obj6)' => bool(false) // 类名依旧不同!
}
Class Obj5: class@anonymous/.../tmp/anon_class_YYYYYY:8$...
Class Obj6: class@anonymous/.../tmp/anon_class_ZZZZZZ:8$...
*/
echo $obj4->getValue() . "\n"; // 输出 "apple"
echo $obj5->getValue() . "\n"; // 输出 "banana"
注意事项:
- 文件系统操作开销: 相比
eval()
, 这种方法涉及磁盘 I/O(创建、写入、读取、删除文件),在高频率调用下,性能开销可能会更大。 - 临时文件管理: 需要确保临时文件在使用后被妥善删除,即使发生错误。上面的
try...finally
结构是推荐做法。也可以使用register_shutdown_function()
来注册一个清理函数,作为最后的保障。 - 临时目录权限: PHP 进程需要有在系统临时目录(
sys_get_temp_dir()
返回的路径)创建和删除文件的权限。 - 依赖接口/类定义: 如果匿名类需要实现某个接口或继承某个基类,这个接口/基类的定义必须在
include
临时文件时是可用的。一种方法是在临时文件代码里也包含接口/类的定义(如示例中对MyInterface
的处理,但这可能引入重复定义问题,如果接口已在别处加载),或者确保在调用include
之前,所需的定义已经通过require_once
或自动加载机制加载好了。 - Opcache 影响? 在某些 PHP Opcache 配置下,对
include
的文件可能会有缓存。由于我们每次都用tempnam
生成新文件,理论上不会命中缓存。但如果复用文件名(不推荐),需要注意缓存可能导致的问题。
进阶使用:
可以将类定义的代码模板做得更复杂,通过占位符替换等方式,根据传入 create_unique_anonymous_class_via_file
的参数动态生成更定制化的类结构,然后写入临时文件。使用 var_export($value, true)
来生成嵌入到代码中的数据,通常比手动字符串拼接和转义更安全可靠。
总结一下
当需要让 PHP 在同一个代码点(比如同一个函数内部)创建的匿名类实例被识别为 不同的类 时,核心思路是打破 PHP 基于“定义位置”判断类身份的常规机制。
eval()
:通过在运行时执行字符串代码创建匿名类。简单直接,但有 严重的安全风险 ,性能开销中等,调试相对困难。只在完全信任输入或代码生成逻辑非常简单可控时考虑。- 临时文件 +
include
:将匿名类定义写入临时文件再包含进来。避免了eval
的安全问题,但引入了文件 I/O 开销,需要处理好临时文件的创建和清理。相对更安全,但实现稍复杂。
选择哪种方法取决于你的具体场景、对安全性的要求、性能敏感度以及愿意付出的实现复杂度。对于测试场景,如果能保证输入是可控的内部数据,eval
可能是个便捷的选择。如果环境要求更严格或涉及外部输入,用临时文件会是更稳妥的做法。