PHP怎么生成物料清单?从递归算法到实战封装的完整指南
📚 目录导读
- 为什么用PHP生成物料清单(BOM)?
- 物料清单的数据结构设计(数据库表与字段)
- 核心算法:递归遍历与层级计算(附代码)
- 循环引用与死循环的防御策略
- 性能优化:预加载与缓存机制
- 实战封装:一个可复用的BOMGenerator类
- 常见问题解答(FAQ)
- SEO关键词策略与总结建议
为什么用PHP生成物料清单(BOM)?
在制造业、电商ERP或进销存系统中,物料清单(Bill of Materials)是描述产品与子件、原材料层级关系的核心数据模型,尽管Python或Java常被推荐,但PHP凭借其部署简单、生态成熟(Laravel/ThinkPHP),依然是中小型企业内部系统的首选,通过PHP生成BOM,不仅能动态计算成本、库存,还能为生产计划提供实时依据。

物料清单的数据结构设计(数据库表与字段)
为了高效生成BOM,至少需要两张表:
-- 产品/物料主档表 CREATE TABLE `item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `sku` VARCHAR(32) UNIQUE, `name` VARCHAR(100), `is_assembly` TINYINT(1) DEFAULT 0 -- 1为父件,0为子件 ); -- 物料关系表(核心BOM表) CREATE TABLE `bom_relation` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `parent_item_id` INT NOT NULL, -- 父件ID `child_item_id` INT NOT NULL, -- 子件ID `quantity` DECIMAL(10,2) NOT NULL DEFAULT 1, -- 用量 `scrap_rate` DECIMAL(5,2) DEFAULT 0, -- 损耗率% UNIQUE KEY `uk_parent_child` (`parent_item_id`,`child_item_id`) );
设计关键点:通过is_assembly判断是否继续向下展开,避免全表扫描。
核心算法:递归遍历与层级计算(附代码)
生成BOM的本质是多叉树遍历,以下是一个简化的递归实现:
<?php
class BomService {
private $relations = [];
// 预加载所有关系,避免重复查询(见性能优化)
public function loadAllRelations() {
$db = new PDO('...');
$stmt = $db->query("SELECT * FROM bom_relation");
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$this->relations[$row['parent_item_id']][] = $row;
}
}
public function buildBom($itemId, $qty = 1, &$result = [], $depth = 0) {
$children = $this->relations[$itemId] ?? [];
foreach ($children as $child) {
$needQty = $qty * $child['quantity'] * (1 + $child['scrap_rate']/100);
$name = $this->getItemName($child['child_item_id']); // 从缓存获取
$result[] = [
'item_id' => $child['child_item_id'],
'name' => $name,
'quantity' => round($needQty, 2),
'depth' => $depth,
'is_assembly' => $this->isAssembly($child['child_item_id']) // 判断是否继续
];
// 若为组装件,递归
if ($this->isAssembly($child['child_item_id'])) {
$this->buildBom($child['child_item_id'], $needQty, $result, $depth+1);
}
}
return $result;
}
}
?>
输出示例:
- 深度0:
产品A 数量1 - 深度1:
子件B 数量2、子件C 数量3 - 深度2:
原材料D 数量4(B的下一层)
循环引用与死循环的防御策略
生产环境中,物料关系可能出现A→B→A的循环错误,使用路径追踪数组:
function buildBomSafe($itemId, $qty, &$result, &$path, $depth) {
if (in_array($itemId, $path)) {
throw new Exception("循环引用检测到: ".implode('->', $path)."->".$itemId);
}
$path[] = $itemId;
// ... 递归逻辑
array_pop($path);
}
数据库层应建立触发器禁止插入导致循环的关系(需使用WITH RECURSIVE提前校验)。
性能优化:预加载与缓存机制
- 一次性加载:如上述代码所示,使用
loadAllRelations()把全部关系放入内存,替代递归中N次查询。 - Redis缓存:将构建好的BOM树序列化存入Redis,键名
bom_成品ID_版本号,生产变更时主动失效。 - 分页/懒加载:如果BOM层级极深(如航空业),改用迭代式栈遍历,避免PHP栈内存溢出。
实战封装:一个可复用的BOMGenerator类
<?php
class BOMGenerator {
private $itemCache = []; // 产品信息缓存
private $relationMap = [];
public function __construct($pdo) {
// 预加载所有item和relation
$this->loadItems($pdo);
$this->loadRelations($pdo);
}
// 对外统一入口
public function generate($finishedGoodsId, $quantity) {
$path = [];
$result = [];
$this->recursiveBom($finishedGoodsId, $quantity, $result, $path, 0);
return ['tree' => $result, 'total_cost' => $this->calcTotalCost($result)];
}
private function recursiveBom($id, $qty, &$result, &$path, $depth) {
if (isset($path[$id])) { die("循环依赖"); }
$path[$id] = true;
foreach ($this->relationMap[$id] ?? [] as $rel) {
$childQty = $qty * $rel['quantity'];
// 计算净需求量并保存
$result[] = ['id'=>$rel['child_id'], ...];
if ($this->isAssembly($rel['child_id'])) {
$this->recursiveBom($rel['child_id'], $childQty, $result, $path, $depth+1);
}
}
unset($path[$id]);
}
}
?>
常见问题解答(FAQ)
Q1:递归次数太多导致PHP报错(超过100层)怎么办?
A1:改用循环+栈数据结构,因为PHP默认递归上限是100-256层(取决于xdebug.max_nesting_level),将递归转换为while循环并手动维护栈数组,可无限扩展。
Q2:BOM中如果同一个子件出现在不同层级,如何汇总数量?
A2:在递归时以item_id为键,累加quantity到哈希表中,最后输出时判断is_assembly标记是否合并。
Q3:如何防止修改BOM时误删父件?
A3:数据库外键设为ON DELETE RESTRICT,并在业务逻辑层检查是否有子件引用。
SEO关键词策略与总结建议
本文核心关键词:PHP BOM生成、物料清单递归算法、Laravel BOM模块、多级BOM展开。
内链建议:在文中自然嵌入“PHP多层级数组处理”、“MySQL递归查询”相关链接(例如指向站内教程)。
:本文通过完整的表结构设计、递归防死循环代码、全局缓存优化,给出了一个可直接落地的PHP BOM生成方案,重点在于数据预加载而非每条查询,以及路径哨兵机制,建议读者根据自身框架(Laravel/Lumen)封装成服务提供者,并添加单元测试。
扩展阅读:如果你需要输出带“最低层级码”(LLC)的BOM用于MRP计算,请在递归返回时增加后序遍历逻辑,将子件的最小层级向上传递,这是一个高阶优化点,欢迎在评论区留言讨论实现细节。