本文目录导读:

在 PHP 中,“副作用”通常指函数或方法在执行过程中,除了返回一个值(如果有)之外,还对系统状态产生了可观察的影响。
对一个 PHP 程序进行“副作用分析”,核心目的是识别代码中哪些操作是“不纯”的(即影响外部状态),从而帮助开发者进行调试、重构、单元测试、函数式编程以及安全审计。
下面我会从以下几个方面详细讲解 PHP 副作用分析:
什么是 PHP 中的副作用?
如果一段代码执行后,改变了程序外部环境的状态,就产生了副作用,常见的 PHP 副作用包括:
-
I/O 操作:
- 写入文件(
fwrite,file_put_contents) - 读取文件(
file_get_contents,虽然读不改变文件,但依赖外部状态) - 网络请求(
curl_exec,file_get_contents(url)) - 数据库读写(
mysqli_query,PDO::exec) - 发送
Header或Cookie(header(),setcookie()) - 到 stdout(
echo,print,var_dump)
- 写入文件(
-
修改全局变量:
- 使用
global关键字 - 修改
$_GET,$_POST,$_SESSION,$_SERVER,$GLOBALS
- 使用
-
修改静态变量:
- 类中的
static属性被修改 - 函数内的
static变量被修改
- 类中的
-
状态变更:
- 修改对象属性(尤其是对象被外部引用时)
- 使用
func_get_args()修改参数(已废弃的引用传参)
-
错误/异常处理:
trigger_error()触发警告- 抛出一个未被捕获的异常(改变程序控制流)
为什么需要分析副作用?
-
单元测试更难:有副作用的函数难以隔离测试,你需要 mock 文件系统、数据库或网络。
- 无副作用:
function add($a, $b) { return $a + $b; }容易测试。 - 有副作用:
function saveUser($name) { file_put_contents('users.txt', $name); }需要清理文件。
- 无副作用:
-
可预测性降低:相同的输入可能产生不同的输出(例如依赖
time()或随机数)。 -
调试更复杂:副作用可能导致“幽灵 bug”,例如某个函数偷偷修改了全局变量
$GLOBALS['config'],导致后续代码行为异常。 -
安全风险:某些副作用是安全攻击的目标(如文件写入、SQL 注入、输出未转义数据)。
如何进行 PHP 副作用分析?
手动静态分析(人工审查)
- 扫描关键字:搜索
echo,print,file_*,header,session_*,global,static,$_GET等。 - 检查函数签名:如果一个函数没有返回值 (
void),它几乎一定是为了副作用而存在的。 - 跟踪引用:检查变量是否被通过
&引用传递。
使用静态分析工具(推荐)
PHPStan 和 Psalm 是当前最强大的 PHP 静态分析工具,它们提供了“纯度分析”或“副作用分析”能力。
例子(使用 PHPStan):
PHPStan 的 @phpstan-pure 注解可以标记一个函数无副作用。
/**
* @phpstan-pure
*/
function calculateTax(float $price, float $rate): float
{
return $price * $rate; // 纯函数,无副作用
}
/**
* 没有 @phpstan-pure,PHPStan 会认为它可能有副作用
*/
function logAction(string $action): void
{
file_put_contents('log.txt', $action . "\n", FILE_APPEND);
}
使用 PHPStan 的 Bleeding Edge 规则(level 9 或 strictRules)可以检测:
- 在“纯”函数内部调用有副作用的函数。
- 在只允许纯表达式的上下文中使用了非纯代码。
其他工具:
- Phan:也支持副作用分析。
- Rector:可以自动重构代码,消除或隔离副作用。
动态分析(运行时)
使用 Xdebug 或 Tideways 进行代码跟踪:
// 开启跟踪
xdebug_start_trace('/tmp/trace.txt');
// 执行你的业务逻辑
doSomething();
// 停止跟踪
xdebug_stop_trace();
然后分析生成的 trace 文件,查看哪些函数调用了 I/O 函数 (fopen, curl_exec)、修改了 $_SESSION 等。
代码设计层面的隔离(最佳实践)
将副作用推到系统的“边界”,使核心业务逻辑保持纯函数。
// 不好的设计:副作用混在逻辑中
class OrderProcessor
{
public function process(array $orderData): void
{
// 逻辑
$total = $orderData['price'] * $orderData['qty'];
// 副作用!!!
$this->db->insert('orders', [...]);
$this->emailService->send('admin@example.com', 'New order');
echo "Order processed";
}
}
// 好的设计:核心逻辑无副作用,副作用在外层处理
class OrderCalculator
{
/**
* @phpstan-pure
*/
public function calculateTotal(array $orderData): float
{
return $orderData['price'] * $orderData['qty']; // 纯函数
}
}
// 在控制器或 Service 中调用有副作用的代码
class OrderController
{
public function store(Request $request): void
{
$calculator = new OrderCalculator();
$total = $calculator->calculateTotal($request->input());
// 副作用集中在这里
$this->db->insert('orders', ['total' => $total]);
$this->emailService->send(...);
}
}
常见陷阱与高级分析
-
魔术方法(Magic Methods):
__get,__set,__call可以隐藏副作用,静态分析工具通常无法完全解析它们的纯度。 -
匿名函数/闭包: 闭包可以捕获外部变量(通过
use),如果修改了捕获的变量(通过引用&$var),就是副作用。$count = 0; $func = function() use (&$count) { $count++; // 副作��! }; -
生成器(Generator): 生成器的
yield本身不一定是副作用,但如果它抛出了异常或调用了 I/O,就是副作用。 -
依赖注入容器: 从容器中获取服务(如
$container->get('db'))通常不被认为是副作用,但调用该服务的方法可能有副作用。
如何在实际项目中落地
| 分析方式 | 工具/方法 | 适用场景 |
|---|---|---|
| 人工审查 | 代码 Review,搜索关键字 | 小型项目、安全审计 |
| 静态分析 | PHPStan + @phpstan-pure |
大型项目,CI/CD 流程 |
| 动态分析 | Xdebug Trace | 调试难以定位的副作用 Bug |
| 架构原则 | 函数式核心 + 命令式边界 | 重构、新项目设计 |
实际建议:
- 在 CI 中配置 PHPStan(level 6 以上),并开启
reportUnmatchedIgnoredErrors。 - 使用
@phpstan-pure和@phpstan-immutable注解 标记你的纯函数和不可变类。 - 对数据库、文件、网络操作进行抽象(Repository Pattern),使其在单元测试中可以被 Mock。
通过系统的副作用分析,你的 PHP 代码将变得更加可测试、可预测、易维护。