diff --git a/README.md b/README.md index 5ef0320c..b81235d9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Baum [![Build Status](https://travis-ci.org/etrepat/baum.png?branch=master)](https://travis-ci.org/etrepat/baum) +[![StyleCI](https://styleci.io/repos/51155875/shield)](https://styleci.io/repos/51155875) Baum is an implementation of the [Nested Set](http://en.wikipedia.org/wiki/Nested_set_model) pattern for [Laravel 5's](http://laravel.com/) Eloquent ORM. diff --git a/src/Baum/Extensions/Eloquent/Collection.php b/src/Baum/Extensions/Eloquent/Collection.php index e6fa41c2..17eadaea 100644 --- a/src/Baum/Extensions/Eloquent/Collection.php +++ b/src/Baum/Extensions/Eloquent/Collection.php @@ -3,39 +3,55 @@ use Illuminate\Database\Eloquent\Collection as BaseCollection; -class Collection extends BaseCollection { - - public function toHierarchy() { - $dict = $this->getDictionary(); +/** + * Class Collection + * @package Baum\Extensions\Eloquent + */ +class Collection extends BaseCollection +{ + + /** + * @return BaseCollection + */ + public function toHierarchy() + { + $dict = $this->getDictionary(); + + // Enforce sorting by $orderColumn setting in Baum\Node instance + uasort($dict, function ($a, $b) { + return ($a->getOrder() >= $b->getOrder()) ? 1 : -1; + }); + + return new BaseCollection($this->hierarchical($dict)); + } - // Enforce sorting by $orderColumn setting in Baum\Node instance - uasort($dict, function($a, $b){ - return ($a->getOrder() >= $b->getOrder()) ? 1 : -1; - }); + /** + * @param $result + * @return mixed + */ + protected function hierarchical($result) + { + foreach ($result as $key => $node) { + $node->setRelation('children', new BaseCollection); + } - return new BaseCollection($this->hierarchical($dict)); - } + $nestedKeys = array(); - protected function hierarchical($result) { - foreach($result as $key => $node) - $node->setRelation('children', new BaseCollection); + foreach ($result as $key => $node) { + $parentKey = $node->getParentId(); - $nestedKeys = array(); + if (!is_null($parentKey) && array_key_exists($parentKey, $result)) { + $result[$parentKey]->children[] = $node; - foreach($result as $key => $node) { - $parentKey = $node->getParentId(); + $nestedKeys[] = $node->getKey(); + } + } - if ( !is_null($parentKey) && array_key_exists($parentKey, $result) ) { - $result[$parentKey]->children[] = $node; + foreach ($nestedKeys as $key) { + unset($result[$key]); + } - $nestedKeys[] = $node->getKey(); - } + return $result; } - foreach($nestedKeys as $key) - unset($result[$key]); - - return $result; - } - } diff --git a/src/Baum/Extensions/Eloquent/Model.php b/src/Baum/Extensions/Eloquent/Model.php index 6a124629..5dc843bd 100644 --- a/src/Baum/Extensions/Eloquent/Model.php +++ b/src/Baum/Extensions/Eloquent/Model.php @@ -6,114 +6,127 @@ use Illuminate\Database\Eloquent\SoftDeletingScope; use Baum\Extensions\Query\Builder as QueryBuilder; -abstract class Model extends BaseModel { - - /** - * Reloads the model from the database. - * - * @return \Baum\Node - * - * @throws ModelNotFoundException - */ - public function reload() { - if ( $this->exists || ($this->areSoftDeletesEnabled() && $this->trashed()) ) { - $fresh = $this->getFreshInstance(); - - if ( is_null($fresh) ) - throw with(new ModelNotFoundException)->setModel(get_called_class()); - - $this->setRawAttributes($fresh->getAttributes(), true); - - $this->setRelations($fresh->getRelations()); - - $this->exists = $fresh->exists; - } else { - // Revert changes if model is not persisted - $this->attributes = $this->original; +abstract class Model extends BaseModel +{ + + /** + * Reloads the model from the database. + * + * @return \Baum\Node + * + * @throws ModelNotFoundException + */ + public function reload() + { + if ($this->exists || ($this->areSoftDeletesEnabled() && $this->trashed())) { + $fresh = $this->getFreshInstance(); + + if (is_null($fresh)) { + throw with(new ModelNotFoundException)->setModel(get_called_class()); + } + + $this->setRawAttributes($fresh->getAttributes(), true); + + $this->setRelations($fresh->getRelations()); + + $this->exists = $fresh->exists; + } else { + // Revert changes if model is not persisted + $this->attributes = $this->original; + } + + return $this; } - return $this; - } - - /** - * Get the observable event names. - * - * @return array - */ - public function getObservableEvents() { - return array_merge(array('moving', 'moved'), parent::getObservableEvents()); - } - - /** - * Register a moving model event with the dispatcher. - * - * @param Closure|string $callback - * @return void - */ - public static function moving($callback, $priority = 0) { - static::registerModelEvent('moving', $callback, $priority); - } - - /** - * Register a moved model event with the dispatcher. - * - * @param Closure|string $callback - * @return void - */ - public static function moved($callback, $priority = 0) { - static::registerModelEvent('moved', $callback, $priority); - } - - /** - * Get a new query builder instance for the connection. - * - * @return \Baum\Extensions\Query\Builder - */ - protected function newBaseQueryBuilder() { - $conn = $this->getConnection(); - - $grammar = $conn->getQueryGrammar(); - - return new QueryBuilder($conn, $grammar, $conn->getPostProcessor()); - } - - /** - * Returns a fresh instance from the database. - * - * @return \Baum\Node - */ - protected function getFreshInstance() { - if ( $this->areSoftDeletesEnabled() ) - return static::withTrashed()->find($this->getKey()); - - return static::find($this->getKey()); - } - - /** - * Returns wether soft delete functionality is enabled on the model or not. - * - * @return boolean - */ - public function areSoftDeletesEnabled() { - // To determine if there's a global soft delete scope defined we must - // first determine if there are any, to workaround a non-existent key error. - $globalScopes = $this->getGlobalScopes(); - - if ( count($globalScopes) === 0 ) return false; - - // Now that we're sure that the calling class has some kind of global scope - // we check for the SoftDeletingScope existance - return static::hasGlobalScope(new SoftDeletingScope); - } - - /** - * Static method which returns wether soft delete functionality is enabled - * on the model. - * - * @return boolean - */ - public static function softDeletesEnabled() { - return with(new static)->areSoftDeletesEnabled(); - } + /** + * Get the observable event names. + * + * @return array + */ + public function getObservableEvents() + { + return array_merge(array('moving', 'moved'), parent::getObservableEvents()); + } + + /** + * Register a moving model event with the dispatcher. + * + * @param Closure|string $callback + * @return void + */ + public static function moving($callback, $priority = 0) + { + static::registerModelEvent('moving', $callback, $priority); + } + + /** + * Register a moved model event with the dispatcher. + * + * @param Closure|string $callback + * @return void + */ + public static function moved($callback, $priority = 0) + { + static::registerModelEvent('moved', $callback, $priority); + } + + /** + * Get a new query builder instance for the connection. + * + * @return \Baum\Extensions\Query\Builder + */ + protected function newBaseQueryBuilder() + { + $conn = $this->getConnection(); + + $grammar = $conn->getQueryGrammar(); + + return new QueryBuilder($conn, $grammar, $conn->getPostProcessor()); + } + + /** + * Returns a fresh instance from the database. + * + * @return \Baum\Node + */ + protected function getFreshInstance() + { + if ($this->areSoftDeletesEnabled()) { + return static::withTrashed()->find($this->getKey()); + } + + return static::find($this->getKey()); + } + + /** + * Returns wether soft delete functionality is enabled on the model or not. + * + * @return boolean + */ + public function areSoftDeletesEnabled() + { + // To determine if there's a global soft delete scope defined we must + // first determine if there are any, to workaround a non-existent key error. + $globalScopes = $this->getGlobalScopes(); + + if (count($globalScopes) === 0) { + return false; + } + + // Now that we're sure that the calling class has some kind of global scope + // we check for the SoftDeletingScope existance + return static::hasGlobalScope(new SoftDeletingScope); + } + + /** + * Static method which returns wether soft delete functionality is enabled + * on the model. + * + * @return boolean + */ + public static function softDeletesEnabled() + { + return with(new static)->areSoftDeletesEnabled(); + } } diff --git a/src/Baum/Extensions/Query/Builder.php b/src/Baum/Extensions/Query/Builder.php index 46ad758d..0504422e 100644 --- a/src/Baum/Extensions/Query/Builder.php +++ b/src/Baum/Extensions/Query/Builder.php @@ -4,36 +4,42 @@ use Illuminate\Database\Query\Builder as BaseBuilder; -class Builder extends BaseBuilder { - - /** - * Replace the "order by" clause of the current query. - * - * @param string $column - * @param string $direction - * @return \Illuminate\Database\Query\Builder|static - */ - public function reOrderBy($column, $direction = 'asc') { - $this->orders = null; - - if ( !is_null($column) ) return $this->orderBy($column, $direction); - - return $this; - } - - /** - * Execute an aggregate function on the database. - * - * @param string $function - * @param array $columns - * @return mixed - */ - public function aggregate($function, $columns = array('*')) { - // Postgres doesn't like ORDER BY when there's no GROUP BY clause - if ( !isset($this->groups) ) - $this->reOrderBy(null); - - return parent::aggregate($function, $columns); - } +class Builder extends BaseBuilder +{ + + /** + * Replace the "order by" clause of the current query. + * + * @param string $column + * @param string $direction + * @return \Baum\Extensions\Query\Builder|static + */ + public function reOrderBy($column, $direction = 'asc') + { + $this->orders = null; + + if (!is_null($column)) { + return $this->orderBy($column, $direction); + } + + return $this; + } + + /** + * Execute an aggregate function on the database. + * + * @param string $function + * @param array $columns + * @return mixed + */ + public function aggregate($function, $columns = array('*')) + { + // Postgres doesn't like ORDER BY when there's no GROUP BY clause + if (!isset($this->groups)) { + $this->reOrderBy(null); + } + + return parent::aggregate($function, $columns); + } } diff --git a/src/Baum/Move.php b/src/Baum/Move.php index 8abed059..7fd6a854 100644 --- a/src/Baum/Move.php +++ b/src/Baum/Move.php @@ -1,54 +1,55 @@ node = $node; - $this->target = $this->resolveNode($target); - $this->position = $position; + public function __construct($node, $target, $position) + { + $this->node = $node; + $this->target = $this->resolveNode($target); + $this->position = $position; - $this->setEventDispatcher($node->getEventDispatcher()); + $this->setEventDispatcher($node->getEventDispatcher()); } /** @@ -81,10 +83,11 @@ public function __construct($node, $target, $position) { * @param string $position * @return \Baum\Node */ - public static function to($node, $target, $position) { - $instance = new static($node, $target, $position); + public static function to($node, $target, $position) + { + $instance = new static($node, $target, $position); - return $instance->perform(); + return $instance->perform(); } /** @@ -92,29 +95,31 @@ public static function to($node, $target, $position) { * * @return \Baum\Node */ - public function perform() { - $this->guardAgainstImpossibleMove(); + public function perform() + { + $this->guardAgainstImpossibleMove(); - if ( $this->fireMoveEvent('moving') === false ) - return $this->node; + if ($this->fireMoveEvent('moving') === false) { + return $this->node; + } - if ( $this->hasChange() ) { - $self = $this; + if ($this->hasChange()) { + $self = $this; - $this->node->getConnection()->transaction(function() use ($self) { + $this->node->getConnection()->transaction(function () use ($self) { $self->updateStructure(); }); - $this->target->reload(); + $this->target->reload(); - $this->node->setDepthWithSubtree(); + $this->node->setDepthWithSubtree(); - $this->node->reload(); - } + $this->node->reload(); + } - $this->fireMoveEvent('moved', false); + $this->fireMoveEvent('moved', false); - return $this->node; + return $this->node; } /** @@ -123,53 +128,55 @@ public function perform() { * * @return int */ - public function updateStructure() { - list($a, $b, $c, $d) = $this->boundaries(); + 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(); + $connection = $this->node->getConnection(); + $grammar = $connection->getQueryGrammar(); - $currentId = $this->quoteIdentifier($this->node->getKey()); - $parentId = $this->quoteIdentifier($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()); + $currentId = $this->quoteIdentifier($this->node->getKey()); + $parentId = $this->quoteIdentifier($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 + $lftSql = "CASE WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b WHEN $wrappedLeft BETWEEN $c AND $d THEN $wrappedLeft + $a - $c ELSE $wrappedLeft END"; - $rgtSql = "CASE + $rgtSql = "CASE WHEN $wrappedRight BETWEEN $a AND $b THEN $wrappedRight + $d - $b WHEN $wrappedRight BETWEEN $c AND $d THEN $wrappedRight + $a - $c ELSE $wrappedRight END"; - $parentSql = "CASE + $parentSql = "CASE WHEN $wrappedId = $currentId THEN $parentId ELSE $wrappedParent END"; - $updateConditions = array( + $updateConditions = [ $leftColumn => $connection->raw($lftSql), $rightColumn => $connection->raw($rgtSql), - $parentColumn => $connection->raw($parentSql) - ); + $parentColumn => $connection->raw($parentSql), + ]; - if ( $this->node->timestamps ) - $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp(); + if ($this->node->timestamps) { + $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp(); + } - return $this->node + return $this->node ->newNestedSetQuery() - ->where(function($query) use ($leftColumn, $rightColumn, $a, $d) { - $query->whereBetween($leftColumn, array($a, $d)) - ->orWhereBetween($rightColumn, array($a, $d)); + ->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) { + $query->whereBetween($leftColumn, [$a, $d]) + ->orWhereBetween($rightColumn, [$a, $d]); }) ->update($updateConditions); } @@ -182,10 +189,13 @@ public function updateStructure() { * @param \Baum\node|int * @return \Baum\Node */ - protected function resolveNode($node) { - if ( $node instanceof \Baum\Node ) return $node->reload(); + protected function resolveNode($node) + { + if ($node instanceof \Baum\Node) { + return $node->reload(); + } - return $this->node->newNestedSetQuery()->find($node); + return $this->node->newNestedSetQuery()->find($node); } /** @@ -193,30 +203,37 @@ protected function resolveNode($node) { * * @return void */ - protected function guardAgainstImpossibleMove() { - if ( !$this->node->exists ) - throw new MoveNotPossibleException('A new node cannot be moved.'); - - if ( array_search($this->position, array('child', 'left', 'right', 'root')) === FALSE ) - throw new MoveNotPossibleException("Position should be one of ['child', 'left', 'right'] but is {$this->position}."); - - if ( !$this->promotingToRoot() ) { - if ( is_null($this->target) ) { - if ( $this->position === 'left' || $this->position === 'right' ) - throw new MoveNotPossibleException("Could not resolve target node. This node cannot move any further to the {$this->position}."); - else - throw new MoveNotPossibleException('Could not resolve target node.'); + protected function guardAgainstImpossibleMove() + { + if (! $this->node->exists) { + throw new MoveNotPossibleException('A new node cannot be moved.'); } - if ( $this->node->equals($this->target) ) - throw new MoveNotPossibleException('A node cannot be moved to itself.'); - - if ( $this->target->insideSubtree($this->node) ) - throw new MoveNotPossibleException('A node cannot be moved to a descendant of itself (inside moved tree).'); + if (array_search($this->position, ['child', 'left', 'right', 'root']) === false) { + throw new MoveNotPossibleException("Position should be one of ['child', 'left', 'right'] but is {$this->position}."); + } - if ( !$this->node->inSameScope($this->target) ) - throw new MoveNotPossibleException('A node cannot be moved to a different scope.'); - } + if (! $this->promotingToRoot()) { + if (is_null($this->target)) { + if ($this->position === 'left' || $this->position === 'right') { + throw new MoveNotPossibleException("Could not resolve target node. This node cannot move any further to the {$this->position}."); + } else { + throw new MoveNotPossibleException('Could not resolve target node.'); + } + } + + if ($this->node->equals($this->target)) { + throw new MoveNotPossibleException('A node cannot be moved to itself.'); + } + + if ($this->target->insideSubtree($this->node)) { + throw new MoveNotPossibleException('A node cannot be moved to a descendant of itself (inside moved tree).'); + } + + if (! $this->node->inSameScope($this->target)) { + throw new MoveNotPossibleException('A node cannot be moved to a different scope.'); + } + } } /** @@ -224,10 +241,13 @@ protected function guardAgainstImpossibleMove() { * * @return int */ - protected function bound1() { - if ( !is_null($this->_bound1) ) return $this->_bound1; + protected function bound1() + { + if (! is_null($this->_bound1)) { + return $this->_bound1; + } - switch ( $this->position ) { + switch ($this->position) { case 'child': $this->_bound1 = $this->target->getRight(); break; @@ -245,8 +265,9 @@ protected function bound1() { break; } - $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); - return $this->_bound1; + $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); + + return $this->_bound1; } /** @@ -255,11 +276,15 @@ protected function bound1() { * * @return int */ - protected function bound2() { - if ( !is_null($this->_bound2) ) return $this->_bound2; + protected function bound2() + { + if (! is_null($this->_bound2)) { + return $this->_bound2; + } - $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); - return $this->_bound2; + $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); + + return $this->_bound2; } /** @@ -267,20 +292,23 @@ protected function bound2() { * * @return array */ - protected function boundaries() { - if ( !is_null($this->_boundaries) ) return $this->_boundaries; + protected function boundaries() + { + if (! is_null($this->_boundaries)) { + return $this->_boundaries; + } // we have defined the boundaries of two non-overlapping intervals, // so sorting puts both the intervals and their boundaries in order - $this->_boundaries = array( - $this->node->getLeft() , - $this->node->getRight() , - $this->bound1() , - $this->bound2() - ); - sort($this->_boundaries); - - return $this->_boundaries; + $this->_boundaries = [ + $this->node->getLeft(), + $this->node->getRight(), + $this->bound1(), + $this->bound2(), + ]; + sort($this->_boundaries); + + return $this->_boundaries; } /** @@ -288,10 +316,11 @@ protected function boundaries() { * * @return int */ - protected function parentId() { - switch( $this->position ) { + protected function parentId() + { + switch ($this->position) { case 'root': - return NULL; + return; case 'child': return $this->target->getKey(); @@ -304,19 +333,21 @@ protected function parentId() { /** * Check wether there should be changes in the downward tree structure. * - * @return boolean + * @return bool */ - protected function hasChange() { - return !( $this->bound1() == $this->node->getRight() || $this->bound1() == $this->node->getLeft() ); + protected function hasChange() + { + return ! ($this->bound1() == $this->node->getRight() || $this->bound1() == $this->node->getLeft()); } /** * Check if we are promoting the provided instance to a root node. * - * @return boolean + * @return bool */ - protected function promotingToRoot() { - return ($this->position == 'root'); + protected function promotingToRoot() + { + return $this->position == 'root'; } /** @@ -324,8 +355,9 @@ protected function promotingToRoot() { * * @return \Illuminate\Events\Dispatcher */ - public static function getEventDispatcher() { - return static::$dispatcher; + public static function getEventDispatcher() + { + return static::$dispatcher; } /** @@ -334,8 +366,9 @@ public static function getEventDispatcher() { * @param \Illuminate\Events\Dispatcher * @return void */ - public static function setEventDispatcher(Dispatcher $dispatcher) { - static::$dispatcher = $dispatcher; + public static function setEventDispatcher(Dispatcher $dispatcher) + { + static::$dispatcher = $dispatcher; } /** @@ -345,16 +378,19 @@ public static function setEventDispatcher(Dispatcher $dispatcher) { * @param bool $halt * @return mixed */ - protected function fireMoveEvent($event, $halt = true) { - if ( !isset(static::$dispatcher) ) return true; + protected function fireMoveEvent($event, $halt = true) + { + if (! isset(static::$dispatcher)) { + return true; + } // Basically the same as \Illuminate\Database\Eloquent\Model->fireModelEvent // but we relay the event into the node instance. $event = "eloquent.{$event}: ".get_class($this->node); - $method = $halt ? 'until' : 'fire'; + $method = $halt ? 'until' : 'fire'; - return static::$dispatcher->$method($event, $this->node); + return static::$dispatcher->$method($event, $this->node); } /** @@ -363,15 +399,17 @@ protected function fireMoveEvent($event, $halt = true) { * @param mixed $value * @return string */ - protected function quoteIdentifier($value) { - if ( is_null($value) ) - return 'NULL'; + protected function quoteIdentifier($value) + { + if (is_null($value)) { + return 'NULL'; + } - $connection = $this->node->getConnection(); + $connection = $this->node->getConnection(); - $pdo = $connection->getPdo(); + $pdo = $connection->getPdo(); - return $pdo->quote($value); + return $pdo->quote($value); } /** @@ -381,8 +419,9 @@ protected function quoteIdentifier($value) { * @param int $rgt * @return void */ - protected function applyLockBetween($lft, $rgt) { - $this->node->newQuery() + protected function applyLockBetween($lft, $rgt) + { + $this->node->newQuery() ->where($this->node->getLeftColumnName(), '>=', $lft) ->where($this->node->getRightColumnName(), '<=', $rgt) ->select($this->node->getKeyName()) diff --git a/src/Baum/MoveNotPossibleException.php b/src/Baum/MoveNotPossibleException.php index 4832ec2c..8ca8cf19 100644 --- a/src/Baum/MoveNotPossibleException.php +++ b/src/Baum/MoveNotPossibleException.php @@ -1,4 +1,7 @@ setDefaultLeftAndRight(); - }); - - static::saving(function($node) { - $node->storeNewParent(); - }); - - static::saved(function($node) { - $node->moveToNewParent(); - $node->setDepth(); - }); - - static::deleting(function($node) { - $node->destroyDescendants(); - }); - - if ( static::softDeletesEnabled() ) { - static::restoring(function($node) { - $node->shiftSiblingsForRestore(); - }); - - static::restored(function($node) { - $node->restoreDescendants(); - }); - } - } - - /** - * Get the parent column name. - * - * @return string - */ - public function getParentColumnName() { - return $this->parentColumn; - } - - /** - * Get the table qualified parent column name. - * - * @return string - */ - public function getQualifiedParentColumnName() { - return $this->getTable(). '.' .$this->getParentColumnName(); - } - - /** - * Get the value of the models "parent_id" field. - * - * @return int - */ - public function getParentId() { - return $this->getAttribute($this->getparentColumnName()); - } - - /** - * Get the "left" field column name. - * - * @return string - */ - public function getLeftColumnName() { - return $this->leftColumn; - } - - /** - * Get the table qualified "left" field column name. - * - * @return string - */ - public function getQualifiedLeftColumnName() { - return $this->getTable() . '.' . $this->getLeftColumnName(); - } - - /** - * Get the value of the model's "left" field. - * - * @return int - */ - public function getLeft() { - return $this->getAttribute($this->getLeftColumnName()); - } - - /** - * Get the "right" field column name. - * - * @return string - */ - public function getRightColumnName() { - return $this->rightColumn; - } - - /** - * Get the table qualified "right" field column name. - * - * @return string - */ - public function getQualifiedRightColumnName() { - return $this->getTable() . '.' . $this->getRightColumnName(); - } - - /** - * Get the value of the model's "right" field. - * - * @return int - */ - public function getRight() { - return $this->getAttribute($this->getRightColumnName()); - } - - /** - * Get the "depth" field column name. - * - * @return string - */ - public function getDepthColumnName() { - return $this->depthColumn; - } - - /** - * Get the table qualified "depth" field column name. - * - * @return string - */ - public function getQualifiedDepthColumnName() { - return $this->getTable() . '.' . $this->getDepthColumnName(); - } - - /** - * Get the model's "depth" value. - * - * @return int - */ - public function getDepth() { - return $this->getAttribute($this->getDepthColumnName()); - } - - /** - * Get the "order" field column name. - * - * @return string - */ - public function getOrderColumnName() { - return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn; - } - - /** - * Get the table qualified "order" field column name. - * - * @return string - */ - public function getQualifiedOrderColumnName() { - return $this->getTable() . '.' . $this->getOrderColumnName(); - } - - /** - * Get the model's "order" value. - * - * @return mixed - */ - public function getOrder() { - return $this->getAttribute($this->getOrderColumnName()); - } - - /** - * Get the column names which define our scope - * - * @return array - */ - public function getScopedColumns() { - return (array) $this->scoped; - } - - /** - * Get the qualified column names which define our scope - * - * @return array - */ - public function getQualifiedScopedColumns() { - if ( !$this->isScoped() ) - return $this->getScopedColumns(); - - $prefix = $this->getTable() . '.'; - - return array_map(function($c) use ($prefix) { - return $prefix . $c; }, $this->getScopedColumns()); - } - - /** - * Returns wether this particular node instance is scoped by certain fields - * or not. - * - * @return boolean - */ - public function isScoped() { - return !!(count($this->getScopedColumns()) > 0); - } - - /** - * Parent relation (self-referential) 1-1. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function parent() { - return $this->belongsTo(get_class($this), $this->getParentColumnName()); - } - - /** - * Children relation (self-referential) 1-N. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function children() { - return $this->hasMany(get_class($this), $this->getParentColumnName()) - ->orderBy($this->getOrderColumnName()); - } - - /** - * Get a new "scoped" query builder for the Node's model. - * - * @param bool $excludeDeleted - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public function newNestedSetQuery($excludeDeleted = true) { - $builder = $this->newQuery($excludeDeleted)->orderBy($this->getQualifiedOrderColumnName()); - - if ( $this->isScoped() ) { - foreach($this->scoped as $scopeFld) - $builder->where($scopeFld, '=', $this->$scopeFld); - } - - return $builder; - } - - /** - * Overload new Collection - * - * @param array $models - * @return \Baum\Extensions\Eloquent\Collection - */ - public function newCollection(array $models = array()) { - return new Collection($models); - } - - /** - * Get all of the nodes from the database. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public static function all($columns = array('*')) { - $instance = new static; - - return $instance->newQuery() - ->orderBy($instance->getQualifiedOrderColumnName()) - ->get(); - } - - /** - * Returns the first root node. - * - * @return NestedSet - */ - public static function root() { - return static::roots()->first(); - } - - /** - * Static query scope. Returns a query scope with all root nodes. - * - * @return \Illuminate\Database\Query\Builder - */ - public static function roots() { - $instance = new static; - - return $instance->newQuery() - ->whereNull($instance->getParentColumnName()) - ->orderBy($instance->getQualifiedOrderColumnName()); - } - - /** - * Static query scope. Returns a query scope with all nodes which are at - * the end of a branch. - * - * @return \Illuminate\Database\Query\Builder - */ - public static function allLeaves() { - $instance = new static; - - $grammar = $instance->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); - - return $instance->newQuery() - ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1') - ->orderBy($instance->getQualifiedOrderColumnName()); - } - - /** - * Static query scope. Returns a query scope with all nodes which are at - * the middle of a branch (not root and not leaves). - * - * @return \Illuminate\Database\Query\Builder - */ - public static function allTrunks() { - $instance = new static; - - $grammar = $instance->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); - - return $instance->newQuery() - ->whereNotNull($instance->getParentColumnName()) - ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1') - ->orderBy($instance->getQualifiedOrderColumnName()); - } - - /** - * Checks wether the underlying Nested Set structure is valid. - * - * @return boolean - */ - public static function isValidNestedSet() { - $validator = new SetValidator(new static); - - return $validator->passes(); - } - - /** - * Rebuilds the structure of the current Nested Set. - * - * @param bool $force - * @return void - */ - public static function rebuild($force = false) { - $builder = new SetBuilder(new static); - - $builder->rebuild($force); - } - - /** - * Maps the provided tree structure into the database. - * - * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean - */ - public static function buildTree($nodeList) { - return with(new static)->makeTree($nodeList); - } - - /** - * Query scope which extracts a certain node object from the current query - * expression. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeWithoutNode($query, $node) { - return $query->where($node->getKeyName(), '!=', $node->getKey()); - } - - /** - * Extracts current node (self) from current query expression. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeWithoutSelf($query) { - return $this->scopeWithoutNode($query, $this); - } - - /** - * Extracts first root (from the current node p-o-v) from current query - * expression. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeWithoutRoot($query) { - return $this->scopeWithoutNode($query, $this->getRoot()); - } - - /** - * Provides a depth level limit for the query. - * - * @param query \Illuminate\Database\Query\Builder - * @param limit integer - * @return \Illuminate\Database\Query\Builder - */ - public function scopeLimitDepth($query, $limit) { - $depth = $this->exists ? $this->getDepth() : $this->getLevel(); - $max = $depth + $limit; - $scopes = array($depth, $max); - - return $query->whereBetween($this->getDepthColumnName(), array(min($scopes), max($scopes))); - } - - /** - * Returns true if this is a root node. - * - * @return boolean - */ - public function isRoot() { - return is_null($this->getParentId()); - } - - /** - * Returns true if this is a leaf node (end of a branch). - * - * @return boolean - */ - public function isLeaf() { - return $this->exists && ($this->getRight() - $this->getLeft() == 1); - } - - /** - * Returns true if this is a trunk node (not root or leaf). - * - * @return boolean - */ - public function isTrunk() { - return !$this->isRoot() && !$this->isLeaf(); - } - - /** - * Returns true if this is a child node. - * - * @return boolean - */ - public function isChild() { - return !$this->isRoot(); - } - - /** - * Returns the root node starting at the current node. - * - * @return NestedSet - */ - public function getRoot() { - if ( $this->exists ) { - return $this->ancestorsAndSelf()->whereNull($this->getParentColumnName())->first(); - } else { - $parentId = $this->getParentId(); - - if ( !is_null($parentId) && $currentParent = static::find($parentId) ) { - return $currentParent->getRoot(); - } else { +abstract class Node extends Model +{ + /** + * Column name to store the reference to parent's node. + * + * @var string + */ + protected $parentColumn = 'parent_id'; + + /** + * Column name for left index. + * + * @var string + */ + protected $leftColumn = 'lft'; + + /** + * Column name for right index. + * + * @var string + */ + protected $rightColumn = 'rgt'; + + /** + * Column name for depth field. + * + * @var string + */ + protected $depthColumn = 'depth'; + + /** + * Column to perform the default sorting. + * + * @var string + */ + protected $orderColumn = null; + + /** + * Guard NestedSet fields from mass-assignment. + * + * @var array + */ + protected $guarded = ['id', 'parent_id', 'lft', 'rgt', 'depth']; + + /** + * Indicates whether we should move to a new parent. + * + * @var int + */ + protected static $moveToNewParentId = null; + + /** + * Columns which restrict what we consider our Nested Set list. + * + * @var array + */ + protected $scoped = []; + + /** + * The "booting" method of the model. + * + * We'll use this method to register event listeners on a Node instance as + * suggested in the beta documentation... + * + * TODO: + * + * - Find a way to avoid needing to declare the called methods "public" + * as registering the event listeners *inside* this methods does not give + * us an object context. + * + * Events: + * + * 1. "creating": Before creating a new Node we'll assign a default value + * for the left and right indexes. + * + * 2. "saving": Before saving, we'll perform a check to see if we have to + * move to another parent. + * + * 3. "saved": Move to the new parent after saving if needed and re-set + * depth. + * + * 4. "deleting": Before delete we should prune all children and update + * the left and right indexes for the remaining nodes. + * + * 5. (optional) "restoring": Before a soft-delete node restore operation, + * shift its siblings. + * + * 6. (optional) "restore": After having restored a soft-deleted node, + * restore all of its descendants. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + static::flushModelEvents(); + } + + public static function flushModelEvents() + { + static::creating(function ($node) { + $node->setDefaultLeftAndRight(); + }); + + static::saving(function ($node) { + $node->storeNewParent(); + }); + + static::saved(function ($node) { + $node->moveToNewParent(); + $node->setDepth(); + }); + + static::deleting(function ($node) { + $node->destroyDescendants(); + }); + + if (static::softDeletesEnabled()) { + static::restoring(function ($node) { + $node->shiftSiblingsForRestore(); + }); + + static::restored(function ($node) { + $node->restoreDescendants(); + }); + } + } + + /** + * Get the parent column name. + * + * @return string + */ + public function getParentColumnName() + { + return $this->parentColumn; + } + + /** + * Get the table qualified parent column name. + * + * @return string + */ + public function getQualifiedParentColumnName() + { + return $this->getTable().'.'.$this->getParentColumnName(); + } + + /** + * Get the value of the models "parent_id" field. + * + * @return int + */ + public function getParentId() + { + return $this->getAttribute($this->getparentColumnName()); + } + + /** + * Get the "left" field column name. + * + * @return string + */ + public function getLeftColumnName() + { + return $this->leftColumn; + } + + /** + * Get the table qualified "left" field column name. + * + * @return string + */ + public function getQualifiedLeftColumnName() + { + return $this->getTable().'.'.$this->getLeftColumnName(); + } + + /** + * Get the value of the model's "left" field. + * + * @return int + */ + public function getLeft() + { + return $this->getAttribute($this->getLeftColumnName()); + } + + /** + * Get the "right" field column name. + * + * @return string + */ + public function getRightColumnName() + { + return $this->rightColumn; + } + + /** + * Get the table qualified "right" field column name. + * + * @return string + */ + public function getQualifiedRightColumnName() + { + return $this->getTable().'.'.$this->getRightColumnName(); + } + + /** + * Get the value of the model's "right" field. + * + * @return int + */ + public function getRight() + { + return $this->getAttribute($this->getRightColumnName()); + } + + /** + * Get the "depth" field column name. + * + * @return string + */ + public function getDepthColumnName() + { + return $this->depthColumn; + } + + /** + * Get the table qualified "depth" field column name. + * + * @return string + */ + public function getQualifiedDepthColumnName() + { + return $this->getTable().'.'.$this->getDepthColumnName(); + } + + /** + * Get the model's "depth" value. + * + * @return int + */ + public function getDepth() + { + return $this->getAttribute($this->getDepthColumnName()); + } + + /** + * Get the "order" field column name. + * + * @return string + */ + public function getOrderColumnName() + { + return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn; + } + + /** + * Get the table qualified "order" field column name. + * + * @return string + */ + public function getQualifiedOrderColumnName() + { + return $this->getTable().'.'.$this->getOrderColumnName(); + } + + /** + * Get the model's "order" value. + * + * @return mixed + */ + public function getOrder() + { + return $this->getAttribute($this->getOrderColumnName()); + } + + /** + * Get the column names which define our scope. + * + * @return array + */ + public function getScopedColumns() + { + return (array) $this->scoped; + } + + /** + * Get the qualified column names which define our scope. + * + * @return array + */ + public function getQualifiedScopedColumns() + { + if (! $this->isScoped()) { + return $this->getScopedColumns(); + } + + $prefix = $this->getTable().'.'; + + return array_map(function ($c) use ($prefix) { + return $prefix.$c; + }, $this->getScopedColumns()); + } + + /** + * Returns wether this particular node instance is scoped by certain fields + * or not. + * + * @return bool + */ + public function isScoped() + { + return (bool) (count($this->getScopedColumns()) > 0); + } + + /** + * Parent relation (self-referential) 1-1. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function parent() + { + return $this->belongsTo(get_class($this), $this->getParentColumnName()); + } + + /** + * Children relation (self-referential) 1-N. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function children() + { + return $this->hasMany(get_class($this), $this->getParentColumnName()) + ->orderBy($this->getOrderColumnName()); + } + + /** + * Get a new "scoped" query builder for the Node's model. + * + * @param bool $excludeDeleted + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function newNestedSetQuery($excludeDeleted = true) + { + $builder = $this->newQuery($excludeDeleted)->orderBy($this->getQualifiedOrderColumnName()); + + if ($this->isScoped()) { + foreach ($this->scoped as $scopeFld) { + $builder->where($scopeFld, '=', $this->$scopeFld); + } + } + + return $builder; + } + + /** + * Overload new Collection. + * + * @param array $models + * @return \Baum\Extensions\Eloquent\Collection + */ + public function newCollection(array $models = []) + { + return new Collection($models); + } + + /** + * Get all of the nodes from the database. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection|static[] + */ + public static function all($columns = ['*']) + { + $instance = new static; + + return $instance->newQuery() + ->orderBy($instance->getQualifiedOrderColumnName()) + ->get(); + } + + /** + * Returns the first root node. + * + * @return NestedSet + */ + public static function root() + { + return static::roots()->first(); + } + + /** + * Static query scope. Returns a query scope with all root nodes. + * + * @return \Illuminate\Database\Query\Builder + */ + public static function roots() + { + $instance = new static; + + return $instance->newQuery() + ->whereNull($instance->getParentColumnName()) + ->orderBy($instance->getQualifiedOrderColumnName()); + } + + /** + * Static query scope. Returns a query scope with all nodes which are at + * the end of a branch. + * + * @return \Illuminate\Database\Query\Builder + */ + public static function allLeaves() + { + $instance = new static; + + $grammar = $instance->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); + + return $instance->newQuery() + ->whereRaw($rgtCol.' - '.$lftCol.' = 1') + ->orderBy($instance->getQualifiedOrderColumnName()); + } + + /** + * Static query scope. Returns a query scope with all nodes which are at + * the middle of a branch (not root and not leaves). + * + * @return \Illuminate\Database\Query\Builder + */ + public static function allTrunks() + { + $instance = new static; + + $grammar = $instance->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); + + return $instance->newQuery() + ->whereNotNull($instance->getParentColumnName()) + ->whereRaw($rgtCol.' - '.$lftCol.' != 1') + ->orderBy($instance->getQualifiedOrderColumnName()); + } + + /** + * Checks wether the underlying Nested Set structure is valid. + * + * @return bool + */ + public static function isValidNestedSet() + { + $validator = new SetValidator(new static); + + return $validator->passes(); + } + + /** + * Rebuilds the structure of the current Nested Set. + * + * @param bool $force + * @return void + */ + public static function rebuild($force = false) + { + $builder = new SetBuilder(new static); + + $builder->rebuild($force); + } + + /** + * Maps the provided tree structure into the database. + * + * @param array|\Illuminate\Support\Contracts\ArrayableInterface + * @return bool + */ + public static function buildTree($nodeList) + { + return with(new static)->makeTree($nodeList); + } + + /** + * Query scope which extracts a certain node object from the current query + * expression. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeWithoutNode($query, $node) + { + return $query->where($node->getKeyName(), '!=', $node->getKey()); + } + + /** + * Extracts current node (self) from current query expression. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeWithoutSelf($query) + { + return $this->scopeWithoutNode($query, $this); + } + + /** + * Extracts first root (from the current node p-o-v) from current query + * expression. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeWithoutRoot($query) + { + return $this->scopeWithoutNode($query, $this->getRoot()); + } + + /** + * Provides a depth level limit for the query. + * + * @param query \Illuminate\Database\Query\Builder + * @param limit integer + * @return \Illuminate\Database\Query\Builder + */ + public function scopeLimitDepth($query, $limit) + { + $depth = $this->exists ? $this->getDepth() : $this->getLevel(); + $max = $depth + $limit; + $scopes = [$depth, $max]; + + return $query->whereBetween($this->getDepthColumnName(), [min($scopes), max($scopes)]); + } + + /** + * Returns true if this is a root node. + * + * @return bool + */ + public function isRoot() + { + return is_null($this->getParentId()); + } + + /** + * Returns true if this is a leaf node (end of a branch). + * + * @return bool + */ + public function isLeaf() + { + return $this->exists && ($this->getRight() - $this->getLeft() == 1); + } + + /** + * Returns true if this is a trunk node (not root or leaf). + * + * @return bool + */ + public function isTrunk() + { + return ! $this->isRoot() && ! $this->isLeaf(); + } + + /** + * Returns true if this is a child node. + * + * @return bool + */ + public function isChild() + { + return ! $this->isRoot(); + } + + /** + * Returns the root node starting at the current node. + * + * @return NestedSet + */ + public function getRoot() + { + if ($this->exists) { + return $this->ancestorsAndSelf()->whereNull($this->getParentColumnName())->first(); + } else { + $parentId = $this->getParentId(); + + if (! is_null($parentId) && $currentParent = static::find($parentId)) { + return $currentParent->getRoot(); + } else { + return $this; + } + } + } + + /** + * Instance scope which targes all the ancestor chain nodes including + * the current one. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function ancestorsAndSelf() + { + return $this->newNestedSetQuery() + ->where($this->getLeftColumnName(), '<=', $this->getLeft()) + ->where($this->getRightColumnName(), '>=', $this->getRight()); + } + + /** + * Get all the ancestor chain from the database including the current node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsAndSelf($columns = ['*']) + { + return $this->ancestorsAndSelf()->get($columns); + } + + /** + * Get all the ancestor chain from the database including the current node + * but without the root node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsAndSelfWithoutRoot($columns = ['*']) + { + return $this->ancestorsAndSelf()->withoutRoot()->get($columns); + } + + /** + * Instance scope which targets all the ancestor chain nodes excluding + * the current one. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function ancestors() + { + return $this->ancestorsAndSelf()->withoutSelf(); + } + + /** + * Get all the ancestor chain from the database excluding the current node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestors($columns = ['*']) + { + return $this->ancestors()->get($columns); + } + + /** + * Get all the ancestor chain from the database excluding the current node + * and the root node (from the current node's perspective). + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsWithoutRoot($columns = ['*']) + { + return $this->ancestors()->withoutRoot()->get($columns); + } + + /** + * Instance scope which targets all children of the parent, including self. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function siblingsAndSelf() + { + return $this->newNestedSetQuery() + ->where($this->getParentColumnName(), $this->getParentId()); + } + + /** + * Get all children of the parent, including self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getSiblingsAndSelf($columns = ['*']) + { + return $this->siblingsAndSelf()->get($columns); + } + + /** + * Instance scope targeting all children of the parent, except self. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function siblings() + { + return $this->siblingsAndSelf()->withoutSelf(); + } + + /** + * Return all children of the parent, except self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getSiblings($columns = ['*']) + { + return $this->siblings()->get($columns); + } + + /** + * Instance scope targeting all of its nested children which do not have + * children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function leaves() + { + $grammar = $this->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); + + return $this->descendants() + ->whereRaw($rgtCol.' - '.$lftCol.' = 1'); + } + + /** + * Return all of its nested children which do not have children. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getLeaves($columns = ['*']) + { + return $this->leaves()->get($columns); + } + + /** + * Instance scope targeting all of its nested children which are between the + * root and the leaf nodes (middle branch). + * + * @return \Illuminate\Database\Query\Builder + */ + public function trunks() + { + $grammar = $this->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); + + return $this->descendants() + ->whereNotNull($this->getQualifiedParentColumnName()) + ->whereRaw($rgtCol.' - '.$lftCol.' != 1'); + } + + /** + * Return all of its nested children which are trunks. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getTrunks($columns = ['*']) + { + return $this->trunks()->get($columns); + } + + /** + * Scope targeting itself and all of its nested children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function descendantsAndSelf() + { + return $this->newNestedSetQuery() + ->where($this->getLeftColumnName(), '>=', $this->getLeft()) + ->where($this->getLeftColumnName(), '<', $this->getRight()); + } + + /** + * Retrieve all nested children an self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDescendantsAndSelf($columns = ['*']) + { + if (is_array($columns)) { + return $this->descendantsAndSelf()->get($columns); + } + + $arguments = func_get_args(); + + $limit = intval(array_shift($arguments)); + $columns = array_shift($arguments) ?: ['*']; + + return $this->descendantsAndSelf()->limitDepth($limit)->get($columns); + } + + /** + * Set of all children & nested children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function descendants() + { + return $this->descendantsAndSelf()->withoutSelf(); + } + + /** + * Retrieve all of its children & nested children. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDescendants($columns = ['*']) + { + if (is_array($columns)) { + return $this->descendants()->get($columns); + } + + $arguments = func_get_args(); + + $limit = intval(array_shift($arguments)); + $columns = array_shift($arguments) ?: ['*']; + + return $this->descendants()->limitDepth($limit)->get($columns); + } + + /** + * Set of "immediate" descendants (aka children), alias for the children relation. + * + * @return \Illuminate\Database\Query\Builder + */ + public function immediateDescendants() + { + return $this->children(); + } + + /** + * Retrive all of its "immediate" descendants. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getImmediateDescendants($columns = ['*']) + { + return $this->children()->get($columns); + } + + /** + * Returns the level of this node in the tree. + * Root level is 0. + * + * @return int + */ + public function getLevel() + { + if (is_null($this->getParentId())) { + return 0; + } + + return $this->computeLevel(); + } + + /** + * Returns true if node is a descendant. + * + * @param NestedSet + * @return bool + */ + public function isDescendantOf($other) + { + return + $this->getLeft() > $other->getLeft() && + $this->getLeft() < $other->getRight() && + $this->inSameScope($other); + } + + /** + * Returns true if node is self or a descendant. + * + * @param NestedSet + * @return bool + */ + public function isSelfOrDescendantOf($other) + { + return + $this->getLeft() >= $other->getLeft() && + $this->getLeft() < $other->getRight() && + $this->inSameScope($other); + } + + /** + * Returns true if node is an ancestor. + * + * @param NestedSet + * @return bool + */ + public function isAncestorOf($other) + { + return + $this->getLeft() < $other->getLeft() && + $this->getRight() > $other->getLeft() && + $this->inSameScope($other); + } + + /** + * Returns true if node is self or an ancestor. + * + * @param NestedSet + * @return bool + */ + public function isSelfOrAncestorOf($other) + { + return + $this->getLeft() <= $other->getLeft() && + $this->getRight() > $other->getLeft() && + $this->inSameScope($other); + } + + /** + * Returns the first sibling to the left. + * + * @return NestedSet + */ + public function getLeftSibling() + { + return $this->siblings() + ->where($this->getLeftColumnName(), '<', $this->getLeft()) + ->orderBy($this->getOrderColumnName(), 'desc') + ->get() + ->last(); + } + + /** + * Returns the first sibling to the right. + * + * @return NestedSet + */ + public function getRightSibling() + { + return $this->siblings() + ->where($this->getLeftColumnName(), '>', $this->getLeft()) + ->first(); + } + + /** + * Find the left sibling and move to left of it. + * + * @return \Baum\Node + */ + public function moveLeft() + { + return $this->moveToLeftOf($this->getLeftSibling()); + } + + /** + * Find the right sibling and move to the right of it. + * + * @return \Baum\Node + */ + public function moveRight() + { + return $this->moveToRightOf($this->getRightSibling()); + } + + /** + * Move to the node to the left of ... + * + * @return \Baum\Node + */ + public function moveToLeftOf($node) + { + return $this->moveTo($node, 'left'); + } + + /** + * Move to the node to the right of ... + * + * @return \Baum\Node + */ + public function moveToRightOf($node) + { + return $this->moveTo($node, 'right'); + } + + /** + * Alias for moveToRightOf. + * + * @return \Baum\Node + */ + public function makeNextSiblingOf($node) + { + return $this->moveToRightOf($node); + } + + /** + * Alias for moveToRightOf. + * + * @return \Baum\Node + */ + public function makeSiblingOf($node) + { + return $this->moveToRightOf($node); + } + + /** + * Alias for moveToLeftOf. + * + * @return \Baum\Node + */ + public function makePreviousSiblingOf($node) + { + return $this->moveToLeftOf($node); + } + + /** + * Make the node a child of ... + * + * @return \Baum\Node + */ + public function makeChildOf($node) + { + return $this->moveTo($node, 'child'); + } + + /** + * Make the node the first child of ... + * + * @return \Baum\Node + */ + public function makeFirstChildOf($node) + { + if ($node->children()->count() == 0) { + return $this->makeChildOf($node); + } + + return $this->moveToLeftOf($node->children()->first()); + } + + /** + * Make the node the last child of ... + * + * @return \Baum\Node + */ + public function makeLastChildOf($node) + { + return $this->makeChildOf($node); + } + + /** + * Make current node a root node. + * + * @return \Baum\Node + */ + public function makeRoot() + { + return $this->moveTo($this, 'root'); + } + + /** + * Equals? + * + * @param \Baum\Node + * @return bool + */ + public function equals($node) + { + return $this == $node; + } + + /** + * Checkes if the given node is in the same scope as the current one. + * + * @param \Baum\Node + * @return bool + */ + public function inSameScope($other) + { + foreach ($this->getScopedColumns() as $fld) { + if ($this->$fld != $other->$fld) { + return false; + } + } + + return true; + } + + /** + * Checks wether the given node is a descendant of itself. Basically, whether + * its in the subtree defined by the left and right indices. + * + * @param \Baum\Node + * @return bool + */ + public function insideSubtree($node) + { + return + $this->getLeft() >= $node->getLeft() && + $this->getLeft() <= $node->getRight() && + $this->getRight() >= $node->getLeft() && + $this->getRight() <= $node->getRight(); + } + + /** + * Sets default values for left and right fields. + * + * @return void + */ + public function setDefaultLeftAndRight() + { + $withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), + 'desc')->take(1)->sharedLock()->first(); + + $maxRgt = 0; + if (! is_null($withHighestRight)) { + $maxRgt = $withHighestRight->getRight(); + } + + $this->setAttribute($this->getLeftColumnName(), $maxRgt + 1); + $this->setAttribute($this->getRightColumnName(), $maxRgt + 2); + } + + /** + * Store the parent_id if the attribute is modified so as we are able to move + * the node to this new parent after saving. + * + * @return void + */ + public function storeNewParent() + { + if ($this->isDirty($this->getParentColumnName()) && ($this->exists || ! $this->isRoot())) { + static::$moveToNewParentId = $this->getParentId(); + } else { + static::$moveToNewParentId = false; + } + } + + /** + * Move to the new parent if appropiate. + * + * @return void + */ + public function moveToNewParent() + { + $pid = static::$moveToNewParentId; + + if (is_null($pid)) { + $this->makeRoot(); + } elseif ($pid !== false) { + $this->makeChildOf($pid); + } + } + + /** + * Sets the depth attribute. + * + * @return \Baum\Node + */ + public function setDepth() + { + $self = $this; + + $this->getConnection()->transaction(function () use ($self) { + $self->reload(); + + $level = $self->getLevel(); + + $self->newNestedSetQuery()->where($self->getKeyName(), '=', + $self->getKey())->update([$self->getDepthColumnName() => $level]); + $self->setAttribute($self->getDepthColumnName(), $level); + }); + + return $this; + } + + /** + * Sets the depth attribute for the current node and all of its descendants. + * + * @return \Baum\Node + */ + public function setDepthWithSubtree() + { + $self = $this; + + $this->getConnection()->transaction(function () use ($self) { + $self->reload(); + + $self->descendantsAndSelf()->select($self->getKeyName())->lockForUpdate()->get(); + + $oldDepth = ! is_null($self->getDepth()) ? $self->getDepth() : 0; + + $newDepth = $self->getLevel(); + + $self->newNestedSetQuery()->where($self->getQualifiedKeyName(), '=', + $self->getKey())->update([$self->getDepthColumnName() => $newDepth]); + $self->setAttribute($self->getDepthColumnName(), $newDepth); + + $diff = $newDepth - $oldDepth; + if (! $self->isLeaf() && $diff != 0) { + $self->descendants()->increment($self->getDepthColumnName(), $diff); + } + }); + return $this; - } - } - } - - /** - * Instance scope which targes all the ancestor chain nodes including - * the current one. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function ancestorsAndSelf() { - return $this->newNestedSetQuery() - ->where($this->getLeftColumnName(), '<=', $this->getLeft()) - ->where($this->getRightColumnName(), '>=', $this->getRight()); - } - - /** - * Get all the ancestor chain from the database including the current node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsAndSelf($columns = array('*')) { - return $this->ancestorsAndSelf()->get($columns); - } - - /** - * Get all the ancestor chain from the database including the current node - * but without the root node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsAndSelfWithoutRoot($columns = array('*')) { - return $this->ancestorsAndSelf()->withoutRoot()->get($columns); - } - - /** - * Instance scope which targets all the ancestor chain nodes excluding - * the current one. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function ancestors() { - return $this->ancestorsAndSelf()->withoutSelf(); - } - - /** - * Get all the ancestor chain from the database excluding the current node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestors($columns = array('*')) { - return $this->ancestors()->get($columns); - } - - /** - * Get all the ancestor chain from the database excluding the current node - * and the root node (from the current node's perspective). - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsWithoutRoot($columns = array('*')) { - return $this->ancestors()->withoutRoot()->get($columns); - } - - /** - * Instance scope which targets all children of the parent, including self. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function siblingsAndSelf() { - return $this->newNestedSetQuery() - ->where($this->getParentColumnName(), $this->getParentId()); - } - - /** - * Get all children of the parent, including self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getSiblingsAndSelf($columns = array('*')) { - return $this->siblingsAndSelf()->get($columns); - } - - /** - * Instance scope targeting all children of the parent, except self. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function siblings() { - return $this->siblingsAndSelf()->withoutSelf(); - } - - /** - * Return all children of the parent, except self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getSiblings($columns = array('*')) { - return $this->siblings()->get($columns); - } - - /** - * Instance scope targeting all of its nested children which do not have - * children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function leaves() { - $grammar = $this->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); - - return $this->descendants() - ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1'); - } - - /** - * Return all of its nested children which do not have children. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getLeaves($columns = array('*')) { - return $this->leaves()->get($columns); - } - - /** - * Instance scope targeting all of its nested children which are between the - * root and the leaf nodes (middle branch). - * - * @return \Illuminate\Database\Query\Builder - */ - public function trunks() { - $grammar = $this->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); - - return $this->descendants() - ->whereNotNull($this->getQualifiedParentColumnName()) - ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1'); - } - - /** - * Return all of its nested children which are trunks. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getTrunks($columns = array('*')) { - return $this->trunks()->get($columns); - } - - /** - * Scope targeting itself and all of its nested children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function descendantsAndSelf() { - return $this->newNestedSetQuery() - ->where($this->getLeftColumnName(), '>=', $this->getLeft()) - ->where($this->getLeftColumnName(), '<', $this->getRight()); - } - - /** - * Retrieve all nested children an self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getDescendantsAndSelf($columns = array('*')) { - if ( is_array($columns) ) - return $this->descendantsAndSelf()->get($columns); - - $arguments = func_get_args(); - - $limit = intval(array_shift($arguments)); - $columns = array_shift($arguments) ?: array('*'); - - return $this->descendantsAndSelf()->limitDepth($limit)->get($columns); - } - - /** - * Set of all children & nested children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function descendants() { - return $this->descendantsAndSelf()->withoutSelf(); - } - - /** - * Retrieve all of its children & nested children. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getDescendants($columns = array('*')) { - if ( is_array($columns) ) - return $this->descendants()->get($columns); - - $arguments = func_get_args(); - - $limit = intval(array_shift($arguments)); - $columns = array_shift($arguments) ?: array('*'); - - return $this->descendants()->limitDepth($limit)->get($columns); - } - - /** - * Set of "immediate" descendants (aka children), alias for the children relation. - * - * @return \Illuminate\Database\Query\Builder - */ - public function immediateDescendants() { - return $this->children(); - } - - /** - * Retrive all of its "immediate" descendants. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getImmediateDescendants($columns = array('*')) { - return $this->children()->get($columns); - } - - /** - * Returns the level of this node in the tree. - * Root level is 0. - * - * @return int - */ - public function getLevel() { - if ( is_null($this->getParentId()) ) - return 0; - - return $this->computeLevel(); - } - - /** - * Returns true if node is a descendant. - * - * @param NestedSet - * @return boolean - */ - public function isDescendantOf($other) { - return ( - $this->getLeft() > $other->getLeft() && - $this->getLeft() < $other->getRight() && - $this->inSameScope($other) - ); - } - - /** - * Returns true if node is self or a descendant. - * - * @param NestedSet - * @return boolean - */ - public function isSelfOrDescendantOf($other) { - return ( - $this->getLeft() >= $other->getLeft() && - $this->getLeft() < $other->getRight() && - $this->inSameScope($other) - ); - } - - /** - * Returns true if node is an ancestor. - * - * @param NestedSet - * @return boolean - */ - public function isAncestorOf($other) { - return ( - $this->getLeft() < $other->getLeft() && - $this->getRight() > $other->getLeft() && - $this->inSameScope($other) - ); - } - - /** - * Returns true if node is self or an ancestor. - * - * @param NestedSet - * @return boolean - */ - public function isSelfOrAncestorOf($other) { - return ( - $this->getLeft() <= $other->getLeft() && - $this->getRight() > $other->getLeft() && - $this->inSameScope($other) - ); - } - - /** - * Returns the first sibling to the left. - * - * @return NestedSet - */ - public function getLeftSibling() { - return $this->siblings() - ->where($this->getLeftColumnName(), '<', $this->getLeft()) - ->orderBy($this->getOrderColumnName(), 'desc') - ->get() - ->last(); - } - - /** - * Returns the first sibling to the right. - * - * @return NestedSet - */ - public function getRightSibling() { - return $this->siblings() - ->where($this->getLeftColumnName(), '>', $this->getLeft()) - ->first(); - } - - /** - * Find the left sibling and move to left of it. - * - * @return \Baum\Node - */ - public function moveLeft() { - return $this->moveToLeftOf($this->getLeftSibling()); - } - - /** - * Find the right sibling and move to the right of it. - * - * @return \Baum\Node - */ - public function moveRight() { - return $this->moveToRightOf($this->getRightSibling()); - } - - /** - * Move to the node to the left of ... - * - * @return \Baum\Node - */ - public function moveToLeftOf($node) { - return $this->moveTo($node, 'left'); - } - - /** - * Move to the node to the right of ... - * - * @return \Baum\Node - */ - public function moveToRightOf($node) { - return $this->moveTo($node, 'right'); - } - - /** - * Alias for moveToRightOf - * - * @return \Baum\Node - */ - public function makeNextSiblingOf($node) { - return $this->moveToRightOf($node); - } - - /** - * Alias for moveToRightOf - * - * @return \Baum\Node - */ - public function makeSiblingOf($node) { - return $this->moveToRightOf($node); - } - - /** - * Alias for moveToLeftOf - * - * @return \Baum\Node - */ - public function makePreviousSiblingOf($node) { - return $this->moveToLeftOf($node); - } - - /** - * Make the node a child of ... - * - * @return \Baum\Node - */ - public function makeChildOf($node) { - return $this->moveTo($node, 'child'); - } - - /** - * Make the node the first child of ... - * - * @return \Baum\Node - */ - public function makeFirstChildOf($node) { - if ( $node->children()->count() == 0 ) - return $this->makeChildOf($node); - - return $this->moveToLeftOf($node->children()->first()); - } - - /** - * Make the node the last child of ... - * - * @return \Baum\Node - */ - public function makeLastChildOf($node) { - return $this->makeChildOf($node); - } - - /** - * Make current node a root node. - * - * @return \Baum\Node - */ - public function makeRoot() { - return $this->moveTo($this, 'root'); - } - - /** - * Equals? - * - * @param \Baum\Node - * @return boolean - */ - public function equals($node) { - return ($this == $node); - } - - /** - * Checkes if the given node is in the same scope as the current one. - * - * @param \Baum\Node - * @return boolean - */ - public function inSameScope($other) { - foreach($this->getScopedColumns() as $fld) { - if ( $this->$fld != $other->$fld ) return false; - } - - return true; - } - - /** - * Checks wether the given node is a descendant of itself. Basically, whether - * its in the subtree defined by the left and right indices. - * - * @param \Baum\Node - * @return boolean - */ - public function insideSubtree($node) { - return ( - $this->getLeft() >= $node->getLeft() && - $this->getLeft() <= $node->getRight() && - $this->getRight() >= $node->getLeft() && - $this->getRight() <= $node->getRight() - ); - } - - /** - * Sets default values for left and right fields. - * - * @return void - */ - public function setDefaultLeftAndRight() { - $withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), 'desc')->take(1)->sharedLock()->first(); - - $maxRgt = 0; - if ( !is_null($withHighestRight) ) $maxRgt = $withHighestRight->getRight(); - - $this->setAttribute($this->getLeftColumnName() , $maxRgt + 1); - $this->setAttribute($this->getRightColumnName() , $maxRgt + 2); - } - - /** - * Store the parent_id if the attribute is modified so as we are able to move - * the node to this new parent after saving. - * - * @return void - */ - public function storeNewParent() { - if ( $this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot()) ) - static::$moveToNewParentId = $this->getParentId(); - else - static::$moveToNewParentId = FALSE; - } - - /** - * Move to the new parent if appropiate. - * - * @return void - */ - public function moveToNewParent() { - $pid = static::$moveToNewParentId; - - if ( is_null($pid) ) - $this->makeRoot(); - else if ( $pid !== FALSE ) - $this->makeChildOf($pid); - } - - /** - * Sets the depth attribute - * - * @return \Baum\Node - */ - public function setDepth() { - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->reload(); - - $level = $self->getLevel(); - - $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $level)); - $self->setAttribute($self->getDepthColumnName(), $level); - }); - - return $this; - } - - /** - * Sets the depth attribute for the current node and all of its descendants. - * - * @return \Baum\Node - */ - public function setDepthWithSubtree() { - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->reload(); - - $self->descendantsAndSelf()->select($self->getKeyName())->lockForUpdate()->get(); - - $oldDepth = !is_null($self->getDepth()) ? $self->getDepth() : 0; - - $newDepth = $self->getLevel(); - - $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $newDepth)); - $self->setAttribute($self->getDepthColumnName(), $newDepth); - - $diff = $newDepth - $oldDepth; - if ( !$self->isLeaf() && $diff != 0 ) - $self->descendants()->increment($self->getDepthColumnName(), $diff); - }); - - return $this; - } - - /** - * Prunes a branch off the tree, shifting all the elements on the right - * back to the left so the counts work. - * - * @return void; - */ - public function destroyDescendants() { - if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return; - - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->reload(); - - $lftCol = $self->getLeftColumnName(); - $rgtCol = $self->getRightColumnName(); - $lft = $self->getLeft(); - $rgt = $self->getRight(); - - // Apply a lock to the rows which fall past the deletion point - $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->select($self->getKeyName())->lockForUpdate()->get(); - - // Prune children - $self->newNestedSetQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); - - // Update left and right indexes for the remaining nodes - $diff = $rgt - $lft + 1; - - $self->newNestedSetQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); - $self->newNestedSetQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); - }); - } - - /** - * "Makes room" for the the current node between its siblings. - * - * @return void - */ - public function shiftSiblingsForRestore() { - if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return; - - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $lftCol = $self->getLeftColumnName(); - $rgtCol = $self->getRightColumnName(); - $lft = $self->getLeft(); - $rgt = $self->getRight(); - - $diff = $rgt - $lft + 1; - - $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->increment($lftCol, $diff); - $self->newNestedSetQuery()->where($rgtCol, '>=', $lft)->increment($rgtCol, $diff); - }); - } - - /** - * Restores all of the current node's descendants. - * - * @return void - */ - public function restoreDescendants() { - if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return; - - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->newNestedSetQuery() - ->withTrashed() - ->where($self->getLeftColumnName(), '>', $self->getLeft()) - ->where($self->getRightColumnName(), '<', $self->getRight()) - ->update(array( - $self->getDeletedAtColumn() => null, - $self->getUpdatedAtColumn() => $self->{$self->getUpdatedAtColumn()} - )); - }); - } - - /** - * Return an key-value array indicating the node's depth with $seperator - * - * @return Array - */ - public static function getNestedList($column, $key = null, $seperator = ' ') { - $instance = new static; - - $key = $key ?: $instance->getKeyName(); - $depthColumn = $instance->getDepthColumnName(); - - $nodes = $instance->newNestedSetQuery()->get()->toArray(); - - return array_combine(array_map(function($node) use($key) { - return $node[$key]; - }, $nodes), array_map(function($node) use($seperator, $depthColumn, $column) { - return str_repeat($seperator, $node[$depthColumn]) . $node[$column]; - }, $nodes)); - } - - /** - * Maps the provided tree structure into the database using the current node - * as the parent. The provided tree structure will be inserted/updated as the - * descendancy subtree of the current node instance. - * - * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean - */ - public function makeTree($nodeList) { - $mapper = new SetMapper($this); - - return $mapper->map($nodeList); - } - - /** - * Main move method. Here we handle all node movements with the corresponding - * lft/rgt index updates. - * - * @param Baum\Node|int $target - * @param string $position - * @return \Baum\Node - */ - protected function moveTo($target, $position) { - return Move::to($this, $target, $position); - } - - /** - * Compute current node level. If could not move past ourseleves return - * our ancestor count, otherwhise get the first parent level + the computed - * nesting. - * - * @return integer - */ - protected function computeLevel() { - list($node, $nesting) = $this->determineDepth($this); - - if ( $node->equals($this) ) - return $this->ancestors()->count(); - - return $node->getLevel() + $nesting; - } - - /** - * Return an array with the last node we could reach and its nesting level - * - * @param Baum\Node $node - * @param integer $nesting - * @return array - */ - protected function determineDepth($node, $nesting = 0) { - // Traverse back up the ancestry chain and add to the nesting level count - while( $parent = $node->parent()->first() ) { - $nesting = $nesting + 1; - - $node = $parent; - } - - return array($node, $nesting); - } + } + + /** + * Prunes a branch off the tree, shifting all the elements on the right + * back to the left so the counts work. + * + * @return void; + */ + public function destroyDescendants() + { + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $self = $this; + + $this->getConnection()->transaction(function () use ($self) { + $self->reload(); + + $lftCol = $self->getLeftColumnName(); + $rgtCol = $self->getRightColumnName(); + $lft = $self->getLeft(); + $rgt = $self->getRight(); + + // Apply a lock to the rows which fall past the deletion point + $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->select($self->getKeyName())->lockForUpdate()->get(); + + // Prune children + $self->newNestedSetQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); + + // Update left and right indexes for the remaining nodes + $diff = $rgt - $lft + 1; + + $self->newNestedSetQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); + $self->newNestedSetQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); + }); + } + /** + * "Makes room" for the the current node between its siblings. + * + * @return void + */ + public function shiftSiblingsForRestore() + { + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $self = $this; + + $this->getConnection()->transaction(function () use ($self) { + $lftCol = $self->getLeftColumnName(); + $rgtCol = $self->getRightColumnName(); + $lft = $self->getLeft(); + $rgt = $self->getRight(); + + $diff = $rgt - $lft + 1; + + $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->increment($lftCol, $diff); + $self->newNestedSetQuery()->where($rgtCol, '>=', $lft)->increment($rgtCol, $diff); + }); + } + + /** + * Restores all of the current node's descendants. + * + * @return void + */ + public function restoreDescendants() + { + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $self = $this; + + $this->getConnection()->transaction(function () use ($self) { + $self->newNestedSetQuery() + ->withTrashed() + ->where($self->getLeftColumnName(), '>', $self->getLeft()) + ->where($self->getRightColumnName(), '<', $self->getRight()) + ->update([ + $self->getDeletedAtColumn() => null, + $self->getUpdatedAtColumn() => $self->{$self->getUpdatedAtColumn()}, + ]); + }); + } + + /** + * Return an key-value array indicating the node's depth with $seperator. + * + * @return array + */ + public static function getNestedList($column, $key = null, $seperator = ' ') + { + $instance = new static; + + $key = $key ?: $instance->getKeyName(); + $depthColumn = $instance->getDepthColumnName(); + + $nodes = $instance->newNestedSetQuery()->get()->toArray(); + + return array_combine(array_map(function ($node) use ($key) { + return $node[$key]; + }, $nodes), array_map(function ($node) use ($seperator, $depthColumn, $column) { + return str_repeat($seperator, $node[$depthColumn]).$node[$column]; + }, $nodes)); + } + + /** + * Maps the provided tree structure into the database using the current node + * as the parent. The provided tree structure will be inserted/updated as the + * descendancy subtree of the current node instance. + * + * @param array|\Illuminate\Support\Contracts\ArrayableInterface + * @return bool + */ + public function makeTree($nodeList) + { + $mapper = new SetMapper($this); + + return $mapper->map($nodeList); + } + + /** + * Main move method. Here we handle all node movements with the corresponding + * lft/rgt index updates. + * + * @param \Baum\Node|int $target + * @param string $position + * @return \Baum\Node + */ + protected function moveTo($target, $position) + { + return Move::to($this, $target, $position); + } + + /** + * Compute current node level. If could not move past ourseleves return + * our ancestor count, otherwhise get the first parent level + the computed + * nesting. + * + * @return int + */ + protected function computeLevel() + { + list($node, $nesting) = $this->determineDepth($this); + + if ($node->equals($this)) { + return $this->ancestors()->count(); + } + + return $node->getLevel() + $nesting; + } + + /** + * Return an array with the last node we could reach and its nesting level. + * + * @param \Baum\Node $node + * @param int $nesting + * @return array + */ + protected function determineDepth($node, $nesting = 0) + { + // Traverse back up the ancestry chain and add to the nesting level count + while ($parent = $node->parent()->first()) { + $nesting = $nesting + 1; + + $node = $parent; + } + + return [$node, $nesting]; + } } diff --git a/src/Baum/SetBuilder.php b/src/Baum/SetBuilder.php index 873c238e..59504d79 100644 --- a/src/Baum/SetBuilder.php +++ b/src/Baum/SetBuilder.php @@ -1,24 +1,22 @@ node = $node; + public function __construct($node) + { + $this->node = $node; } /** @@ -37,11 +36,14 @@ public function __construct($node) { * @param bool $force * @return void */ - public function rebuild($force = false) { - $alreadyValid = forward_static_call(array(get_class($this->node), 'isValidNestedSet')); + public function rebuild($force = false) + { + $alreadyValid = forward_static_call([get_class($this->node), 'isValidNestedSet']); // Do not rebuild a valid Nested Set tree structure - if ( !$force && $alreadyValid ) return true; + if (! $force && $alreadyValid) { + return true; + } // Rebuild lefts and rights for each root node and its children (recursively). // We go by setting left (and keep track of the current left bound), then @@ -50,9 +52,10 @@ public function rebuild($force = false) { // setting the right indexes and saving the nodes... $self = $this; - $this->node->getConnection()->transaction(function() use ($self) { - foreach($self->roots() as $root) - $self->rebuildBounds($root, 0); + $this->node->getConnection()->transaction(function () use ($self) { + foreach ($self->roots() as $root) { + $self->rebuildBounds($root, 0); + } }); } @@ -61,8 +64,9 @@ public function rebuild($force = false) { * * @return Illuminate\Database\Eloquent\Collection */ - public function roots() { - return $this->node->newQuery() + public function roots() + { + return $this->node->newQuery() ->whereNull($this->node->getQualifiedParentColumnName()) ->orderBy($this->node->getQualifiedLeftColumnName()) ->orderBy($this->node->getQualifiedRightColumnName()) @@ -74,18 +78,20 @@ public function roots() { * Recompute left and right index bounds for the specified node and its * children (recursive call). Fill the depth column too. */ - public function rebuildBounds($node, $depth = 0) { - $k = $this->scopedKey($node); + public function rebuildBounds($node, $depth = 0) + { + $k = $this->scopedKey($node); - $node->setAttribute($node->getLeftColumnName(), $this->getNextBound($k)); - $node->setAttribute($node->getDepthColumnName(), $depth); + $node->setAttribute($node->getLeftColumnName(), $this->getNextBound($k)); + $node->setAttribute($node->getDepthColumnName(), $depth); - foreach($this->children($node) as $child) - $this->rebuildBounds($child, $depth + 1); + foreach ($this->children($node) as $child) { + $this->rebuildBounds($child, $depth + 1); + } - $node->setAttribute($node->getRightColumnName(), $this->getNextBound($k)); + $node->setAttribute($node->getRightColumnName(), $this->getNextBound($k)); - $node->save(); + $node->save(); } /** @@ -94,21 +100,23 @@ public function rebuildBounds($node, $depth = 0) { * @param Baum\Node $node * @return Illuminate\Database\Eloquent\Collection */ - public function children($node) { - $query = $this->node->newQuery(); + public function children($node) + { + $query = $this->node->newQuery(); - $query->where($this->node->getQualifiedParentColumnName(), '=', $node->getKey()); + $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); + 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()); + $query->orderBy($this->node->getQualifiedLeftColumnName()); + $query->orderBy($this->node->getQualifiedRightColumnName()); + $query->orderBy($this->node->getQualifiedKeyName()); - return $query->get(); + return $query->get(); } /** @@ -117,16 +125,18 @@ public function children($node) { * @param Baum\Node $node * @return array */ - protected function scopedAttributes($node) { - $keys = $this->node->getScopedColumns(); + protected function scopedAttributes($node) + { + $keys = $this->node->getScopedColumns(); - if ( count($keys) == 0 ) - return array(); + if (count($keys) == 0) { + return []; + } - $values = array_map(function($column) use ($node) { + $values = array_map(function ($column) use ($node) { return $node->getAttribute($column); }, $keys); - return array_combine($keys, $values); + return array_combine($keys, $values); } /** @@ -136,31 +146,35 @@ protected function scopedAttributes($node) { * @param Baum\Node $node * @return string */ - protected function scopedKey($node) { - $attributes = $this->scopedAttributes($node); + protected function scopedKey($node) + { + $attributes = $this->scopedAttributes($node); - $output = array(); + $output = []; - foreach($attributes as $fld => $value) - $output[] = $this->qualify($fld).'='.(is_null($value) ? 'NULL' : $value); + foreach ($attributes as $fld => $value) { + $output[] = $this->qualify($fld).'='.(is_null($value) ? 'NULL' : $value); + } // NOTE: Maybe an md5 or something would be better. Should be unique though. return implode(',', $output); } /** - * Return next index bound value for the given key (current scope identifier) + * Return next index bound value for the given key (current scope identifier). * * @param string $key - * @return integer + * @return int */ - protected function getNextBound($key) { - if ( false === array_key_exists($key, $this->bounds) ) - $this->bounds[$key] = 0; + protected function getNextBound($key) + { + if (false === array_key_exists($key, $this->bounds)) { + $this->bounds[$key] = 0; + } - $this->bounds[$key] = $this->bounds[$key] + 1; + $this->bounds[$key] = $this->bounds[$key] + 1; - return $this->bounds[$key]; + return $this->bounds[$key]; } /** @@ -168,8 +182,8 @@ protected function getNextBound($key) { * * @return string */ - protected function qualify($column) { - return $this->node->getTable() . '.' . $column; + protected function qualify($column) + { + return $this->node->getTable().'.'.$column; } - } diff --git a/src/Baum/SetMapper.php b/src/Baum/SetMapper.php index a44dabab..56948c30 100644 --- a/src/Baum/SetMapper.php +++ b/src/Baum/SetMapper.php @@ -1,21 +1,21 @@ node = $node; + public function __construct($node, $childrenKeyName = 'children') + { + $this->node = $node; - $this->childrenKeyName = $childrenKeyName; + $this->childrenKeyName = $childrenKeyName; } /** * Maps a tree structure into the database. Unguards & wraps in transaction. * * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean + * @return bool */ - public function map($nodeList) { - $self = $this; + public function map($nodeList) + { + $self = $this; - return $this->wrapInTransaction(function() use ($self, $nodeList) { - forward_static_call(array(get_class($self->node), 'unguard')); + return $this->wrapInTransaction(function () use ($self, $nodeList) { + forward_static_call([get_class($self->node), 'unguard']); $result = $self->mapTree($nodeList); - forward_static_call(array(get_class($self->node), 'reguard')); + forward_static_call([get_class($self->node), 'reguard']); return $result; }); @@ -58,107 +60,123 @@ public function map($nodeList) { * inside a transaction. * * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean + * @return bool */ - public function mapTree($nodeList) { - $tree = $nodeList instanceof ArrayableInterface ? $nodeList->toArray() : $nodeList; + public function mapTree($nodeList) + { + $tree = $nodeList instanceof ArrayableInterface ? $nodeList->toArray() : $nodeList; - $affectedKeys = array(); + $affectedKeys = []; - $result = $this->mapTreeRecursive($tree, $this->node->getKey(), $affectedKeys); + $result = $this->mapTreeRecursive($tree, $this->node->getKey(), $affectedKeys); - if ( $result && count($affectedKeys) > 0 ) - $this->deleteUnaffected($affectedKeys); + if ($result && count($affectedKeys) > 0) { + $this->deleteUnaffected($affectedKeys); + } - return $result; + return $result; } /** - * Returns the children key name to use on the mapping array + * Returns the children key name to use on the mapping array. * * @return string */ - public function getChildrenKeyName() { - return $this->childrenKeyName; + public function getChildrenKeyName() + { + return $this->childrenKeyName; } /** - * Maps a tree structure into the database + * Maps a tree structure into the database. * * @param array $tree * @param mixed $parent - * @return boolean + * @return bool */ - protected function mapTreeRecursive(array $tree, $parentKey = null, &$affectedKeys = array()) { - // For every attribute entry: We'll need to instantiate a new node either + protected function mapTreeRecursive(array $tree, $parentKey = null, &$affectedKeys = []) + { + // For every attribute entry: We'll need to instantiate a new node either // from the database (if the primary key was supplied) or a new instance. Then, // append all the remaining data attributes (including the `parent_id` if // present) and save it. Finally, tail-recurse performing the same // operations for any child node present. Setting the `parent_id` property at // each level will take care of the nesting work for us. - foreach($tree as $attributes) { - $node = $this->firstOrNew($this->getSearchAttributes($attributes)); + foreach ($tree as $attributes) { + $node = $this->firstOrNew($this->getSearchAttributes($attributes)); - $data = $this->getDataAttributes($attributes); - if ( !is_null($parentKey) ) - $data[$node->getParentColumnName()] = $parentKey; + $data = $this->getDataAttributes($attributes); + if (! is_null($parentKey)) { + $data[$node->getParentColumnName()] = $parentKey; + } - $node->fill($data); + $node->fill($data); - $result = $node->save(); + $result = $node->save(); - if ( ! $result ) return false; + if (! $result) { + return false; + } - $affectedKeys[] = $node->getKey(); + $affectedKeys[] = $node->getKey(); - if ( array_key_exists($this->getChildrenKeyName(), $attributes) ) { - $children = $attributes[$this->getChildrenKeyName()]; + if (array_key_exists($this->getChildrenKeyName(), $attributes)) { + $children = $attributes[$this->getChildrenKeyName()]; - if ( count($children) > 0 ) { - $result = $this->mapTreeRecursive($children, $node->getKey(), $affectedKeys); + if (count($children) > 0) { + $result = $this->mapTreeRecursive($children, $node->getKey(), $affectedKeys); - if ( ! $result ) return false; + if (! $result) { + return false; + } + } } - } } - return true; + return true; } - protected function getSearchAttributes($attributes) { - $searchable = array($this->node->getKeyName()); + protected function getSearchAttributes($attributes) + { + $searchable = [$this->node->getKeyName()]; - return array_only($attributes, $searchable); - } + return array_only($attributes, $searchable); + } - protected function getDataAttributes($attributes) { - $exceptions = array($this->node->getKeyName(), $this->getChildrenKeyName()); + protected function getDataAttributes($attributes) + { + $exceptions = [$this->node->getKeyName(), $this->getChildrenKeyName()]; - return array_except($attributes, $exceptions); - } - - protected function firstOrNew($attributes) { - $className = get_class($this->node); + return array_except($attributes, $exceptions); + } - if ( count($attributes) === 0 ) - return new $className; + protected function firstOrNew($attributes) + { + $className = get_class($this->node); - return forward_static_call(array($className, 'firstOrNew'), $attributes); - } + if (count($attributes) === 0) { + return new $className; + } - protected function pruneScope() { - if ( $this->node->exists ) - return $this->node->descendants(); + return forward_static_call([$className, 'firstOrNew'], $attributes); + } - return $this->node->newNestedSetQuery(); - } + protected function pruneScope() + { + if ($this->node->exists) { + return $this->node->descendants(); + } - protected function deleteUnaffected($keys = array()) { - return $this->pruneScope()->whereNotIn($this->node->getKeyName(), $keys)->delete(); - } + return $this->node->newNestedSetQuery(); + } - protected function wrapInTransaction(Closure $callback) { - return $this->node->getConnection()->transaction($callback); - } + protected function deleteUnaffected($keys = []) + { + return $this->pruneScope()->whereNotIn($this->node->getKeyName(), $keys)->delete(); + } + protected function wrapInTransaction(Closure $callback) + { + return $this->node->getConnection()->transaction($callback); + } } diff --git a/src/Baum/SetValidator.php b/src/Baum/SetValidator.php index 10d87640..72bead70 100644 --- a/src/Baum/SetValidator.php +++ b/src/Baum/SetValidator.php @@ -1,16 +1,15 @@ node = $node; + public function __construct($node) + { + $this->node = $node; } /** * Determine if the validation passes. * - * @return boolean + * @return bool */ - public function passes() { - return $this->validateBounds() && $this->validateDuplicates() && + public function passes() + { + return $this->validateBounds() && $this->validateDuplicates() && $this->validateRoots(); } /** * Determine if validation fails. * - * @return boolean + * @return bool */ - public function fails() { - return !$this->passes(); + public function fails() + { + return ! $this->passes(); } /** @@ -46,66 +48,69 @@ public function fails() { * 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 + * @return bool */ - protected function validateBounds() { - $connection = $this->node->getConnection(); - $grammar = $connection->getQueryGrammar(); + protected function validateBounds() + { + $connection = $this->node->getConnection(); + $grammar = $connection->getQueryGrammar(); - $tableName = $this->node->getTable(); - $primaryKeyName = $this->node->getKeyName(); - $parentColumn = $this->node->getQualifiedParentColumnName(); + $tableName = $this->node->getTable(); + $primaryKeyName = $this->node->getKeyName(); + $parentColumn = $this->node->getQualifiedParentColumnName(); - $lftCol = $grammar->wrap($this->node->getLeftColumnName()); - $rgtCol = $grammar->wrap($this->node->getRightColumnName()); + $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()); + $qualifiedLftCol = $grammar->wrap($this->node->getQualifiedLeftColumnName()); + $qualifiedRgtCol = $grammar->wrap($this->node->getQualifiedRightColumnName()); + $qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName()); - $whereStm = "($qualifiedLftCol IS NULL OR + $whereStm = "($qualifiedLftCol IS NULL OR $qualifiedRgtCol IS NULL OR $qualifiedLftCol >= $qualifiedRgtCol OR ($qualifiedParentCol IS NOT NULL AND ($qualifiedLftCol <= parent.$lftCol OR $qualifiedRgtCol >= parent.$rgtCol)))"; - $query = $this->node->newQuery() + $query = $this->node->newQuery() ->join($connection->raw($grammar->wrapTable($tableName).' AS parent'), $parentColumn, '=', $connection->raw('parent.'.$grammar->wrap($primaryKeyName)), 'left outer') ->whereRaw($whereStm); - return ($query->count() == 0); + return $query->count() == 0; } /** * Checks that there are no duplicates for the `lft` and `rgt` columns. * - * @return boolean + * @return bool */ - protected function validateDuplicates() { - return ( - !$this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) && - !$this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName()) - ); + protected function validateDuplicates() + { + return + ! $this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) && + ! $this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName()); } /** * For each root of the whole nested set tree structure, checks that their * `lft` and `rgt` bounds are properly set. * - * @return boolean + * @return bool */ - protected function validateRoots() { - $roots = forward_static_call(array(get_class($this->node), 'roots'))->get(); + protected function validateRoots() + { + $roots = forward_static_call([get_class($this->node), 'roots'])->get(); // If a scope is defined in the model we should check that the roots are // valid *for each* value in the scope columns. - if ( $this->node->isScoped() ) - return $this->validateRootsByScope($roots); + if ($this->node->isScoped()) { + return $this->validateRootsByScope($roots); + } - return $this->isEachRootValid($roots); + return $this->isEachRootValid($roots); } /** @@ -113,29 +118,31 @@ protected function validateRoots() { * the Nested Set scope columns into account (if appropiate). * * @param string $column - * @return boolean + * @return bool */ - protected function duplicatesExistForColumn($column) { - $connection = $this->node->getConnection(); - $grammar = $connection->getQueryGrammar(); + protected function duplicatesExistForColumn($column) + { + $connection = $this->node->getConnection(); + $grammar = $connection->getQueryGrammar(); - $columns = array_merge($this->node->getQualifiedScopedColumns(), array($column)); + $columns = array_merge($this->node->getQualifiedScopedColumns(), [$column]); - $columnsForSelect = implode(', ', array_map(function($col) use ($grammar) { + $columnsForSelect = implode(', ', array_map(function ($col) use ($grammar) { return $grammar->wrap($col); }, $columns)); - $wrappedColumn = $grammar->wrap($column); + $wrappedColumn = $grammar->wrap($column); - $query = $this->node->newQuery() + $query = $this->node->newQuery() ->select($connection->raw("$columnsForSelect, COUNT($wrappedColumn)")) ->havingRaw("COUNT($wrappedColumn) > 1"); - foreach($columns as $col) - $query->groupBy($col); + foreach ($columns as $col) { + $query->groupBy($col); + } - $result = $query->first(); + $result = $query->first(); - return !is_null($result); + return ! is_null($result); } /** @@ -143,23 +150,25 @@ protected function duplicatesExistForColumn($column) { * values (lft, rgt indexes) are less than the next. * * @param mixed $roots - * @return boolean + * @return bool */ - protected function isEachRootValid($roots) { - $left = $right = 0; + protected function isEachRootValid($roots) + { + $left = $right = 0; - foreach($roots as $root) { - $rootLeft = $root->getLeft(); - $rootRight = $root->getRight(); + foreach ($roots as $root) { + $rootLeft = $root->getLeft(); + $rootRight = $root->getRight(); - if ( !($rootLeft > $left && $rootRight > $right) ) - return false; + if (! ($rootLeft > $left && $rootRight > $right)) { + return false; + } - $left = $rootLeft; - $right = $rootRight; - } + $left = $rootLeft; + $right = $rootRight; + } - return true; + return true; } /** @@ -167,40 +176,44 @@ protected function isEachRootValid($roots) { * values (lft, rgt indexes) are less than the next *within each scope*. * * @param mixed $roots - * @return boolean + * @return bool */ - protected function validateRootsByScope($roots) { - foreach($this->groupRootsByScope($roots) as $scope => $groupedRoots) { - $valid = $this->isEachRootValid($groupedRoots); + protected function validateRootsByScope($roots) + { + foreach ($this->groupRootsByScope($roots) as $scope => $groupedRoots) { + $valid = $this->isEachRootValid($groupedRoots); - if ( !$valid ) - return false; - } + if (! $valid) { + return false; + } + } - return true; + return true; } /** * Given a list of root nodes, it returns an array in which the keys are the * array of the actual scope column values and the values are the root nodes - * inside that scope themselves + * inside that scope themselves. * * @param mixed $roots * @return array */ - protected function groupRootsByScope($roots) { - $rootsGroupedByScope = array(); + protected function groupRootsByScope($roots) + { + $rootsGroupedByScope = []; - foreach($roots as $root) { - $key = $this->keyForScope($root); + foreach ($roots as $root) { + $key = $this->keyForScope($root); - if ( !isset($rootsGroupedByScope[$key]) ) - $rootsGroupedByScope[$key] = array(); + if (! isset($rootsGroupedByScope[$key])) { + $rootsGroupedByScope[$key] = []; + } - $rootsGroupedByScope[$key][] = $root; - } + $rootsGroupedByScope[$key][] = $root; + } - return $rootsGroupedByScope; + return $rootsGroupedByScope; } /** @@ -210,15 +223,16 @@ protected function groupRootsByScope($roots) { * @param Baum\Node $node * @return string */ - protected function keyForScope($node) { - return implode('-', array_map(function($column) use ($node) { + protected function keyForScope($node) + { + return implode('-', array_map(function ($column) use ($node) { $value = $node->getAttribute($column); - if ( is_null($value) ) - return 'NULL'; + if (is_null($value)) { + return 'NULL'; + } return $value; }, $node->getScopedColumns())); } - }