PHP Nested Navigation - php

I am building out a database-driven navigation, and I need some help in a method to build my data structure. I'm not very experienced with recursion, but that is most likely the path this will take. The database table has an id column, a parent_id column, and a label column. The result of calling the method provides me with the data structure. The way my data structure should result in the following:
Records with a parent_id of 0 are assumed to be root elements.
Each root element contains an array of children if a child exists which holds an array of elements containing the parent_id equal to the root element id.
Children may contain a children array containing parent_ids equal to the immediate child (this would be the recursive point)
When a record exists that contains a parent_id which isn't 0, it gets added to the array of the children elements.
Here is how the data structure should look:
$data = array(
'home' => array(
'id' => 1,
'parent_id' => 0,
'label' => 'Test',
'children' => array(
'immediatechild' => array(
'id' => 2,
'parent_id' => 1,
'label' => 'Test1',
'children' => array(
'grandchild' => array(
'id' => 3,
'parent_id' => 2,
'label' => 'Test12',
))
))
)
);
Here's something I came up with in a few moments. Its not correct, but its what I want to use and Id like some help fixing it.
<?php
// should i pass records and parent_id? anything else?
function buildNav($data,$parent_id=0)
{
$finalData = array();
// if is array than loop
if(is_array($data)){
foreach($data as $record){
// not sure how/what to check here
if(isset($record['parent_id']) && ($record['parent_id'] !== $parent_id){
// what should i pass into the recursive call?
$finalData['children'][$record['label'][] = buildNav($record,$record['parent_id']);
}
}
} else {
$finalData[] = array(
'id' => $data['id'],
'parent_id' => $parent_id,
'label' => $data['label'],
)
}
return $finalData
}
Thanks for the help!

Simplest solution (assuming you've got the data stored in relational represenation using the parent id as a FK to indicate the hierarchy) is to just brute force it:
$start=array(
array('parent_id'=>0, 'title'=>'Some root level node', 'id'=>100),
array('parent_id'=>0, 'title'=>'Other root level node', 'id'=>193),
array('parent_id'=>100, 'title'=>'a child node', 'id'=>83),
....
);
// NB this method will work better if you sort the list by parent id
$tree=get_children($start, 0);
function get_children(&$arr, $parent)
{
static $out_index;
$i=0;
$out=array();
foreach($arr as $k=>$node) {
if ($node['parent_id']==$parent) {
++$i;
$out[$out_index+$i]=$node;
if (count($arr)>1) {
$out[$out_index+$i]['children']=get_children($arr, $node['id']);
}
unset($arr[$k]);
}
$out_index+=$i;
if ($i) {
return $out;
} else {
return false;
}
}
But a better solution is to use an adjacency list model for the data in the database. As an interim solution you might want to serialize the tree array and cache it in a file rather than parse it every time.

Related

Recursive array search - keep proper order and iterate down array

When accessing the script I am writing, you pass the category path to it when accessing the page. The script then compares the data to an array of actual categories, or a branch, that should be associated with that category.
I am setting the parents and all of its children into a tree and then going down the branch and comparing the data to ensure the customer is using a correct url. Here's a quick example of how the code works:
// Customer is accessing from site.com/store/some-cat/some-othercat
// We pass those variables with the htaccess to script.php?var=$1,$2
// We then explode that to make an array on $var[0] and $var[1]
$categoryMap = explode(",", $_GET['var']);
$categoryID = array();
$categoryInfoMap = array();
foreach ($categoryMap as $a) {
$categoryIDs[] = trim($a);
}
$getCategoryInfo = $db->fn->query("SELECT * FROM store_category");
....
// Inside while loop...
$categoryInfoMap[] = $db->result[]; // stored whole results as array
// End of the while loop
$masterKey = $mainClass->findKeyInDbArray($categoryInfoMap, 'c.path', $categoryMap[0]);
if ((isset($masterKey) && $masterKey === "0") || !empty($masterKey)) {
$thisId = $categoryInfoMap[$masterKey]['c.id'];
$thisPath = $categoryInfoMap[$masterKey]['c.path'];
$thisName = $categoryInfoMap[$masterKey]['c.name'];
$tree = $mainClass->buildTree($categoryInfoMap);
$children = $tree['children'][$thisId];
$childrenItems = "";
foreach ($categoryIDs as $cid) {
// One of the categories entered doesnt exist at all so we redirect,
// else we will go through them and make sure theyre apart of the branch
if (!$mainClass->recursive_array_search($cid, $tree)) {
... redirect them somewhere and die()
} else {
if (!$mainClass->recursive_array_search($cid, $children)) {
... redirect them somewhere and die()
} else {
!!!!!!!!!!!!============!!!!!!!!!!!!!!
THIS IS THE IMPORTANT PART HERE
!!!!!!!!!!!!============!!!!!!!!!!!!!!
}
}
}
}
... Rest of the script which works for now
Here is the functions used in the code above
public function findKeyInDbArray($products, $field, $value) {
foreach($products as $key => $product) {
if ($product[$field] === $value) {
return "$key";
}
}
return NULL;
}
public function buildTree($arr) {
$tree = array(
'children' => array()
);
$index = array(0=>&$tree);
foreach ($arr as $key => $val) {
$parent = &$index[$val['c.parentcatid']];
$node = $val;
$parent['children'][$val['c.id']] = $node;
$index[$val['c.id']] = &$parent['children'][$val['c.id']];
}
return $tree;
}
public function recursive_array_search($needle,$haystack) {
foreach($haystack as $key=>$value) {
$current_key=$key;
if($needle===$value OR (is_array($value) && $this->recursive_array_search($needle,$value) !== false)) {
return $current_key;
}
}
return false;
}
And here's an example of the tree array, from the parent node down. Shorted for visibility reasons
Array(
[c.id] => 1
[c.name] => Radios
[c.path] => radios
[c.parentcatid] => 0
[children] => (
[2] => (
[0] => 2
....
[children] => (
[3] => (
[c.id] => 3
....
[c.parentcatid] => 2
),
[4] => (
[c.id] => 4
....
[c.parentcatid] => 2
)
)
)
......
[10] => (
[0] => 10
....
[c.parentcatid] => 1
)
)
SO onto the good bits
Right now the code is working to prove that the branches have matching variables from their tree. If the item path, which is the variable we are using to compare to the url $var matches, then it will continue and work. so if in the branch the following values exist:
array(c.path => 'foo'),
array(c.path => 'bar')
And I visit the script as site.com/store/foo/bar then everything works great. If i visit the site as site.com/store/foo/notBar then it will fail, as the notBar variable is not a member of this branch. That's perfect right? Everything should work! Except it doesn't and for a good reason.
The issue here
If the item matches in the branch then it has passed the check and it's the end of the check. Not if the item is passed in the wrong order, such as site.com/store/bar/foo, then it still technically has good variables in it, but it should NOT pass since the structure is not in the order its coming down the parent array. Likewise, if another branch farther up the tree, lets say barwithNoChildren exists, i can swap foo or bar out with it and still pass, even though nothing should be there.
Hopefully you guy understand what I am asking, and can help suggest ways around this. I've been wracking my brain on this system for the last couple of days and since they want fancy urls for seo and other reasons, it's been a lot more difficult than I planned. Thanks for any suggestions!
A tree structure is not really helpful for this purpose. You should be thinking about how to create a data structure that makes it easy for you to match the input. Since your category input describes a branch of the tree, the best thing to do is build an array that you can use to match those branch descriptions to your categories efficiently.
Let's build an array where the keys are the paths for each category as described by their slugs, and the values are the category IDs. We can then immediately identify the matching category, or fail if the path is not in the array.
This breadcrumb-like structure is another pattern that is commonly used with categories. Along with the tree and flat id map, you can do pretty much anything you need. The key takeaway is to think about creating different structures with your data to accomplish different tasks. It's usually more efficient and less error prone to create a new structure that's easy to work with than it is to create complex logic to try and work with an existing structure that doesn't lend itself to the task at hand.
<?php
//Mock category records, would come from the DB in the real world
$categoryRecords = [
['id' => 1, 'title' => 'Radios', 'slug' => 'radios', 'parent_id' => 0],
['id' => 2, 'title' => 'Accessories', 'slug' => 'misc', 'parent_id' => 1],
['id' => 3, 'title' => 'Motorola', 'slug' => 'motorola', 'parent_id' => 1],
['id' => 4, 'title' => 'Handheld', 'slug' => 'handheld', 'parent_id' => 3],
['id' => 5, 'title' => 'Mobile', 'slug' => 'mobile', 'parent_id' => 3]
];
//Create an array that maps parent IDs to primary keys
$idMap = [];
foreach ($categoryRecords as $currRecord)
{
$idMap[$currRecord['id']] = $currRecord;
}
//Traverse the flat array and build the path lines
$paths = [];
$categoryIds = array_keys($idMap);
foreach ($categoryIds as $currLeafId)
{
$currCategoryId = $currLeafId;
$currLine = [];
do
{
$currLine[] = $idMap[$currCategoryId]['slug'];
$currCategoryId = $idMap[$currCategoryId]['parent_id'];
} while ($currCategoryId != 0);
$currLine = array_reverse($currLine);
$currPath = implode('/', $currLine);
$paths[$currPath] = $currLeafId;
}
//Join your input - $_GET['var'] in your example
$inputPath = implode('/', ['radios', 'motorola', 'handheld']);
//Now you can see if the incoming path matched a category
if(array_key_exists($inputPath, $paths))
{
$category = $categoryRecords[$paths[$inputPath]];
echo 'Matched category: '.$category['title'].PHP_EOL;
}
else
{
echo 'Invalid category path';
}

Optimization of an algorithm performed on a big array in PHP

I have a query that populates an array from the database. In some cases, this query returns a great amount of data, (let's say for purpose of an example, 100.000 records). Each row of the database has at least 6 or 7 columns.
$results = [
['id' => 1, 'name' => 'name', 'status' => true, 'date' => '10-01-2012'],
['id' => 2, 'name' => 'name 2', 'status' => false 'date' => '10-01-2013'],
...
]
I need to perform a substitution of some of the data inside the $results array, based on another one that give me some information about how i would change the values in the rows.
$model = [
'status' => ['function' => 'formatStatus', params => ['status']],
'date' => ['function' => 'formatDate', params => ['date']]
]
Now that i have all the data and what do i do with it i have the following routine.
foreach ($results as &$itemResult) {
$oldValues = $itemResult;
foreach ($itemResult as $attribute => &$value) {
if (isset($model[$attribute]['function'])) {
$function = $model[$attribute]['function'];
$params = $model[$attribute]['params'];
$paramsSize = count($params);
for ($i = 0; $i < $paramsSize; $i++) {
$newParams[] = $oldValues[$params[$i]];
}
$itemResult[$attribute] = call_user_func_array([$this, $function], $newParams);
$newParams = null;
}
}
}
So, for each attribute for each row of my data array, i run check for the existence of a function and params information. When the attribute in question needs to be replaced, i call the function via call_user_func_array and replace the value with the function return value.
Also notice that i am replacing the current array, not creating another, by passing the reference &$itemResult inside the loop, so in the end, i have the same array from the beginning but with all columns that needed to be replaced with its new values.
The thing is, for little arrays, this method is quite good. But for big ones, it becomes a pain.
Could you guys provide me some alternative to the problem?
Should i use another data structure instead of the PHP array?

Merging CSV lines where column value is the same

I have a big CSV file with about 30 columns and 2.5K rows.
Some of the rows have exactly the same values except some columns.
I would like to merge those alike and concatenate with a comma between the values of the columns that are not the same.
Small example:
id name age kid
1 Tom 40 John
1 Tom 40 Roger
---merged becomes---
1 Tom 40 John, Roger
I can do this with PHP using lots and lots of fors and ifs but I am hoping that there's a more elegant and fast way.
This is a great beginner question for a common programming problem. What you'll want to do is a two step approach. First, parse the CSV into a data structure that you can easily modify, then loop over that structure and generate a new array that matches the output.
<?php
// Parse CSV into rows like:
$rows = array(
array(
'id' => 1,
'name' => 'Tom',
'age' => 50,
'kid' => 'John'
),
array(
'id' => 1,
'name' => 'Tom',
'age' => 50,
'kid' => 'Roger'
),
array(
'id' => 2,
'name' => 'Pete',
'age' => 40,
'kid' => 'Pete Jr.'
),
);
// Array for output
$concatenated = array();
// Key to organize over
$sortKey = 'id';
// Key to concatenate
$concatenateKey = 'kid';
// Separator string
$separator = ', ';
foreach($rows as $row) {
// Guard against invalid rows
if (!isset($row[$sortKey]) || !isset($row[$concatenateKey])) {
continue;
}
// Current identifier
$identifier = $row[$sortKey];
if (!isset($concatenated[$identifier])) {
// If no matching row has been found yet, create a new item in the
// concatenated output array
$concatenated[$identifier] = $row;
} else {
// An array has already been set, append the concatenate value
$concatenated[$identifier][$concatenateKey] .= $separator . $row[$concatenateKey];
}
}
// Do something useful with the output
var_dump($concatenated);
If you only have the data in a CSV file, I think that the easiest way to do what you want is build an associative array using the common data as key and modifing it if exists:
$array=[];
while ($a=fgetcsv($handle)){
if (isset($array[$a[0]."-".$a[1]."-".$a[2]])) {
$array[$a[0]."-".$a[1]."-".$a[2]].=",".$a[3];
}
else {
$array[$a[0]."-".$a[1]."-".$a[2]]=$a[3];
}
}

Some questions on MongoDb Category hierarchy use case

I'm a PHP developer trying to tackle http://docs.mongodb.org/ecosystem/use-cases/category-hierarchy/, but I don't know much about Python.
My first question:
for cat in db.categories.find(
{'ancestors._id': bop_id},
{'parent_id': 1}):
build_ancestors_full(cat['_id'], cat['parent_id'])
Where does 'parent_id' come from? Isn't it suppose to be just 'parent'?
My second question:
def build_ancestors_full(_id, parent_id):
ancestors = []
while parent_id is not None:
parent = db.categories.find_one(
{'_id': parent_id},
{'parent': 1, 'name': 1, 'slug': 1, 'ancestors':1})
parent_id = parent.pop('parent')
ancestors.append(parent)
db.categories.update(
{'_id': _id},
{'$set': { 'ancestors': ancestors } })
I would appreciate a psuedo explanation (or PHP equivalent) of this helper function,
mainly the following lines:
parent_id = parent.pop('parent')
ancestors.append(parent)
Thank you!
UPDATE & Answer:
Two errors in the example codes:
The first is 'parent_id' => should be 'parent'
The second is
{'parent': 1, 'name': 1, 'slug': 1, 'ancestors':1})
=> ancestors field should be _id
{'parent_id': 1} in the find query is saying that you only want to return the key parent_id, think of it like ( and it can be used like ) {'parent_id': true} try it out in the mongo shell. This parameter is called the projection, you can suppress keys as well. But in this instance it is saying, only return to me the 'parent_id' key from the document that is found. However, if you do not suppress the _id column explicitly, it WILL be returned.
The second part of your question: This code is assigning the value that is returned from the find query, in this case it will be a document where the _id is equal to the parent_id passed into the function - build_ancestors_full. This document will display the parent key, name key, slut, and ancestors key. parent.pop('parent') will pop a value from the 'parent' key of the parent variable which holds the document I just described. Ancestors is an array, ancestors.append(parent) will append the document I described above to the ancestors array.
PHP Equivalent:
// get all documents that contain ancestors._id = $bop_id, only return the 'parent_id' field
$result = $db->categories->find(array('ancestors._id' => $bop_id), array('parent_id' => 1));
foreach ($result as $document) {
build_ancestors_full($document['_id'], $document['parent_id']);
}
From your first question - I agree that the use of parent_id is a typo.
Part 2 PHP:
function build_ancestors_full($id, $parent_id) {
$ancestors = array();
while ($parent_id != null) {
$parent = $db->categories->find_one(
array('_id' => parent_id),
array('parent' => 1, 'name' => 1, 'slug' => 1, 'ancestors' => 1));
$parent_id = $parent['parent'];
unset($parent['parent']);
// array push
$ancestors[] = $parent;
}
$result = $db->categories->update(
array('_id' => _id),
array('$set' => array('ancestors' => $ancestors ) ));
}

PHP - How to access a deep array's contents, building the path to it dynamically

I am trying to build a hierarchical array in PHP, from relational database contents that are stored using a closure table. For a given result, I will have the full path to the LEAF node, the below would look like my result set.
1~root~the root node
1~root~the root node>>>2~category1~First category
1~root~the root node>>>3~category2~Second category
1~root~the root node>>>2~category1~First category>>>4~subCatOfCategory1~SubCategory of Cat 1
Anyway, those are my database results. So I want to traverse them and build a hierarchical structure in PHP so I can convert it to JSON and render a tree in DOJO
So as I walk through each row, I am building a "path" to the leaf because I only need to add an element to tree when the element is a "leaf"... Along that thinking I decided that I would tokenize each result, using ">>>" as the delimiter, giving me the nodes that are in that row. Then I loop through those nodes, tokenizing each one by "~" which gives me the attributes of each node.
So, I have a for loop to process each ROW and it basically determines that if the node being processed is NOT a leaf, add it's ID to an array that is to track the path to get to the eventual leaf that will be processed. THEN, when I finally do arrive at the LEAF, I can call a function to insert a node, using the PATH that I've compiled along the way.
Hopefully that all makes sense.. so I've included the code below.. Consider the second result from above. When I have processed that entire result and am about to call the function insertNodeInTreeV2(), the arrays look as below...
$fullTree is an array with 1 element, indexed at [1]
That element contains an array with four elements: ID(1), NAME(root), Description(the root node), CHILDREN(empty array)
$pathEntries is an array with only one element, (1). That is to mean that the PATH to the LEAF node being inserted is by node [1], which is the root node.
$nodeToInsert is an array with four elements: ID(2), NAME(category1), Description(First Category), CHILDREN(empty array)
$treeRootPattern is a STRING that contains the Variable name I'm using to store the whole array/tree, which in this case is "fullTree".
private function insertNodeInTreeV2( array &$fullTree, array $pathEntries, array $nodeToInsert, $treeRootPattern )
{
$compiledPath = null;
foreach ( $pathEntries as $path ) {
$compiledPath .= $treeRootPattern . '[' . $path . '][\'CHILDREN\']';
}
// as this point $compiledPath = "fullTree[1]['CHILDREN']"
$treeVar = $$compiledPath;
}
So when I make the assignment, $treeVar = $$compiledPath;, I THINK I am setting the variable $treeVar to be equal to $fullTree[1]['CHILDREN'] (which I have verified in my debugger is a valid array index). Even if I paste the contents of $compiledPath into a new Expression in Eclipse debugger, it shows me an empty array, which makes sense because that is what's located in $fullTree[1]['CHILDREN']
But instead, the runtime is telling me the following error...
troller.php line 85 - Undefined variable: fullTree[1]['CHILDREN']
Any help on this would be greatly appreciated... And if you have a better way for me to get from the result set I described to the hierarchical array I'm trying to build, I'd be eager to adopt a better method.
UPDATED TO ADD THE CODE THAT CALLS THE ABOVE FUNCTION -- THE FOR LOOP PROCESSES ROWS OF DATABASE RESULTS, AS DESCRIBED ABOVE
foreach ( $ontologyEntries as $entry ) {
// iterating over rows of '1~~root~~The root node>>>2~~category1~~The first category
$nodes = explode( '>>>', $entry['path'] );
$numNodes = count( $nodes ) - 1 ;
$pathToNewNode = null; // this is the path, based on ID, to get to this *new* node
for ( $level = 0; $level <= $numNodes; $level++ ) {
// Parse the node out of the database search result
$thisNode = array(
'ID' => strtok($nodes[$level], '~~'), /* 1 */
'NAME' => strtok( '~~'), /* Root */
'DESCRIPTION' => strtok( '~~'), /* This is the root node */
'CHILDREN' => array()
);
if ( $level < $numNodes ) { // Not a leaf, add it to the pathToThisNodeArray
$pathToNewNode[] = $thisNode['ID'];
}
else {
// processing a leaf, add it to the array
$this->insertNodeInTreeV2( $$treeRootPattern, $pathToNewNode, $thisNode, $treeRootPattern );
}
}
}
See my comments below your question for an explanation.
$paths = array(
"1~root~the root node",
"1~root~the root node>>>2~category1~First category",
"1~root~the root node>>>3~category2~Second category",
"1~root~the root node>>>2~category1~First category>>>4~subCatOfCategory1~SubCategory of Cat 1"
);
$tree = array();
foreach ($paths as $path)
{
$currentNode = &$tree;
$parts = explode(">>>", $path);
foreach ($parts as $part)
{
$node = explode("~", $part);
// create all nodes along this path
if (!isset($currentNode[$node[0]]))
{
$currentNode[$node[0]] = array(
"ID" => $node[0],
"NAME" => $node[1],
"DESCRIPTION" => $node[2],
"CHILDREN" => array(),
);
}
$currentNode = &$currentNode[$node[0]]["CHILDREN"];
}
}
var_dump($tree);
Outputs:
array
1 =>
array
'ID' => string '1' (length=1)
'NAME' => string 'root' (length=4)
'DESCRIPTION' => string 'the root node' (length=13)
'CHILDREN' =>
array
2 =>
array
'ID' => string '2' (length=1)
'NAME' => string 'category1' (length=9)
'DESCRIPTION' => string 'First category' (length=14)
'CHILDREN' =>
array
4 =>
array
'ID' => string '4' (length=1)
'NAME' => string 'subCatOfCategory1' (length=17)
'DESCRIPTION' => string 'SubCategory of Cat 1' (length=20)
'CHILDREN' => &
array
empty
3 =>
array
'ID' => string '3' (length=1)
'NAME' => string 'category2' (length=9)
'DESCRIPTION' => string 'Second category' (length=15)
'CHILDREN' =>
array
empty
The loop will create all nodes that are included in a path, so you won't need to insert 1~root~the root node, if you also insert 1~root~the root node>>>2~category1~First category.
You can change this by only creating nodes if the node is the path's last node. The length of a path is count($parts) and you can count which level you are in inside the inner foreach-loop.
I hope this is what you wanted.

Categories