电商项目里购物车是核心模块,但很多开发者在实现时只做了简单的商品累加,等到产品经理提出“结算页要按类别分组展示,每个类别显示商品数量和小计金额”这类需求时才发现手里的数据结构不够用。其实用PHP的数组函数就能优雅地解决这个问题,不需要写多层嵌套循环,也不用依赖数据库的GROUP BY查询。这篇文章从数据结构出发,给出完整的分组统计实现,并覆盖各种容易踩坑的边界情况。

一、先明确购物车的数据结构
不管是存在session里还是redis里,购物车最终展开后通常是这样一个二维数组:每个元素对应一件商品,包含商品ID、名称、所属分类、单价和购买数量。为了方便演示,我们定义一个结构清晰的测试数据:
<?php
$cart = [
[
'id' => 101,
'name' => '机械键盘',
'category' => '数码配件',
'price' => 299.00,
'quantity' => 2,
],
[
'id' => 205,
'name' => '无线鼠标',
'category' => '数码配件',
'price' => 129.00,
'quantity' => 3,
],
[
'id' => 302,
'name' => '纯棉T恤',
'category' => '服装',
'price' => 89.00,
'quantity' => 1,
],
[
'id' => 408,
'name' => '牛仔裤',
'category' => '服装',
'price' => 159.00,
'quantity' => 2,
],
[
'id' => 501,
'name' => '坚果礼盒',
'category' => '食品',
'price' => 68.00,
'quantity' => 5,
],
];这个结构的关键在于每个商品元素都有category字段,它是后面分组的依据。如果真实项目中商品的分类信息存在另一张表里,可以先用商品ID批量查出分类并合并到数组中,合并完成后再做分组,逻辑是完全一样的。
还有一种常见的存储格式是以商品ID为键的关联数组,比如$cart[101]直接对应机械键盘那条记录。这种格式做分组时用array_values转成索引数组即可,或者直接遍历也行,不影响后续处理。
二、用array_reduce一次性完成分组统计
PHP提供的array_reduce函数非常适合这种“把数组归纳成一个结果”的场景。它接收三个参数:要处理的数组、一个回调函数和初始值。回调函数在每次迭代时接收上一次的累积结果和当前元素,我们把分类作为键逐步累积数量和金额,代码如下:
<?php
$result = array_reduce($cart, function ($carry, $item) {
$category = $item['category'];
if (!isset($carry[$category])) {
$carry[$category] = [
'quantity' => 0,
'subtotal' => 0.0,
'product_num' => 0,
];
}
$carry[$category]['quantity'] += $item['quantity'];
$carry[$category]['subtotal'] += $item['price'] * $item['quantity'];
$carry[$category]['product_num'] += 1;
return $carry;
}, []);
print_r($result);执行后得到的$result是一个以分类名为键的数组,每个分类下记录了三样东西:quantity是该分类的商品总件数,subtotal是该分类的金额小计,product_num是该分类包含几种商品。以演示数据为例,数码配件分类的quantity是5,subtotal是1023元,正好是299乘2加上129乘3的结果。
这种写法的优点是逻辑集中在一个闭包里,初始结构清晰,没有中间临时变量,整个统计过程一气呵成。需要注意的是初始值必须传空数组,漏传第三个参数的话array_reduce会把第一个元素作为初始值直接返回,导致第一条数据没有参与统计。
三、foreach版本对照实现
如果团队成员对闭包写法不熟悉,或者统计逻辑后续要扩展得比较复杂,用传统的foreach循环同样可以完成,而且更容易打断点调试:
<?php
$result = [];
foreach ($cart as $item) {
$category = $item['category'];
if (!isset($result[$category])) {
$result[$category] = [
'quantity' => 0,
'subtotal' => 0.0,
'product_num' => 0,
];
}
$result[$category]['quantity'] += $item['quantity'];
$result[$category]['subtotal'] += $item['price'] * $item['quantity'];
$result[$category]['product_num'] += 1;
}
// 按数量降序排列,方便结算页优先展示件数多的分类
uasort($result, function ($a, $b) {
return $b['quantity'] <=> $a['quantity'];
});
print_r($result);foreach版本和array_reduce版本的核心判断完全相同,区别只在于书写形式。从可读性角度说,foreach对新手更友好;从函数式编程的角度说,array_reduce体现了“归约”思想,链式调用时更简洁。两者性能几乎没差别,因为无论哪种写法都只遍历一次数组,时间复杂度都是O(n)。
顺带一提,分组完成后的排序用uasort比uksort更常用,因为结算页一般按数量或金额排序而不是按分类名排序。如果想按分类名排序,直接ksort($result)即可,默认就是升序的字母或拼音序。
四、边界情况与实用增强
真实数据不会像演示数据那么规整,有几种情况需要提前防御。第一种是商品缺少category字段,直接取值会产生notice级别错误,统计结果里也会丢失这条数据。可以在取值前做个兜底:
<?php $category = $item['category'] ?? '未分类'; $quantity = max(1, intval($item['quantity'] ?? 1)); $price = floatval($item['price'] ?? 0);
第二种情况是分类信息需要动态关联。假设购物车里只存了category_id,我们可以先定义分类映射表,统计时把ID翻译成名称,这样输出结果直接可用于页面渲染:
<?php
$categoryMap = [
1 => '数码配件',
2 => '服装',
3 => '食品',
];
$result = [];
foreach ($cart as $item) {
$catId = $item['category_id'] ?? 0;
$category = $categoryMap[$catId] ?? '未分类';
// 后续统计逻辑与前面相同
$result[$category]['quantity'] = ($result[$category]['quantity'] ?? 0) + $item['quantity'];
}
// 输出整体合计:总件数与总金额
$totalQuantity = array_sum(array_column($result, 'quantity'));
$totalAmount = array_sum(array_column($result, 'subtotal'));
echo "共 {$totalQuantity} 件商品,合计 {$totalAmount} 元";第三种增强是金额格式化。金额计算时建议保留浮点运算但用round($value, 2)控制精度,展示时再用number_format加上千位分隔符。如果项目对金额精度要求严格,比如涉及分账或对账,应该改用整数分存储(price乘100),统计完成后再除回来,彻底避免浮点误差。
最后补充一点架构层面的建议:如果购物车数据量特别大(上千个SKU)且分类统计需要频繁刷新,可以考虑把统计逻辑封装成独立的类方法,在购物车增删改时增量维护分组结果,而不是每次渲染页面都全量重算。当然对于绝大多数站点来说,全量遍历一次的开销可以忽略不计,简单直接的全量统计反而是更合理的选择。
PHP购物车数组分组array_reduce修改时间:2026-09-15 07:12:33