【问题标题】:PHP Nested JSON from flat JSON来自平面 JSON 的 PHP 嵌套 JSON
【发布时间】:2017-10-16 04:19:36
【问题描述】:

我有一个数据库查询,它为我提供了一些员工数据的输出。我想使用这些数据传递给生成组织结构图的插件。我正在下拉的 JSON 对象中有几个字段是:

FirstName
LastName
EmployeeID
ManagerEmployeeID
Manager Name

数据以平面 JSON 对象的形式返回,员工及其经理在层次结构中没有嵌套或关联。

由于我无法更改源数据(数据库查询)的输出,因此我试图找出一种嵌套数据的方法,以便 JSON 输出成为嵌套输出。

我的目标是获取这个数组并根据 ManagerID 和 EmployeeID 嵌套它,这样我就可以创建一个树状层次结构。

示例数据:

•   Tom Jones
   o    Alice Wong
   o    Tommy J.
•   Billy Bob
   o    Rik A.
     ♣  Bob Small
     ♣  Small Jones
   o    Eric C.

我的平面数据示例:

    {
        "FirstName": "Tom"
        "LastName": "Jones"
        "EmployeeID": "123"
        "ManagerEmployeeID": ""
        "Manager Name": ""
    },
    {
        "FirstName": "Alice"
        "LastName": "Wong"
        "EmployeeID": "456"
        "ManagerEmployeeID": "123"
        "Manager Name": "Tom Jones"
    },
    {
        "FirstName": "Tommy"
        "LastName": "J."
        "EmployeeID": "654"
        "ManagerEmployeeID": "123"
        "Manager Name": "Tom Jones"
    },
    {
        "FirstName": "Billy"
        "LastName": "Bob"
        "EmployeeID": "777"
        "ManagerEmployeeID": ""
        "Manager Name": ""
    },
    {
        "FirstName": "Rik"
        "LastName": "A."
        "EmployeeID": "622"
        "ManagerEmployeeID": "777"
        "Manager Name": "Billy Bob"
    },
    {
        "FirstName": "Bob"
        "LastName": "Small"
        "EmployeeID": "111"
        "ManagerEmployeeID": "622"
        "Manager Name": "Rik A."
    },
    {
        "FirstName": "Small"
        "LastName": "Jones"
        "EmployeeID": "098"
        "ManagerEmployeeID": "622"
        "Manager Name": "Rik A"
    },
    {
        "FirstName": "Eric"
        "LastName": "C."
        "EmployeeID": "222"
        "ManagerEmployeeID": "777"
        "Manager Name": "Billy Bob"
    }

所需输出示例:

[
  {
    "FirstName": "Tom",
    "LastName": "Jones",
    "EmployeeID": "123",
    "ManagerEmployeeID": "",
    "Manager Name": "",
    "employees": [
      {
        "FirstName": "Alice",
        "LastName": "Wong",
        "EmployeeID": "456",
        "ManagerEmployeeID": "123",
        "Manager Name": "Tom Jones"
      },
      {
        "FirstName": "Tommy",
        "LastName": "J.",
        "EmployeeID": "654",
        "ManagerEmployeeID": "123",
        "Manager Name": "Tom Jones"
      }
    ]
  },
  {
    "FirstName": "Billy",
    "LastName": "Bob",
    "EmployeeID": "777",
    "ManagerEmployeeID": "",
    "Manager Name": "",
    "employees": [
      {
        "FirstName": "Rik",
        "LastName": "A.",
        "EmployeeID": "622",
        "ManagerEmployeeID": "777",
        "Manager Name": "Billy Bob",
        "employees": [
          {
            "FirstName": "Bob",
            "LastName": "Small",
            "EmployeeID": "111",
            "ManagerEmployeeID": "622",
            "Manager Name": "Rik A."
          },
          {
            "FirstName": "Small",
            "LastName": "Jones",
            "EmployeeID": "098",
            "ManagerEmployeeID": "622",
            "Manager Name": "Rik A"
          }
        ]
      },
      {
        "FirstName": "Eric",
        "LastName": "C.",
        "EmployeeID": "222",
        "ManagerEmployeeID": "777",
        "Manager Name": "Billy Bob"
      }
    ]
  }
]

基本上,我正在尝试使用EmployeeIDManagerEmployeeID 作为两者之间的链接,从平面对象创建嵌套的 JSON 输出。

用 PHP 解决此类问题的最佳方法是什么?

赏金更新:

这是一个问题的测试用例:https://eval.in/private/4b0635c6e7b059

您将看到名称为Issue Here 的最后一条记录未显示在结果集中。这有一个与根节点匹配的managerID,应该在“Tom Jones”的employees 数组中。

【问题讨论】:

  • 最好的方法 - 编写一些代码 - 测试它 - 修复任何错误 - 测试它...重复直到对结果满意为止
  • @RiggsFolly - 谢谢,我做到了,并让它与 Javascript 完美配合。 PHP 不是我的强项,所以我想弄清楚 PHP 和 JS 之间是否有任何通用函数可以实现这一点。 php有没有像JS这样的reduce方法可以做到这一点? jsfiddle.net/87ztvk8m
  • php 有array_reduce 功能。看看这里 - php.net/manual/en/function.array-reduce.php

标签: php arrays json


【解决方案1】:

我有以下实用程序类可以完全满足您的需要。

class NestingUtil
{
    /**
     * Nesting an array of records using a parent and id property to match and create a valid Tree
     *
     * Convert this:
     * [
     *   'id' => 1,
     *   'parent'=> null
     * ],
     * [
     *   'id' => 2,
     *   'parent'=> 1
     * ]
     *
     * Into this:
     * [
     *   'id' => 1,
     *   'parent'=> null
     *   'children' => [
     *     'id' => 2
     *     'parent' => 1,
     *     'children' => []
     *    ]
     * ]
     *
     * @param array  $records      array of records to apply the nesting
     * @param string $recordPropId property to read the current record_id, e.g. 'id'
     * @param string $parentPropId property to read the related parent_id, e.g. 'parent_id'
     * @param string $childWrapper name of the property to place children, e.g. 'children'
     * @param string $parentId     optional filter to filter by parent
     *
     * @return array
     */
    public static function nest(&$records, $recordPropId = 'id', $parentPropId = 'parent_id', $childWrapper = 'children', $parentId = null)
    {
        $nestedRecords = [];
        foreach ($records as $index => $children) {
            if (isset($children[$parentPropId]) && $children[$parentPropId] == $parentId) {
                unset($records[$index]);
                $children[$childWrapper] = self::nest($records, $recordPropId, $parentPropId, $childWrapper, $children[$recordPropId]);
                $nestedRecords[] = $children;
            }
        }

        return $nestedRecords;
    }
}

与您的代码一起使用:

$employees = json_decode($flat_employees_json, true);
$managers = NestingUtil::nest($employees, 'EmployeeID', 'ManagerEmployeeID', 'employees');
print_r(json_encode($managers));

输出:

[
  {
    "FirstName": "Tom",
    "LastName": "Jones",
    "EmployeeID": "123",
    "ManagerEmployeeID": "",
    "Manager Name": "",
    "employees": [
      {
        "FirstName": "Alice",
        "LastName": "Wong",
        "EmployeeID": "456",
        "ManagerEmployeeID": "123",
        "Manager Name": "Tom Jones",
        "employees": []
      },
      {
        "FirstName": "Tommy",
        "LastName": "J.",
        "EmployeeID": "654",
        "ManagerEmployeeID": "123",
        "Manager Name": "Tom Jones",
        "employees": []
      }
    ]
  },
  {
    "FirstName": "Billy",
    "LastName": "Bob",
    "EmployeeID": "777",
    "ManagerEmployeeID": "",
    "Manager Name": "",
    "employees": [
      {
        "FirstName": "Rik",
        "LastName": "A.",
        "EmployeeID": "622",
        "ManagerEmployeeID": "777",
        "Manager Name": "Billy Bob",
        "employees": [
          {
            "FirstName": "Bob",
            "LastName": "Small",
            "EmployeeID": "111",
            "ManagerEmployeeID": "622",
            "Manager Name": "Rik A.",
            "employees": []
          },
          {
            "FirstName": "Small",
            "LastName": "Jones",
            "EmployeeID": "098",
            "ManagerEmployeeID": "622",
            "Manager Name": "Rik A",
            "employees": []
          }
        ]
      },
      {
        "FirstName": "Eric",
        "LastName": "C.",
        "EmployeeID": "222",
        "ManagerEmployeeID": "777",
        "Manager Name": "Billy Bob",
        "employees": []
      }
    ]
  }
]

Edit1:修复以避免忽略一些员工

如果最后一项是具有有效经理的员工但经理不在列表中,则忽略,因为应该位于哪里?,它不是根但没有有效的经理。强>

为避免这种情况,请在实用程序的 return 语句之前添加以下行。

if (!$parentId) {
    //merge residual records with the nested array
    $nestedRecords = array_merge($nestedRecords, $records);
}

return $nestedRecords;

Edit2:将实用程序更新到 PHP5.6

在 PHP7 中进行了一些测试后,该实用程序在 php7.0 中运行良好,但在 php5.6 中无法正常运行,我不知道为什么,但在数组引用和未设置中存在一些问题。我更新了实用程序代码以使用 php5.6 和您的用例。

 public static function nest($records, $recordPropId = 'id', $parentPropId = 'parent_id', $childWrapper = 'children', $parentId = null)
    {
        $nestedRecords = [];
        foreach ($records as $index => $children) {
            if (isset($children[$parentPropId]) && $children[$parentPropId] == $parentId) {
                $children[$childWrapper] = self::nest($records, $recordPropId, $parentPropId, $childWrapper, $children[$recordPropId]);
                $nestedRecords[] = $children;
            }
        }

        if (!$parentId) {
            $employeesIds = array_column($records, $recordPropId);
            $managers = array_column($records, $parentPropId);
            $missingManagerIds = array_filter(array_diff($managers, $employeesIds));
            foreach ($records as $record) {
                if (in_array($record[$parentPropId], $missingManagerIds)) {
                    $nestedRecords[] = $record;
                }
            }
        }

        return $nestedRecords;
    }

【讨论】:

  • 太棒了,我很快就会对此进行测试,但它看起来很棒!
  • 所以我试图将它集成到我的应用程序中,但遇到了一些麻烦。这是我正在使用的代码的 sn-p。 pastebin.com/j25bN7PN
  • 可以将你的json分享到$orgData进行本地测试
  • 我想我已经明白了。我现在遇到的唯一问题是该插件不喜欢 JSON 输出中开头和结尾的方括号。函数中有没有办法对此进行调整,使其以 { } 而不是 [{ }] 开头和结尾
  • 如果删除 [] 那么它不是一个有效的 json,但可以使用 preg_replace('/^\[|\]$/','',json_encode($array)); 来做到这一点
【解决方案2】:

这是从你的小提琴直接翻译成 PHP 的:

function makeTree($data, $parentId){
    return array_reduce($data,function($r,$e)use($data,$parentId){
        if(((empty($e->ManagerEmployeeID)||($e->ManagerEmployeeID==(object)[])) && empty($parentId)) or ($e->ManagerEmployeeID == $parentId)){
            $employees = makeTree($data, $e->EmployeeID);
            if($employees) $e->employees = $employees;
            $r[] = $e;
        }
        return $r;
    },[]);
}

它适用于您的测试输入。 见https://eval.in/private/ee9390e5e8ca95

示例用法:

$nested = makeTree(json_decode($json), '');
echo json_encode($nested, JSON_PRETTY_PRINT);

@rafrsr 解决方案非常灵活,但问题在于foreach 中的unset()。 它在迭代时修改数组,这是一个坏主意。 如果您删除unset(),它可以正常工作。

【讨论】:

  • 现在尝试用我的应用程序来实现它。我注意到的一件事是,当我在我的数据库中提供一个空/空管理器时,当我查看解码的 json 时,它会将其视为一个数组。 [MgrQID] => Array()。我在让您的代码与我的数据库数据集一起运行时遇到了一些麻烦,该数据集应该几乎相同。可能是上面的问题吗?是否期望在该值上看到 array
  • 您能告诉我您遇到问题的代码的具体部分吗?
  • 我必须为我的根管理员提供一些东西作为它的 managerID,所以我将它定义为root。然后我使用了您的代码,而不是将 parentID 留空,而是将其更改为 root。 $managers = makeTree($employees['data'], 'root');。我只是返回并清空数组而不是对其进行格式化,试图找出哪里出错了。
  • 这是问题所在 - 我的 ManagerID 被视为一个数组,因为它没有任何价值。我相信这导致reduce函数失败。看根节点,不是空字符串,实际上是一个空数组值。 eval.in/private/faade9774c7482 - 我认为考虑到这一点将解决问题。
  • 我更新了代码以说明空的ManagerEmployeeID。试一试。
【解决方案3】:

你可以在这里使用递归的魔力。请参考以下示例。 getTreeData 如您所见,在其自身下被调用。

function getTreeData($data=[], $parent_key='', $self_key='', $key='')
{
    if(!empty($data))
    {

        $new_array = array_filter($data, function($item) use($parent_key, $key) {

            return $item[$parent_key] == $key;
        });

        foreach($new_array as &$array)
        {
            $array["employees"] = getTreeData($data, $parent_key, $self_key, $array[$self_key]);

            if(empty($array["employees"]))
            {
                unset($array["employees"]);
            }
        }

        return $new_array;
    }
    else
    {
        return $data;
    }
}

$employees = json_decode($employees_json_string, true);

$employees_tree = getTreeData($employees, "ManagerEmployeeID", "EmployeeID");

【讨论】:

  • 我收到此错误:“致命错误:无法使用 stdClass 类型的对象作为数组”在这一行:return $item[$parent_key] == $key;
  • 在 json_decode 中添加 true 作为第二个参数可以正常工作
【解决方案4】:

这是一个巧妙的技巧,它利用对象通过引用传递的事实。

array_column 首先用于由EmployeeID 重新设置输入数组的密钥。 然后使用array_map 迭代条目并将每个项目重新分配给相应管理器的employees 数组。因为条目是对象(stdClass),所以正在修改相同的输入项。

通过引用添加到employees 数组的项目在根级别为空。然后使用array_filter 删除这些空条目。最后临时 EmployeeID 键被array_values 删除。

$input = json_decode($flat_employees_json, true);
$input = array_column($input, null, "EmployeeID");
$input = array_map(function ($entry) {
    return (object) $entry;
}, $input);
$output = array_values(array_filter(array_map(function ($entry) use ($input) {
    if (!empty($entry->ManagerEmployeeID)) {
        $input[$entry->ManagerEmployeeID]->employees[] = $entry;
        return null;
    }
    return $entry;
}, $input)));
echo json_encode($output, JSON_PRETTY_PRINT);

试试online

PHP 7 支持array_column 的对象数组,因此输入初始化可以简化为:

$input = array_column(json_decode($flat_employees_json), null, "EmployeeID");

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-21
    • 1970-01-01
    • 1970-01-01
    • 2017-11-22
    • 1970-01-01
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多