PHP Вложенный JSON из плоского JSON

У меня есть запрос к базе данных, который дает мне вывод некоторых данных о сотрудниках. Я хочу использовать эти данные для перехода к плагину, который генерирует диаграмму org. В объекте 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"
      }
    ]
  }
]

Иногда я пытаюсь создать вложенный вывод JSON из плоского объекта с использованием EmployeeID и ManagerEmployeeID в качестве связей между ними.

Каков наилучший способ решить что-то подобное с помощью PHP?

Обновление Bounty:

Вот тестовый пример проблемы: https://eval.in/private/4b0635c6e7b059

Вы увидите, что самая последняя запись с именем Issue Here не отображается в наборе результатов. У этого есть managerID, который соответствует корню node и должен находиться внутри массива employees Тома Джонса.

Ответ 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: Fix, чтобы избежать игнорирования некоторых сотрудников.

Если последний элемент является сотрудником с действующим менеджером, но менеджер отсутствует в списке, то игнорируется, потому что где должно быть расположено?, это не корень, но не имеющий действительного менеджера.

Чтобы избежать этого, добавьте следующие строки непосредственно перед оператором 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;
    }

Ответ 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 прекрасно гибко, но проблема заключается в unset() внутри foreach. Он изменяет массив во время его повторения, что является плохой идеей. Если вы удалите unset(), он будет работать правильно.

Ответ 3

Здесь вы можете использовать магическую силу рекурсии. См. Пример ниже. getTreeData​​strong > вызывается под собой, как вы можете видеть здесь.

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");

Ответ 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);

Попробуйте онлайн.

PHP 7 поддерживает массив объектов для array_column, поэтому инициализация ввода может быть уменьшена до:

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