本文目录导读:

在 PHP 开发中,“决策记录”通常指架构决策记录(ADR,Architecture Decision Record),或者是业务逻辑中的决策日志,我会从这两个维度分别给出落地方案。
架构决策记录(团队协作 / 文档)
这是给开发者看的,记录“为什么”要这么写代码,推荐使用轻量级的 Markdown 文件,放进代码仓库。
标准目录结构(ADR 模式)
在项目根目录创建 docs/adr/ 文件夹,每个决策一个文件,用递增数字命名:
docs/adr/
├── 0001-use-vite-as-build-tool.md
├── 0002-adopt-repository-pattern.md
└── 0003-use-redis-for-session-storage.md
标准模板(避免写废话)
# 3. 使用 Redis 替代文件存储 Session - 日期: 2025-05-20 - 状态: 已接受 (Accepted) // 可选:提议/已接受/已废弃 - 决策者: 张三、李四 ## 背景 当前使用 PHP 原生 `$_SESSION`,在负载均衡多机部署时,会话数据不同步,导致用户频繁掉线。 ## 决策 采用 Redis 作为会话存储驱动(使用 `phpredis` 扩展)。 ## 后果 - 正面: 支持横向扩展,会话读取速度快。 - 负面: 运维需额外维护 Redis 高可用;若 Redis 宕机,所有用户会话失效。 ## 替代方案 - 使用数据库存储(太慢)。 - 使用 JWT 无状态 Token(改动前端较大,暂缓)。
实施要点
- 写“为什么”:记录当时考量的技术约束和业务场景,别记“用了什么函数”。
- 状态流转:如果方案被推翻,不要删除文件,将原文件状态改为“已废弃”,并新建 0004 文件说明新方案,保留历史脉络。
业务决策日志(运行时追踪)
这是给业务方或排查问题时看的,记录用户或系统在运行时的关键操作选择。
建议不要直接 error_log() 乱写,应该结构化地记录到日志表或文件。
核心类封装(决策记录器) 使用单例模式注册到容器,方便在任何地方调用。
<?php
namespace App\Services;
use Psr\Log\LoggerInterface;
class DecisionLogger
{
private array $context = [];
public function __construct(private LoggerInterface $logger) {}
/**
* 开始记录一次业务决策过程
*/
public function start(string $processName, string $userId, array $initData = []): void
{
$this->context = [
'process' => $processName,
'user_id' => $userId,
'start_time' => microtime(true),
'steps' => [],
];
$this->log('决策开始', $initData);
}
/**
* 记录每一步判断
*/
public function step(string $decisionPoint, string $result, array $reason = []): void
{
$this->context['steps'][] = [
'point' => $decisionPoint,
'result' => $result,
'reason' => $reason,
];
// 写入系统日志或异步队列
$this->logger->info("决策节点: {$decisionPoint} -> {$result}", $reason);
}
/**
* 结束记录并归档
*/
public function finish(string $finalResult): void
{
$elapsed = microtime(true) - $this->context['start_time'];
// 这里可以推送到 ElasticSearch 或存入数据库表
$this->logger->notice("决策完成: {$finalResult}", [
'process' => $this->context['process'],
'steps' => $this->context['steps'],
'duration_ms' => round($elapsed * 1000, 2),
]);
}
}
在业务逻辑中的使用(优酷案例:风控/推荐/优惠券)
假设你在做订单折扣计算:
public function calculateDiscount(Order $order, DecisionLogger $logger): float
{
$logger->start('order_discount', $order->user_id, ['order' => $order->id]);
// 如过用户是 VIP
if ($order->user->isVip()) {
$logger->step('vip_check', '通过', ['vip_level' => $order->user->vip_level]);
$rate = 0.9;
} else {
$logger->step('vip_check', '未通过', ['err' => '用户非VIP']);
$rate = 1.0;
}
// 如果金额大于 1000 使用大额券
if ($order->total > 1000) {
$logger->step('threshold_check', '命中大额券', ['total' => $order->total]);
$rate -= 0.05;
} else {
$logger->step('threshold_check', '未达门槛');
}
$final = $order->total * max(0.1, $rate);
$logger->finish("最终折扣: {$final}");
return $final;
}
存储方案选择(重要)
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 低并发、内部系统 | 写入 MySQL 日志表 | 好查好看,配合后台管理界面过滤用户ID |
| 高并发、核心链路 | 写入 InfluxDB/TDengine 时序数据库或 Redis Stream | 异步批量落盘,不阻塞主流程 |
| 需要全文检索 | 直接输出 JSON 到 ELK (ElasticSearch) | 后续可按关键词搜“为什么没送券” |
技术选型决策(代码注释里的决策)
在代码里遇到关键的 IF/ELSE 或设计模式选择时,除了魔法数字,可以加一个标准化的注释块:
/**
* 决策:为什么不直接用 `==` 判断?
* 原因:`0 == "foo"` 在 PHP 8.0 之前的版本会返回 true,这里必须严格比较类型。
* 成本:牺牲少量代码简洁性,换取类型安全。
*/
if ($inputValue === 0) { // 严格的 Int 比较
// ...
}
总结给 PHP 开发者的建议
- 团队决策:用 ADR 放进
docs/adr/,GIT 提交时一起走 PR 审核,记录演进史。 - 运行决策:封装一个
DecisionLogger类,关键节点埋点,日志格式统一为point -> result,方便排查“为什么走到了这个分支”。 - 核心原则:决策记录是给未来的自己看的,犯过的错不重复,想过的理由不重想。