11

I have a database query that provides me the output of some employee data. I want to use this data to pass to a plugin that generates an org chart. There are a few fields in the JSON object that I am pulling down which are:

FirstName
LastName
EmployeeID
ManagerEmployeeID
Manager Name

The data is returned as flat JSON object with no nesting or corellation between employees and their managers in the hierarchy.

Since I am unable to change the output of the source data (the database query), I am trying to figure out a way to nest the data so that the JSON output becomes a nested output.

My goal is to take this array and nest it based on the ManagerID and EmployeeID so I can make a tree hierarchy.

Example Data:

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

My flat data example:

    {
        "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"
    }

Example Desired Output:

[
  {
    "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"
      }
    ]
  }
]

Esentially I am trying to create a nested JSON output from a flat object using the EmployeeID and ManagerEmployeeID as the links between the two.

What is the best way to solve something like this with PHP?

Bounty Update:

Here is a test case of the issue: https://eval.in/private/4b0635c6e7b059

You will see that the very last record with the name of Issue Here does not show up in the result set. This has a managerID that matches the root node and should be within "Tom Jones's" employees array.

3
  • 3
    The best way - Is to write some code - test it -fix any mistakes - test it... repeat until happy with the result Commented May 16, 2017 at 22:09
  • @RiggsFolly - Thanks, I did that and have it working perfectly with Javascript. PHP isn't my strong suit so I am trying to figure out if there are any common functions between PHP and JS that can accomplish this. Does php have a reduce method like JS that can do this? jsfiddle.net/87ztvk8m Commented May 16, 2017 at 22:12
  • php has array_reduce function. have a look at here - php.net/manual/en/function.array-reduce.php Commented May 16, 2017 at 22:19

4 Answers 4

7
+50

I have the following utility class to do exactly what you need.

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;
    }
}

Usage with your code:

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

Output:

[
  {
    "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 to avoid ignoring some employees

If the last item is a employee with valid manager but the manager is not in the list, then is ignored, because where should be located?, it's not a root but does not have a valid manager.

To avoid this add the following lines just before the return statement in the utility.

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

return $nestedRecords;

Edit2: Updating the utility to PHP5.6

After some tests in PHP7 the utility works fine in php7.0 but not in php5.6, I'm not sure why, but is something in the array reference and the unset. I update the utility code to work with php5.6 and your use case.

 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;
    }
Sign up to request clarification or add additional context in comments.

15 Comments

Amazing, I will test this shortly but it looks great!
So I am trying to integrate this into my application and I am running into a little trouble. Here is a snippet of my code that I am working with. pastebin.com/j25bN7PN
can share your json in $orgData to make a test locally
I think I have figured it out. The only problem I am running into now is that the plugin doesn't like the square brackets on the JSON output in the beginning and the end. Is there a way in the function to tweak this so it begins and ends with { } not [{ }]
if remove [] then it's not a valid json, but can do this using preg_replace('/^\[|\]$/','',json_encode($array));
|
1

Here is a direct translation to PHP from your fiddle:

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;
    },[]);
}

It works correctly with your test input. See https://eval.in/private/ee9390e5e8ca95.

Example usage:

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

@rafrsr solution is nicely flexible, but the problem is the unset() inside the foreach. It modifies the array while it is being iterated, which is a bad idea. If you remove the unset(), it works correctly.

9 Comments

Trying to implement this now with my application. One thing I noticed is that when I provide a null/empty manager in my database, its treating it as an array when I view the decoded json. [MgrQID] => Array(). I am having some trouble getting your code to run with my database dataset which should be just about identical. Could the above be the issue? Is it expecting to see an array on that value?
Can you show me the specific part of your code that you're having trouble with?
I had to give my root manager something for its managerID so I defined it as root. I then used your code and instead of leaving the parentID blank, I changed it to root. $managers = makeTree($employees['data'], 'root');. I am just getting back and empty array instead of it being formatted, trying to figure out where it went wrong.
Here is the issue - my ManagerID is being treated as an array since it has no value. This is causing the reduce function to fail I believe. Look at the root node, instead of being an empty string, it is actually an empty array value. eval.in/private/faade9774c7482 - I think accounting for that will solve the issue.
I updated the code to account for empty ManagerEmployeeID. Give it a try.
|
1

You can use the magic power of recursion here. Please refer below example. getTreeData is being called under itself as you can see here.

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

2 Comments

I am getting this error: "Fatal error: Cannot use object of type stdClass as array " on this line: return $item[$parent_key] == $key;
Add true as second parameter in json_decode will work fine
0

Here's a neat trick that leverages the fact that objects are passed around by reference.

array_column is first used to rekey the input array by EmployeeID. array_map is then used to iterate the entries and to reassign each item to an employees array of the corresponding manager. Because entries are objects (stdClass), the same input items are being modified.

Items that are added to employees array by reference, are nulled at root level. array_filter is then used to remove these null entries. Finally the temporary EmployeeID keying is removed by 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);

Try it online.

PHP 7 supports an array of objects for array_column, so input initialization can be reduced to:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.