Doctrine Tree getting disorganized after changing child position - php

When I create three-levels nested tree ( with only three entities ) that looks like this:
1 (lft 1, rgt:6)
-2 (lft 2, rgt:5)
-3 (lft 3, rgt:4)
and then I try to move node ( with id=3, i.e. ) from the third level to the second level as, let's say second child with this piece of code:
/* this line can be commented - it doesn't work with it either */ $chapter->setParent($parentEntity);
$repo->persistAsFirstChildOf($chapter, $parentEntity);
$repo->moveDown($chapter, 1);
As a result I got the tree that goes like this:
1 ( lft:-4, rgt:6 )
-3 (lft: 5, rgt:6)
-2 (lft 7, rgt:5)
instead of this:
1 (lft 1, rgt:6)
-2 (lft 2, rgt:5)
-3 (lft 3, rgt:4)
and child which should become second in order becomes first. As You can see, lft values aren't proper. Am I missing something?

You should update node and set new parent by gedmo TreeListener (get it by NestedTreeRepository->listener) :
<?php
class YourNestedTreeRepository extends NestedTreeRepository
.......
/**
* #param Node $node
* #param Node $newParent
*
* #return void
*/
public function setNewParent($node, $newParent)
{
$meta = $this->getClassMetadata();
$this->listener
->getStrategy($this->_em, $meta->name)
->updateNode($this->_em, $node, $newParent)
;
}
and then, anywhere in your code:
//set as first child of a new parent - Tree hierarchy, it doesn't touch ORM relation
$repo->setNewParent($node, $newParent);
//set new parent and save. It updates ORM relation only, doesn't touch Tree hierarchy
$node->setParent($newParent);
$entityProvider->save($node); // or $entityManager->flush()
//additionaly move it down
if ($yourCondition) {
$result = $repo->moveDown($node, $position);
}

You actually are OK there.
I think left/right values does not matter there since your order is still ok.
Your probleme is that you call
$repo->moveDown($chapter, 1);
This function will make your $chapter to move at next position (second one in your case).
Delete the call to moveDown and try it again.
As far as lft and rgt attributes are concerned, adding/removing node may recompute them.
If it's a real matter to you, then try calling (i'm not sur about this):
$repo->recover();
$em->flush(); // ensures cache clean

Actually guys, I've chosen bad tree type. Don't use nested when You want to change children position often.

Related

Changing the position of nodes with CakePHP 2.x Tree and jstree

I have an application using CakePHP 2.x and jstree 3.2.1. I'm trying to figure out how it's possible to specify the position of a node when writing it to the database with CakePHP. The position itself comes from jstree...
When I drag and drop items with jstree the request URL gives me:
id - The ID of the node being dragged.
parent_id - The (parent) ID which the node has been dropped under.
position - this is an integer which starts at 0 and represents the position at which id has been dropped under parent_id. For example, a position of 2 means it should appear in 3rd position (3rd, not 2nd, because they start at 0).
CakePHP has methods in it's Tree Behaviour that allow you to move nodes in its Tree up and down. These methods are called moveUp() and moveDown() respectively.
I don't understand how it's possible to use the data provided from jstree with Cake's Tree behaviour such that you could update/save positions correctly.
Passing position to moveUp() or moveDown() would produce the wrong result. Why?
If jstree provides position = 2 and you were moving the 10th item in a list calling moveUp(2) through CakePHP means it would go into the 8th position, not the 2nd as intended. Similarly, moveDown(2) would move it to the 12th, which is not the desired outcome.
The schema that Cake has for it's Tree Behavior does not have a "position" field. Rather it uses lft and rght fields. The schema has:
id - ID of each individual tree node (auto increment)
parent_id - parent ID of the node. NULL if top level (no parent).
lft, rght - for MPTT logic. Cake generates these values automatically. They can be used to determine the order using ORDER BY lft ASC. But they are not the same values as position provided by jstree, and are unique for every row
name - text name of the node.
For example, consider the following tree:
D (id = 149)
1 (id = 150, parent_id = 149)
2 (id = 153, parent_id = 149)
3 (id = 154, parent_id = 149)
4 (id = 155, parent_id = 149)
5 (id = 156, parent_id = 149)
In the database Cake stores this as follows:
If I was to use jstree to drag and drop "2" so it appears between "3" and "4" it would make a request containing the following GET variables:
id = 153. This is the ID of "2"
parent_id = 149. This is the ID of "D" which is the parent node of "2".
position = 2. This means the 3rd position (3rd because positions start from 0).
But I cannot regenerate the lft and rght values from this data? And moveUp()/moveDown() are not helpful here because position cannot be passed in a way that would make this work.
The tree should be in the following order. id and parent_id should not change, but lft and rght must, because everything under "D" has effectively been re-ordered:
D
1
3
2 (moved)
4
5
Can anyone help with this?
I never used jsTree, but if it gives you the new parent ID and the sort position, then you should be able to use a combination of saving the new parent:
$Model->save(array('parent_id' => $parentId));
afterwards obtaining its child list, which will include the new child:
$children = $Model->children($parentId, true, array('id'));
and using the sort position to determine the delta to move the modified child:
$childIds = Hash::extract($children, '{n}.Model.id');
$positionMap = array_flip($childIds);
$currentPosition = $positionMap[$nodeId];
$delta = (int)$position - $currentPosition;
if ($delta !== 0) {
if ($delta < 0) {
$Model->moveUp($nodeId, abs($delta));
} else {
$Model->moveDown($nodeId, abs($delta));
}
}
This is just some rough example that should illustrate the idea of how this could work, it assumes $parentId to be the ID of the new parent, $nodeId to be the ID of the row being moved, and $position the (zero based) position the child was moved to. You'd have to account for other situations too, like for example when the parent ID doesn't change, ie only the sort position changes, and this should all be done in a transaction!
Here's a complete snippet based on the Cookbook example, it will move the Gwendolyn node to the second position in the Work node:
$nodeId = 8;
$parentId = 9;
$position = 1;
$dataSource = $this->Category->getDataSource();
$dataSource->begin();
$this->Category->id = $nodeId;
if (!$this->Category->save(array('parent_id' => $parentId))) {
$dataSource->rollback();
} else {
$children = $this->Category->children($parentId, true, array('id'));
$childIds = Hash::extract($children, '{n}.Category.id');
$positionMap = array_flip($childIds);
$result = true;
if (isset($positionMap[$nodeId])) {
$currentPosition = $positionMap[$nodeId];
$delta = (int)$position - $currentPosition;
if ($delta !== 0) {
if ($delta < 0) {
$result = $this->Category->moveUp($nodeId, abs($delta));
} else {
$result = $this->Category->moveDown($nodeId, abs($delta));
}
}
}
if ($result) {
$dataSource->commit();
} else {
$dataSource->rollback();
}
}

Symfony/Sonata get next child (Sibling)

I'm looking for the cleanest way to get the next child sibling of an object (next child of the parent).
-- Parent Object
-- Child 1
-- Child 2 (<== Current object)
-- Child 3 (<== Required object)
-- Child 4
Let assume in this example that we are talking about pages (Sonata pages). Currently I have Page 2 (Child 2), and need the next page of the same parent (in this case child 3). In the case I have the last page (child 4), then I need the first child again.
One option would be to request the parent, then request all the childs, loop over all the childs and look for the current child. Then take the next child, or the first one in case there is no next one. But this seems like a lot of code, with a lot of ugly if logic and loops. So I'm wondering if there is some sort of pattern to solve this.
Eventually I came up with the next solution:
/**
* $siblings is an array containing all pages with the same parent.
* So it also includes the current page.
* First check if there are siblings: Check if the parent has more then 1 child
**/
if (count($siblings) != 1) {
// Find the current page in the array
for ($i = 0; $i < count($siblings); $i++) {
// If we're not at the end of the array: Return next sibling
if ($siblings{$i}->getId() === $page->getId() && $i+1 != count($siblings)) {
return $siblings{$i+1};
}
// If we're at the end: Return first sibling
if ($siblings{$i}->getId() === $page->getId() && $i+1 == count($siblings)) {
return $siblings{0};
}
}
}
This seems like a quite clean solution to tackle this problem. We don't have an excessive amount of loops and if logic, however the code is still readable.

Placing a user in a binary tree

How can I find where to place users in my binary tree? considering the following binary tree:
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ / \
8 9 10 11
How can I build a function to return where the next user should be placed?
The rule here is that a user must be placed up as high as possible in the tree under their sponsor with each node getting them evenly. So for example, for anyone coming in with 1 as their sponsor: the next user should go to 6, the one after to 7, then to 6, then to 7, then to 8.
If someone had a sponsor of 5, they would go under 10. Sponsor of 2, they would go under 8, etc.
I've been stuck for hours trying to figure this out, and built a recursive function but it would just go down the left side and land on 8 first (as it checks 1, then 2, then 4, then 8)
My model for the tree is pretty basic (if relevant), its a table with a self-relationship like so:
id | user_id | parent_id
So each user can only have 2 children (2 entries with their id as a parent_id)
Typically when you do recursion, you end up with exactly what you got, you fly down one side (in this case the leftmost) checking everything as you go and then end up finding something way down at the bottom of the tree.
What you want to do is use a First In First Out ("FIFO") stack. You start out by adding your tree to the stack and while there is anything on the stack and you still haven't found space, you loop. Each loop you get the first item off the stack and loop again to check each of it's children for space. If you find space, you set the child aside, if you didn't find space, you add the child to the end of the stack so we check the child's children on a later iteration. You keep doing this until you find some space. After checking all of the children nodes, if any space was found you compare the nodes set aside to see which has the most free space and that is your target. This lets you search horizontally by pulling stuff off the start of the stack and adding new things to the end. You end up checking each neighbor before the children.
Here is a sample that will return a reference to the first open node found:
<?php
$tree = [
1 => [
2 => [
4 => [
8 => [],
9 => [],
],
5 => [
10 => [],
11 => [],
],
],
3 => [
6 => [],
7 => [],
],
]
];
function &findNextLocation(&$tree, $maxChildren=2){
//shortcut, check the root tree first
if(count($tree) < $maxChildren){
return $tree;
}
//fifo stack, start with the whole tree.
//we use a FIFO stack here so that we can check all nodes horizontally
//before traversing down another level. This stores all the child nodes
//we still need to search.
$stack = array(&$tree);
//potential place with space
$out = null;
//go through and check everything
//loop while there is something on the stack and we haven't
//found space yet ($out is null).
//we check $out here so that we stop as soon as somewhere with
//space is found and assigned.
while(is_null($out) && !empty($stack)){
//get the first node from the tree
$branch = &$stack[0];
array_shift($stack);
//loop over every node at this branch and look for space
foreach($branch as $id=>&$node){
//if there is space, assign it to our output variable
if(count($node) < $maxChildren){
//check the number of open spaces compared to our out spaces
if(is_null($out) || count($out) > count($node)){
//better spot, assign this node
$out = &$node;
//if anyone has zero children, we can't find less than that so break
if(count($out) == 0){
break;
}
}
} else {
//space not found here, add to our stack so we check the children
$stack[] = &$node;
}
}
}
//not found
return $out;
}
print_r($tree);
//loop a few more times starting at our next number
for($i=12; $i<=20; $i++){
//get a reference to the open node
$node = &findNextLocation($tree);
//add this node as a child
$node[$i] = [];
//remove reference to the found node to prevent errors.
unset($node);
}
print_r($tree);
You can see a demo here: https://3v4l.org/X3BX4
The way you are describing this, it sounds like it is in an SQL table. You can use the above to find a position and insert a new value to an existing tree. Or if you add another column to the table that is depth meaning the distance from the root node, you can do it all in a query. The depth can't easily be calculated because it would require recursion which isn't really available with a procedure or user function. However, it is easy to update your current records by just parent depth+1. With a depth column, you can do something like this:
SELECT *
FROM tree_table as tt
WHERE
#where there is space (less than 2 children)
(SELECT COUNT(*)
FROM tree_table
WHERE parent_id=tt.id) < 2
ORDER BY
#distance from top of tree
depth,
#by free space
(SELECT COUNT(*)
FROM tree_table
WHERE parent_id=tt.id),
#leftmost
id
Demo here: http://sqlfiddle.com/#!9/5f6bfc/5

How to store big binary tree

How to store big binary trees (about thousands in depth).
We tried to store in database, in table with rows: elem, parent, left_child, right_child
but it's very slow to handle this tree (to calculate, draw a part of it).
Which way you recommend? (calculating may be done in php). May be to store it in XML or json (text file) or in some matrices?
You could use an array or other linear store (e.g. relation id->node value) in the way that each node x has the children 2*x and 2*x+1. The problem might be that there are unused ids if the tree is unbalanced.
Sample:
2---4---8
/ \ \9
1-- 5---10
\ \11
3---6---12
\ \13
7---14
\15
Seems like writing to something like XML as you suggest would be best. Converting to XML and then back when you want to reload it would be pretty straight forward.
I use a structure like following
id parent tree slug name path children
1 0 1 root Root || |5|
2 7 1 child-1 Child 1 |1-5-8-7| |3|
It is super easy to query huge depth trees, because the query is linear.
There is a bit more data in each line, but that makes it quicker.
For example you can get a whole subtree with a simple query like:
$sql = 'SELECT * FROM `'.$this->_table_name.'` WHERE
`path` LIKE "%-:parent-%"
OR `path` LIKE "%|:parent-%"
OR `path` LIKE "%-:parent|%"
OR `path` LIKE "%|:parent|%"';
$result = $this->_db->custom_query($sql, array('parent' => $root->id));
And then build the depth array by a simple function:
private function _build_tree(Node $root, $data)
{
$mapped_node = array(
'node' => $root,
'subnodes' => array()
);
if ( !empty($root->children) )
{
foreach( $root->children as $child )
{
if ( array_key_exists($child, $data) )
{
$mapped_node['subnodes'][] = $this->_build_tree($data[$child], $data);
}
}
}
return $mapped_node;
}
NOTE! When I get back the children and path columns, I split them into arrays for PHP to be able to better work with them.

Linear Array with nodes randomly linked to other nodes in array, shortest path

INFO:
I have an Array of 100 nodes, [ 0 .. 99 ]. Each node can have an arbitrary number of linked nodes:
eg1, 0 links to 5, 10, 15, 20.
eg2, 1 links to 30, 40, 50.
eg3, etc..
All 100 nodes have at least one linked node, nodes do not know who links to them.
QUESTION:
How can I find the shortest link-path if provided with START and END.
eg. START=5, END=80, Link Path (example) : [5]->10->24->36->[80]?
I'm using Pascal and/or PHP, but understanding how is what I'm looking for [code helps too].
Plenty of reading/algorithms:
Shortest path problem. You effectively just have every edge ("link", as you called it) with an equal weight.
Do a Breadth First Traversal starting with the Start node and quit as soon as you find the end node.
Does this have cycles? i.e. is it a DAG?
If there aren't cycles:
List<Node> GetShortestPath(Node startNode, Node endNode)
{
//If this is the node you are looking for...
if (startNode.ReferenceEquals(endNode))
{
//return a list with just the end node
List<Nodes> result = new List<Nodes>();
result.Add(endNode);
return result;
}
List<Node> bestPath = null;
foreach(Node child in startNode.Children)
{
//get the shortest path from this child
List<Node> childPath = GetShortestPath(child, endNode);
if (childPath != null &&
( bestPath == null || childPath.Count < bestPath.Count))
{
bestPath = childPath;
}
}
bestPath.Insert(0, startNode);
return bestPath;
}
[Edit: Added an example for cycles]
If there can be cycles:
List<Node> GetShortestPath(Node startNode, Node endNode)
{
List<Node> nodesToExclude = new List<Node>();
return GetShortestPath(startNode, endNOde, nodesToExclude);
}
List<Node> GetShortestPath(Node startNode, Node endNode, List<Node> nodesToExclude)
{
nodesToExclude.Add(startNode);
List<Node> bestPath = null;
//If this is end node...
if (startNode.ReferenceEquals(endNode))
{
//return a list with just the child node
List<Nodes> result = new List<Nodes>();
result.Add(endNode);
return result;
}
foreach(Node child in startNode.Children)
{
if (!nodesToExclude.Contains(child))
{
//get the shortest path from this child
List<Node> childPath = GetShortestPath(child, endNode);
if (childPath != null &&
( bestPath == null || childPath.Count < bestPath.Count))
{
bestPath = childPath;
}
}
}
nodesToExclude.Remove(startNode);
bestPath.Insert(0, child);
return bestPath;
}
Two structures: a set and a list.
In the set, you store nodes you have already visited. This prevents you from following cycles.
The list is of objects containing: (1) a node, and (2) a pointer back to the node that found it.
Starting at the start node, add it to the set, add it to the list with a null back reference, and then add all the nodes it can reach to the list with back references to the index 0 in the list (the start node).
Then for each element in the list thereafter, up until you reach the end, do the following:
if it is in the set already skip it (you have already visited it) and move to the next item in the list.
otherwise, add it to the set, and add all nodes it can reach to the list with back references to the index you are 'looking at' to the end of the list. Then go to the next index in the list and repeat.
If at any point you reach the end node (optimally as you are adding it to the list - as opposed to visiting it in the list), track back through the back references to the start node and invert the path.
Example:
Given nodes 0 through 3, where
node0 --> node1
node0 --> node2
node1 --> node2
node2 --> node3
and node0 is START and node3 is END
SET = {}
LIST = []
Step 1 - add START:
SET = {node0}
LIST = [[node0, null]]
Step 2 - at index 0 of the list - add reachable nodes:
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 3 - at index 1 of the LIST - add reachable nodes:
node2 is already in the SET. skip adding reachable nodes to LIST.
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 4 - at index 2 of the LIST - add reachable nodes:
SET = {node0, node1, node2, node3}
LIST = [[node0, null], [node1, 0], [node2, 0], [node3, 2]]
Step 5 - reached END, now backtrack:
The END node (node3) inserted in the LIST has a back reference to index 2 in the list, which is node2. This has a back reference to index 0 in the list, which is node0 (START). Invert this and you get a shortest path of node0 --> node2 --> node3.
Is this a tree/graph or a forest? If it is a forest, the path may not be defined always. In case this is a tree/graph, try using Breadth-First-Search.
Think of it this way: say, you are out on a stealth mission to find cute chicks in your neighbourhood. You start at your own house and mark it as the START. You'd next go to knock on your closest neighbours, right? So, we'll do just that -- push all nodes connected to the start in a queue. Now, repeat the neighbour search for all the nodes in this queue. And keep doing this till you get your girl, err, the END.

Categories