/** * Validates bounds of the nested tree structure. It will perform checks on * the `lft`, `rgt` and `parent_id` columns. Mainly that they're not null, * rights greater than lefts, and that they're within the bounds of the parent. * * @return boolean */ protected function validateBounds() { $connection = $this->node->getConnection(); $grammar = $connection->getQueryGrammar(); $tableName = $this->node->getTable(); $primaryKeyName = $this->node->getKeyName(); $parentColumn = $this->node->getQualifiedParentColumnName(); $lftCol = $grammar->wrap($this->node->getLeftColumnName()); $rgtCol = $grammar->wrap($this->node->getRightColumnName()); $qualifiedLftCol = $grammar->wrap($this->node->getQualifiedLeftColumnName()); $qualifiedRgtCol = $grammar->wrap($this->node->getQualifiedRightColumnName()); $qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName()); $whereStm = "({$qualifiedLftCol} IS NULL OR\n {$qualifiedRgtCol} IS NULL OR\n {$qualifiedLftCol} >= {$qualifiedRgtCol} OR\n ({$qualifiedParentCol} IS NOT NULL AND\n ({$qualifiedLftCol} <= parent.{$lftCol} OR\n {$qualifiedRgtCol} >= parent.{$rgtCol})))"; $query = $this->node->newQuery()->join($connection->raw($grammar->wrap($tableName) . ' AS parent'), $parentColumn, '=', $connection->raw('parent.' . $grammar->wrap($primaryKeyName)), 'left outer')->whereRaw($whereStm); return $query->count() == 0; }
/** * Get the fully qualified value for the specified column. * * @return string */ protected function qualify($column) { return $this->node->getTable() . '.' . $column; }
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); } }