/** * Maps a tree structure into the database without unguarding nor wrapping * inside a transaction. * * @param array|\Illuminate\Support\Contracts\ArrayableInterface * * @return bool */ public function mapTree($nodeList) { $tree = $nodeList instanceof ArrayableInterface ? $nodeList->toArray() : $nodeList; $affectedKeys = array(); $result = $this->mapTreeRecursive($tree, $this->node->getKey(), $affectedKeys); if ($result && count($affectedKeys) > 0) { $this->deleteUnaffected($affectedKeys); } return $result; }
/** * Runs the SQL query associated with the update of the indexes affected * by the move operation. * * @return int */ public function updateStructure() { list($a, $b, $c, $d) = $this->boundaries(); // select the rows between the leftmost & the rightmost boundaries and apply a lock $this->applyLockBetween($a, $d); $connection = $this->node->getConnection(); $grammar = $connection->getQueryGrammar(); $currentId = (int) $this->node->getKey(); $parentId = (int) $this->parentId(); $leftColumn = $this->node->getLeftColumnName(); $rightColumn = $this->node->getRightColumnName(); $parentColumn = $this->node->getParentColumnName(); $wrappedLeft = $grammar->wrap($leftColumn); $wrappedRight = $grammar->wrap($rightColumn); $wrappedParent = $grammar->wrap($parentColumn); $wrappedId = $grammar->wrap($this->node->getKeyName()); $lftSql = "CASE\n WHEN {$wrappedLeft} BETWEEN {$a} AND {$b} THEN {$wrappedLeft} + {$d} - {$b}\n WHEN {$wrappedLeft} BETWEEN {$c} AND {$d} THEN {$wrappedLeft} + {$a} - {$c}\n ELSE {$wrappedLeft} END"; $rgtSql = "CASE\n WHEN {$wrappedRight} BETWEEN {$a} AND {$b} THEN {$wrappedRight} + {$d} - {$b}\n WHEN {$wrappedRight} BETWEEN {$c} AND {$d} THEN {$wrappedRight} + {$a} - {$c}\n ELSE {$wrappedRight} END"; $parentSql = "CASE\n WHEN {$wrappedId} = {$currentId} THEN {$parentId}\n ELSE {$wrappedParent} END"; $updateConditions = array($leftColumn => $connection->raw($lftSql), $rightColumn => $connection->raw($rgtSql), $parentColumn => $connection->raw($parentSql)); if ($this->node->timestamps) { $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp(); } return $this->node->newNestedSetQuery()->where(function ($query) use($leftColumn, $rightColumn, $a, $d) { $query->whereBetween($leftColumn, array($a, $d))->orWhereBetween($rightColumn, array($a, $d)); })->update($updateConditions); }
/** * Return all children for the specified node. * * @param Baum\Node $node * @return Illuminate\Database\Eloquent\Collection */ public function children($node) { $query = $this->node->newQuery(); $query->where($this->node->getQualifiedParentColumnName(), '=', $node->getKey()); // We must also add the scoped column values to the query to compute valid // left and right indexes. foreach ($this->scopedAttributes($node) as $fld => $value) { $query->where($this->qualify($fld), '=', $value); } $query->orderBy($this->node->getQualifiedLeftColumnName()); $query->orderBy($this->node->getQualifiedRightColumnName()); $query->orderBy($this->node->getQualifiedKeyName()); return $query->get(); }
protected function performSave() { // there are two situation here, an orthodox form submittal and a ajax one: // - the orthodox will send a json string // - the ajax version will send an array $var = \Input::get($this->name); if (is_string($var)) { $var = json_decode($var, true); } $movements = []; $subtreeId = $this->source->getKey(); // We now invert the order of movements and group/sort them by // depth. This is done to avoid the situation where a node wants // to become the descendant of one of its own descendants. // This kind of sort will prevent the issue ensuring all the descendants // are moved first. $this->sortMovementsByDepth($var, $movements, $subtreeId); ksort($movements); $movements = call_user_func_array('array_merge', $movements); $movements = Collection::make($movements)->keyBy('id'); /** @var \Baum\Extensions\Eloquent\Collection $nodes */ $root = $this->source->getRoot(); // store depth and left ot the root, to build upon when // we will rebuild the tree. $rootDepth = $root->depth; $rootLeft = $root->lft; // now we read the entire tree. We need to do that because // of the nested set way workings: Baum provides handy methods // to move the nodes, but they trigger an awful lot of queries. // We'd rather read the whole tree once instead, and perform all // the calculations in-memory. $nodes = $root->getDescendantsAndSelf([$this->source->getKeyName(), 'lft', 'rgt', 'depth', 'parent_id']); // the ids of all the moved elements $movedIds = $movements->keys()->toArray(); // index the elements by primary key for speedy retrieval $dictionary = $nodes->getDictionary(); // the elements of the bigger tree that did not change their // parent_id $unmoved = new \Baum\Extensions\Eloquent\Collection(); foreach ($dictionary as $n) { if (!in_array($n->getKey(), $movedIds)) { $unmoved[] = $n; } } // the elements that were moved to a different parent $moved = new \Baum\Extensions\Eloquent\Collection(); foreach ($movedIds as $i) { $moved[] = $dictionary[$i]; } // this is the column that Baum uses to order the tree // the default is `lft` $orderColumn = $this->source->getOrderColumnName(); // we backup the order column, because we have to mess with // it later and we want to be able to restore it so we can // still use `$node->isDirty()` to see if the the node needs // to be updated or not. foreach ($dictionary as $n) { $n->__order = $n->{$orderColumn}; } // what now? We put all the moved nodes before the rest of the // tree. This way they'll be put before their unmoved siblings // shall they exist. $orderedNodes = $moved->merge($unmoved); // shady stuff going on here: Baum collections build the hierarchy // based on parent id AND the order column (lft). We thus update the // order column with an incremental value to be sure the siblings // order is preserved. $order = 1; foreach ($orderedNodes as $n) { $n->{$orderColumn} = $order++; if (isset($movements[$n->getKey()])) { // is the parent_id changed? If so, let's update it $n->parent_id = $movements[$n->getKey()]['parent_id']; } } // let Baum build the new tree $newTree = $orderedNodes->toHierarchy(); // lets restore the order column and delete the previous backup, // so we can use `$node->isDirty` later foreach ($dictionary as $n) { $n->{$orderColumn} = $n->__order; unset($n->__order); } // if everything worked correctly we should have a nested collection // with only one root element. The root ID should be unchanged. $newRoot = $newTree->first(); if ($newRoot->getKey() != $root->getKey() || count($newTree) != 1) { throw new \LogicException("Invalid tree"); } // now we take the new tree and recursively recalculate the left, right // and depth fields. $left = $rootLeft - 1; $depth = $rootDepth; $reindex = function ($tree, $reindex, $depth) use(&$left) { foreach ($tree as $node) { $left++; $node->lft = $left; $node->depth = $depth; $reindex($node->getRelation('children'), $reindex, $depth + 1); $left++; $node->rgt = $left; } }; $reindex($newTree, $reindex, $depth); // compute the changes and only save the changed ones! $bulk = []; foreach ($dictionary as $n) { if ($n->isDirty()) { $bulk[$n->getKey()] = ['lft' => $n->lft, 'rgt' => $n->rgt, 'depth' => $n->depth, 'parent_id' => $n->parent_id]; } } foreach ($bulk as $id => $fields) { \DB::table($this->source->getTable())->where($this->source->getKeyName(), $id)->update($fields); } }