PHP 中动态方法调用的风险
概述
在你的 PHP 应用中,有时可能会看到使用动态方法调用。这通常是指在运行时构造方法名称,然后在对象上调用。例如,$this->{'methodName'}() 可用于调用名为 methodName 的方法。
动态方法调用很有用,但也存在一些应该注意的风险。
本文中,我们将探讨在 PHP 中使用动态方法调用的风险,并提供一些可供考虑的替代方案。
什么是动态方法调用?
PHP 中的动态方法调用允许你使用变量或表达式在运行时确定方法名称,从而调用对象上的方法。
通常,你会使用类似 $this->methodName() 的方法来调用方法。但使用动态方法调用,你可以执行类似 $this->{'methodName'}() 或 $this->{$variable}() 的操作,其中 $variable 包含要调用的方法的名称。
可以想象,这种方法非常强大,因为它允许代码灵活。尤其是在运行时才知道要调用的方法的情况下(例如,你正在构建框架或库)。我认为这就是它们发挥作用的地方。但是,我认为在大多数应用代码中,通常存在更好、更安全的替代方法。
为了帮助我们理解这些危险,让我们看一个例子。假设我们正在构建一个类,用于处理从外部服务接收的 Webhook。 Webhook 负载包含一个事件字段,用于告诉我们事件的类型(例如“成功”、“失败”等)。我们希望根据此事件字段的值调用不同的方法。例如,如果事件为“成功”,则调用 handleSuccessWebhook 方法。如果事件为“失败”,则调用 handleFailureWebhook 方法。负载可能如下所示:
$payload = [
'event' => 'success',
'details' => [
// ...
],
];Webhook 处理程序类可能看起来像这样:
final readonly class WebhookHandler
{
public function handleWebhook(array $payload): void
{
$this->{'handle' . ucfirst($payload['event']) . 'Webhook'}($payload);
}
private function handleSuccessWebhook(array $payload): void
{
// Handle a "success" webhook here...
}
private function handleFailureWebhook(array $payload): void
{
// Handle a "failure" webhook here...
}
// Other webhook handling methods here...
}我们可以看到,这里有一个公共的 handleWebhook 方法,它接受有效负载作为参数。它从有效负载中提取 event 字段,然后根据该事件构造要调用的方法名。它使用 ucfirst 确保事件的首字母大写,并在其末尾附加“Webhook”。最后,它在 $this 上调用构造的方法名。
动态方法调用的风险
现在让我们探讨一下使用这种方法的一些危险。
IDE 的处理困境
我在处理动态方法调用时遇到的最大问题之一是,像 PhpStorm 这样的集成开发环境 (IDE) 很难理解它们的用法。由于方法名称是在运行时构建的,IDE 很难检测 handleSuccessWebhook 和 handleFailureWebhook 方法是否真的被使用。这可能会导致 IDE 将它们标记为未使用,从而产生误导。
过去,我曾试图删除 IDE 标记为未使用的方法,但后来才发现它们实际上是通过动态方法调用被使用的。这可能会导致应用出现错误和意外行为。值得庆幸的是,我在部署到生产环境之前及时发现了这些问题。但那次事故也算是惊险一击。
PhpStorm 无法理解动态方法调用的另一个缺点是,你无法充分利用 IDE 的重构工具。例如,如果你想在 PhpStorm 中重命名某个方法,它将无法找到所有引用(因为它们是动态的),并且不会自动重命名。如果你不小心,这可能会导致破坏性代码。
难以查找
我发现动态方法调用的另一个问题是,你无法轻松地在代码库中搜索它们的用法。
假设你想查找 handleSuccessWebhook 方法被调用的位置。于是你在 PhpStorm 中按下 CMD+SHIFT+F 打开全局搜索窗口,搜索“handleSuccessWebhook”。你不会找到任何结果(除了方法定义本身),因为该方法名称在代码的其他任何地方都没有明确提及。
这时,你必须问自己:“这个方法是否在任何地方使用?或者删除它是否安全?” 如果你有一个全面的测试套件,涵盖了该方法并确认它正在被使用,这个问题就会变得容易回答得多。但是,如果你的测试套件没有涵盖这个特定功能,那么你就必须手动检查代码,看看它是否在任何地方被使用。这可能非常耗时且容易出错,尤其是在较大的代码库中。
难以阅读
我个人觉得动态方法调用会让代码乍一看更难读。你觉得下面哪个代码乍一看更清晰?
// 动态方法调用:
$this->{'handle' . ucfirst($payload['event']) . 'Webhook'}($payload);
// 传统方式调用:
$this->handleSuccessWebhook($payload);我猜大多数人会觉得传统的方法调用更清晰。而动态方法调用则需要你费脑筋去解析字符串连接,才能搞清楚到底调用了什么方法。如果字符串连接比较复杂,或者你不知道 $event 的可能值是什么,那么这尤其具有挑战性。
替代方案
出于上述原因,我通常避免在代码中使用动态方法调用。相反,我更喜欢使用更明确的方法。
但这纯粹是我个人的偏好,并不是说动态方法调用本质上不好。所以,如果你正在阅读这篇文章并在自己的代码中使用它们,请不要认为我在侮辱你的代码。如果它适合你的用例,经过充分的测试,并且能提高你的效率,那么我完全支持它。
我只是更喜欢使用更明确的方法所带来的额外的信心和安全感。而且我认为它使代码更易于阅读,尤其是对于刚接触代码库的开发人员而言。
如果我要重构上面的 WebhookHandler 类,我可能会改用 match 表达式:
final readonly class WebhookHandler
{
public function handleWebhook(array $payload): void
{
match ($payload['event']) {
'success' => $this->handleSuccessWebhook($payload),
'failure' => $this->handleFailureWebhook($payload),
default => throw new Exception('No handler for the event: ' . $event)
};
}
private function handleSuccessWebhook(array $payload): void
{
// Handle a "success" webhook here...
}
private function handleFailureWebhook(array $payload): void
{
// Handle a "failure" webhook here...
}
// Other webhook handling methods here...
}在上述方法中,我们使用了一个“match 表达式”,它读取有效负载的 event 字段,然后根据其值调用相应的方法。如果事件无法识别,则会抛出异常。
通过这种方法,我们能够在代码中明确地写入方法名称。这意味着我们的 IDE 能够理解它们的用法,从而让我们能够充分利用其功能(例如重构工具以及了解将返回哪些类型)。这也意味着我们可以轻松地在代码库中搜索它们的用法。我还认为,这也使代码乍一看更容易阅读。
总结
希望本文能让您思考在 PHP 中使用动态方法调用的危险性。虽然它们在某些情况下很有用,但也存在一些应该注意的风险。