你有没有遇到过这种情况?打开一个网站,商品分类陈列得整整齐齐,点击一级分类,下级菜单自动展开,再点击一下,商品列表瞬间切换。这种体验很流畅,但如果你自己动手写过,就知道背后其实藏着不少门道。今天这篇文章,咱们就来聊聊PHP商品分类显示代码到底是个啥,怎么一步步写出来,以及日常开发中那些容易踩的坑。
先说说最基础的问题。商品分类显示,说白了就是把数据库里存的一大堆分类数据,按照上下级关系,在网页上展示出来。比如服装下面有男装、女装,男装下面又有T恤、衬衫。这种层级结构,在数据库设计阶段一般用“邻接表”模型来实现,就是一张表里放一个`parent_id`字段,指向上一级分类的ID。
举个例子,你打开数据库,会看到一张类似这样的表:
| id | name | parent_id | sort_order |
|---|---|---|---|
| 1 | 服装 | 0 | 1 |
| 2 | 男装 | 1 | 2 |
| 3 | 女装 | 1 | 3 |
| 4 | T恤 | 2 | 1 |
| 5 | 衬衫 | 2 | 2 |
parent_id为0表示顶级分类,没有上级。这个设计但写代码的时候,你就要面对一个核心问题:怎么把这种扁平的数据,变成有层次感的树形结构?
常见的做法是写个递归函数。不知道你有没有写过递归,反正我第一次写的时候,感觉脑子有点绕。核心思路是:先查所有顶级分类,然后对每个顶级分类,再查它的子分类,子分类再查子分类,一层层往下找。比如这样:
```php
function getTree($parentId = 0) {
$sql = " FROM category WHERE parent_id = $parentId ORDER BY sort_order ASC" // 执行查询,得到结果集
$rows = $db->query($sql);
$tree = [];
foreach ($rows as $row) {
$row['children'] = getTree($row['id']);
$tree[] = $row;
}
return $tree;
}
```
这段代码逻辑上没问题,但性能是个大坑。如果分类层级很深,或者分类数量很多,每次递归都会执行一次数据库查询。假设你有100个分类,平均每个查一次,那就是100次查询,页面加载速度直接崩掉。你可能会说,用缓存啊。对,但缓存之前,我们得先把数据取出来。
所以很多老手会换一种思路:一次性取出所有分类数据,然后在内存里构建树。这样只用一次数据库查询,效率高得多。关键代码大概是这样:
```php
$sql = " FROM category ORDER BY sort_order ASC" = $db->query($sql)->fetchAll();
$tree = [];
$map = [];
foreach ($rows as $row) {
$map[$row['id']] = $row;
}
foreach ($map as $id => &$item) {
if ($item['parent_id'] == 0) {
$tree[] = &$item;
} else {
$map[$item['parent_id']]['children'][] = &$item;
}
}
```
注意这个引用传递,是核心技巧。通过`$map`数组,按ID索引,然后通过引用把子分类挂到父分类的`children`数组里。这样即便数据不是按顺序来的,也能正确构建树。
数据拿到手之后,接下来就是显示了。通常的做法是用无限级分类下拉菜单或者树形导航菜单。下拉菜单比较简单,用递归遍历树,每层缩进几个空格或者加几个` `,就能形成层级视觉。比如:
```php
function renderSelect($tree, $level = 0) {
$html = '';
foreach ($tree as $item) {
$prefix = str_repeat(' ', $level);
$html .= "" .= renderSelect($item['children'], $level + 1);
}
}
return $html;
}
```
树形导航菜单稍微复杂一点,要考虑到CSS和JS的配合。比如你希望点击一级分类,展开下面所有子分类,这就要用到前端联动。后端只需要输出一个嵌套的`
- `结构,前端用JS控制显示隐藏。
还有一种场景是商品列表页的分类筛选。用户点击某个分类,页面只显示该分类及其子分类下的商品。你要做的就不是简单显示树了,而是根据选中的分类ID,递归获取所有下级分类的ID,然后去商品表里查询。这个函数也很常见:
```php
function getChildrenIds($parentId) {
static $ids = [];
$sql = "id FROM category WHERE parent_id = $parentId" $children = $db->query($sql)->fetchAll();
foreach ($children as $child) {
$ids[] = $child['id'];
getChildrenIds($child['id']);
}
return $ids;
}
```
注意这里用了static变量,但多线程或多次调用时容易出问题,更稳妥的做法是传入一个引用参数。
说到实际开发,你可能会遇到一个很常见的需求:分类路径展示。比如用户在浏览“T恤”这个分类时,页面上方显示“服装 > 男装 > T恤”。这个功能实现起来也不复杂,从当前分类ID开始,往上找父分类,一直找到最顶层。类似这样:
```php
function getCategoryPath($id) {
$path = [];
while ($id > 0) {
$row = $db->query("*FROM category WHERE id = $id" array_unshift($path, $row['name']);
$id = $row['parent_id'];
}
return implode(' > ', $path);
}
```
每次循环都要查一次数据库,性能依然不好。所以很多时候,我们会在表里额外加一个`path`字段,存形如“1,2,4”这样的路径ID,或者直接存“服装,男装,T恤”。用空间换时间,在分类保存时就把路径算好存进去,查询时直接取出来,省心省力。
还有一个容易被忽视的点是排序。很多商品分类都有排序需求,比如想让“男装”排在“女装”前面。一般做法是加一个`sort_order`字段,数值越小越靠前。在构建树的时候,先按这个字段排序,再处理子分类。如果数据量特别大,排序也是个性能瓶颈,可以在数据库里建索引。
咱们来聊聊缓存。商品分类通常不会频繁变化,但每次加载页面都要查数据库、构建树,太浪费了。常见的做法是:把构建好的树结构序列化后存到文件或Redis里,设置一个过期时间,比如10分钟。或者更简单,用静态变量在请求周期内缓存。
```php
static $categoryTree = null;
if ($categoryTree === null) {
$categoryTree = buildTree();
}
```
这个技巧在页面多次调用分类函数时特别好用,避免重复查询。
写到这里,你可能会觉得,PHP商品分类显示代码不就是一个递归加循环的事吗?对,但写得好不好,差距就在细节里。比如sql注入,你直接用变量拼接查询,是不安全的,要用`prepare`绑定参数。比如编码问题,分类名用`utf-8`,数据库连接也要设置`set names utf8`。再比如大数据量,分类过万时,递归可能撑爆栈,得考虑用迭代栈替代递归。
我刚开始搞这个的时候,也踩过不少坑。有一次,一个电商后台的分类管理页面,点击删除父分类,所有子分类也一起删了,但忘记判断子分类下是否有商品,结果导致一堆商品变成无分类商品,数据直接乱了。后来才发现,删除分类前,一定要先检查是否有子分类或有商品。
单独写一个`checkCategoryDelete`函数,是很有必要的。
```php
function canDeleteCategory($id) {
$hasChildren = $db->query(" COUNT(*) FROM category WHERE parent_id = $id" $hasProducts = $db->query("(*) FROM product WHERE category_id = $id" $hasChildren == 0 && $hasProducts == 0;
}
```
这个函数返回true时才能删除,否则提示用户先处理下级分类或商品。
PHP商品分类显示代码,核心就是数据模型设计、树结构构建、递归遍历显示、以及性能优化。黑盒里没有太多花哨的东西,但每一步都考验你对数据结构、数据库和PHP语言本身的理解。如果你能把这些细节处理好,写出来的代码不仅快,而且好维护。
希望这篇文章能帮你理清思路,下次再遇到“商品分类显示”这个需求,你就能直接上手,写出既高效又健壮的代码。如果你遇到什么特别的坑,记得回来留言,咱们一起讨论。


