PHP项目中如何处理无限级分类?

wen PHP项目 3

本文目录导读:

PHP项目中如何处理无限级分类?

  1. 方法一:邻接表 (Adjacency List) - 最常用
  2. 方法二:递归获取所有子分类 ID (用于权限或展示)
  3. 方法三:路径枚举 (Materialized Path)
  4. 方法四:嵌套集 (Nested Set) - 高性能读
  5. 实战建议
  6. 最后:一个完整的树形下拉选择框生成函数

在 PHP 项目中处理无限级分类,核心在于数据表设计查询/遍历算法,常用的方法主要有三种:邻接表路径枚举嵌套集

对于大多数中小型 PHP 项目(如 CMS、电商、论坛),邻接表 + 递归 是最常用且直观的方案;对于大量读取、不常修改的场景,嵌套集 效率更高。

下面分别说明这些方法,并给出 PHP 代码示例。


邻接表 (Adjacency List) - 最常用

这是最经典、理解成本最低的方法,数据表中每个节点记录其父节点的 ID。

数据表结构 (MySQL)

CREATE TABLE `categories` (
    `id` INT UNSIGNED AUTO_INCREMENT,
    `name` VARCHAR(255) NOT NULL,
    `parent_id` INT UNSIGNED DEFAULT 0, -- 父分类ID,顶级为0
    `sort` INT UNSIGNED DEFAULT 0,      -- 排序字段
    PRIMARY KEY (`id`),
    INDEX `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

PHP 获取树形结构 (递归)

最标准的做法是:一次查询所有数据,然后在内存中递归构建树。

class CategoryTree
{
    /**
     * 获取所有分类并构建为树形结构
     * @param array $categories 从数据库查出的所有分类(索引数组)
     * @param int $parentId 父级ID
     * @return array 树形结构
     */
    public static function buildTree(array $categories, int $parentId = 0): array
    {
        $tree = [];
        foreach ($categories as $category) {
            if ((int)$category['parent_id'] === $parentId) {
                // 递归查找子节点
                $children = self::buildTree($categories, (int)$category['id']);
                if ($children) {
                    $category['children'] = $children;
                }
                $tree[] = $category;
            }
        }
        return $tree;
    }
    /**
     * 获取某个分类的完整路径(用于面包屑导航)
     * @param array $categories 所有分类
     * @param int $id 目标分类ID
     * @return array 从顶级到自身的路径
     */
    public static function getPath(array $categories, int $id): array
    {
        $path = [];
        // 建立 ID => 数据的映射,避免循环查找
        $map = [];
        foreach ($categories as $cat) {
            $map[$cat['id']] = $cat;
        }
        // 向上追溯
        $currentId = $id;
        while (isset($map[$currentId])) {
            array_unshift($path, $map[$currentId]); // 插入到数组头部
            $currentId = (int)$map[$currentId]['parent_id'];
        }
        return $path;
    }
}
// 使用示例(在控制器中)
// $categories = DB::table('categories')->orderBy('sort')->get()->toArray();
// $tree = CategoryTree::buildTree($categories);

优点:

  • 表结构简单,增删改分类非常方便(只需要改 parent_id)。
  • 查询某个分类的子孙(递归获取)逻辑清晰。

缺点:

  • 如果层级很深(如>5级),递归遍历所有节点时 PHP 内存消耗较大。
  • 查询某个节点的“所有子节点”或“所有父级”需要多次数据库查询(或一次查出所有在 PHP 中处理)。

递归获取所有子分类 ID (用于权限或展示)

这是邻接表中的常见需求:给定一个分类ID,获取它及其所有子分类的ID列表。

/**
 * 获取一个分类及其所有子分类的ID列表
 * @param array $categories 所有分类
 * @param int $parentId 起始分类ID
 * @return array ID数组
 */
function getChildrenIds(array $categories, int $parentId): array
{
    $ids = [$parentId];
    foreach ($categories as $cat) {
        if ((int)$cat['parent_id'] === $parentId) {
            // 递归合并子分类的ID
            $ids = array_merge($ids, getChildrenIds($categories, (int)$cat['id']));
        }
    }
    return $ids;
}
// 使用示例:查询分类ID=5 下面所有子分类
// $allIds = getChildrenIds($allCategories, 5);
// 然后可以使用 whereIn 查询这些分类下的文章

路径枚举 (Materialized Path)

在表中增加一个 path 字段,存储从根到当前节点的路径(如 0,1,5,81-5-8)。

表结构

CREATE TABLE `categories` (
    `id` INT UNSIGNED AUTO_INCREMENT,
    `name` VARCHAR(255) NOT NULL,
    `path` VARCHAR(500) NOT NULL DEFAULT '0', -- 路径,如 '0,1,2'
    `parent_id` INT UNSIGNED DEFAULT 0,
    PRIMARY KEY (`id`),
    INDEX `idx_path` (`path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

PHP 插入与查询

插入时:

// 假设新增分类属于父ID=5
$parentCategory = DB::table('categories')->find(5);
$newPath = $parentCategory->path . ',' . $parentCategory->id; // 变成 '0,5'
DB::table('categories')->insert([
    'name' => '新分类',
    'path' => $newPath,
    'parent_id' => 5
]);

查询某个节点的所有子节点:

// 查询ID=5的所有后代
$pathPrefix = DB::table('categories')->where('id', 5)->value('path') . ',' . 5; // '0,5'
// 这里假设path格式用逗号分隔,且逗号后没有空格
$children = DB::table('categories')
    ->where('path', 'LIKE', $pathPrefix . '%')
    ->orWhere('id', 5) // 包含自身
    ->get();

优点:

  • 查询某个节点的所有子孙非常快(单次 LIKE 查询)。
  • 查询某个节点的祖先(通过解析 path 字段)也很快。

缺点:

  • 移动分类时需要更新所有子孙的 path 字段(较麻烦)。
  • 如果层级很深,path 字段可能变得很长。

嵌套集 (Nested Set) - 高性能读

适合读非常频繁、写很少的场景(如商品分类树),每个节点存储 lftrgt 两个值,形成一个嵌套区间。

表结构

CREATE TABLE `categories` (
    `id` INT UNSIGNED AUTO_INCREMENT,
    `name` VARCHAR(255) NOT NULL,
    `lft` INT UNSIGNED NOT NULL,
    `rgt` INT UNSIGNED NOT NULL,
    PRIMARY KEY (`id`),
    INDEX `idx_lft_rgt` (`lft`, `rgt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键查询

查询某个节点的所有子孙:

SELECT * FROM categories WHERE lft BETWEEN ? AND ? ORDER BY lft;

为该节点的 lftrgt

查询某个节点的所有祖先:

SELECT * FROM categories WHERE lft < ? AND rgt > ? ORDER BY lft;

PHP 插入示例 (较复杂)

插入一个节点需要重新计算左右值,通常需要锁表或事务。

// 在父节点(lft=2, rgt=7)下新增一个子节点
$rightOfParent = 7;
DB::statement("UPDATE categories SET lft = lft + 2 WHERE lft > ?", [$rightOfParent]);
DB::statement("UPDATE categories SET rgt = rgt + 2 WHERE rgt >= ?", [$rightOfParent]);
DB::table('categories')->insert([
    'name' => '新节点',
    'lft' => $rightOfParent,
    'rgt' => $rightOfParent + 1
]);

优点:

  • 查询子树或祖先的速度极快(单次SQL,无需递归)。
  • 删除非叶子节点不会破坏树的结构。

缺点:

  • 增加、删除、移动节点需要更新大量记录的 lft/rgt,写操作性能较差。
  • 实现复杂,容易出错。

实战建议

场景 推荐方案
一般后台管理、CRUD频繁(文章分类) 邻接表 + 一次加载+递归
层级很深(>10级),查询子树频繁 路径枚举
极端读多写少,性能要求极高 嵌套集
数据量极小(几百条以内) 邻接表足够,不需要优化

一个折中方案: 使用邻接表存储数据,但在查询时一次性加载所有分类到内存,然后使用 PHP 递归构建树(比多次查询数据库快得多)。

对于企业级项目,也可以考虑使用 laravel-nestedset (Laravel 包) 或 doctrine-extensionsTree 行为。


一个完整的树形下拉选择框生成函数

/**
 * 生成用于 <select> 的下拉选项(带缩进)
 * @param array $tree 树形结构(buildTree的返回值)
 * @param int $level 层级(用于控制缩进)
 * @return array [ ['id'=>1, 'name'=>'---分类名'] ]
 */
function flattenTree(array $tree, int $level = 0): array
{
    $options = [];
    $prefix = str_repeat('—', $level);
    foreach ($tree as $node) {
        $options[] = [
            'id' => $node['id'],
            'name' => ($level > 0 ? $prefix . ' ' : '') . $node['name']
        ];
        if (isset($node['children'])) {
            $options = array_merge($options, flattenTree($node['children'], $level + 1));
        }
    }
    return $options;
}
// 使用
// $tree = buildTree($allCategories);
// $options = flattenTree($tree);
// 然后渲染为 <option value="...">...</option>

是 PHP 处理无限级分类最常见的几种方案,推荐从邻接表 + 一次加载开始,绝大多数场景都足够用了,如果后期遇到性能瓶颈,再重构为路径枚举或嵌套集。

抱歉,评论功能暂时关闭!