koseven / koseven

Koseven a Kohana fork compatible with PHP7
https://koseven.dev
Other
377 stars 157 forks source link

ORM nested set #345

Open WinterSilence opened 5 years ago

WinterSilence commented 5 years ago

What's u think about nested set model?

<?php
/**
 * ORM [nested set](https://wikipedia.org/wiki/Nested_set_model).
 *
 * @package    KO7/ORM
 * @category   Models
 * @author     Kohana Team
 * @copyright  (c) Kohana Team
 * @license    https://koseven.ga/LICENSE.md
 */
abstract class KO7_ORM_Nested_Set extends ORM {
    /**
     * @var array
     */
    protected $_sorting = ['left_key' => 'ASC'];
    /**
     * @var string Left key column name.
     */
    protected $left_key = 'left_key';
    /**
     * @var string Rigth key column name.
     */
    protected $right_key = 'right_key';
    /**
     * @var string Depht column name.
     */
    protected $depht_key = 'depth';
    /**
     * @var string Scope column name.
     */
    protected $scope_key = 'scope';
    /**
     * @var bool Use or not scope for multi root's tree.
     */
    protected $use_scope = FALSE;

    /**
     * Check if node has previous sibling.
     * 
     * @return bool
     */
    public function has_prev_sibling()
    {
        return $this->get_prev_sibling()->is_valid_node();
    }

    /**
     * Test if node has next sibling.
     *
     * @return bool
     */
    public function has_next_sibling()
    {
        return $this->get_next_sibling()->is_valid_node();
    }

    /**
     * Check if node has children.
     *
     * @return bool
     */
    public function has_children()
    {
        return ($this->{$this->right_key} - $this->{$this->left_key}) > 1;
    }

    /**
     * Check if node has parent.
     *
     * @return bool
     */
    public function has_parent()
    {
        return $this->is_valid_node() AND ! $this->is_root();
    }

    /**
     * Gets prev sibling or empty record.
     *
     * @return ORM_Nested_Set
     */
    public function get_prev_sibling()
    {
        $model = new $this->_object_name;
        $model->where($this->right_key, '=', $this->{$this->left_key} - 1);
        return $this->and_scope($model)->find();
    }

    /**
     * Gets next sibling or empty record.
     *
     * @return ORM_Nested_Set
     */
    public function get_next_sibling()
    {
        $model = new $this->_object_name;
        $model->where($this->left_key, '=', $this->{$this->right_key} + 1);
        return $this->and_scope($model)->find();
    }

    /**
     * Gets siblings for node
     *
     * @todo Optimize
     * @param bool $include_node
     * @return array List of sibling ORM_Nested_Set objects
     */
    public function get_siblings($include_node = FALSE)
    {
        $siblings = [];
        $parent = $this->get_parent();
        if ($parent AND $parent->loaded())
        {
            foreach ($parent->get_children() as $child)
            {
                if ($this->is_equal_to($child) AND ! $include_node)
                {
                    continue;
                }
                $siblings[] = $child;
            }
        }
        return $siblings;
    }

    /**
     * Gets record of first child or empty record.
     *
     * @return ORM_Nested_Set
     */
    public function get_first_child()
    {
        $model = new $this->_object_name;
        $model->where($this->left_key, '=', $this->{$this->left_key} + 1);
        return $this->and_scope($model)->find();
    }

    /**
     * Gets record of last child or empty record.
     *
     * @return ORM_Nested_Set
     */
    public function get_last_child()
    {
        $model = new $this->_object_name;
        $model->where($this->right_key, '=', $this->{$this->right_key} - 1)
        return $this->and_scope($model)->find();
    }

    /**
     * Gets children for node (direct descendants only).
     * @todo check
     * @return mixed The children of the node or FALSE if the node has no children.
     */
    public function get_children()
    {
        return $this->get_descendants(1);
    }

    /**
     * Gets descendants for node (direct descendants only).
     *
     * @param int|NULL $depth
     * @param bool $include_node Include or not this node, default: false
     * @return mixed  The descendants of the node or FALSE if the node has no descendants
     */
    public function get_descendants($depth = NULL, $include_node = FALSE)
    {
        $model = new $this->_object_name;
        $model->where($this->left_key, $include_node ? '>=' : '>', $this->{$this->left_key})
              ->where($this->right_key, $include_node ? '<=' : '<', $this->{$this->right_key});
        $result = $this->and_scope($model)->order_by($this->left_key, 'ASC');

        if ($depth !== NULL)
        {
            $result->where($this->depht_key, '<=', $this->{$this->depht_key} + $depth);
        }

        $result = $result->find_all();

        return $result->count() > 0 ? $result : FALSE;
    }

    /**
     * Gets record of parent or empty record.
     *
     * @return self
     */
    public function get_parent()
    {
        $model = new $this->_object_name;
        $model->where($this->left_key, '<', $this->{$this->left_key})
              ->where($this->right_key, '>', $this->{$this->right_key})
              ->where($this->depht_key, '>=', $this->{$this->depht_key} - 1);
        return $this->and_scope($model)->order_by($this->right_key, 'ASC')->find();
    }

    /**
     * Gets ancestors for node
     *
     * @param NULL|int $depth
     * @return mixed The ancestors of the node or FALSE if the node has no ancestors 
     *               (this basically means it's a root node).
     */
    public function get_ancestors($depth = NULL)
    {
        $model = new $this->_object_name;
        $model->where($this->left_key, '<', $this->{$this->left_key})
              ->where($this->right_key, '>', $this->{$this->right_key});
        $result = $this->and_scope($model)->order_by($this->left_key, 'ASC');

        if ($depth !== NULL)
        {
            $result->and_where($this->depht_key, '>=', $this->{$this->depht_key} - $depth);
        }
        $result = $result->find_all();
        return $result->count() > 0 ? $result : FALSE;
    }

    /**
     * Gets path to node from root, uses record::toString() method to get node names
     *
     * @param  bool  $include_root Include or not in path root node
     * @param  bool  $include_self Include or not in path self node
     *
     * @return  array
     */
    public function get_path($include_root = FALSE, $include_self = FALSE)
    {
        $path = [];

        $ancestors = $this->get_ancestors();
        if ($ancestors)
        {
            foreach ($ancestors as $ancestor)
            {
                if ( ! $include_root)
                {
                    $include_root = TRUE;
                    continue;
                }
                $path[] = $ancestor;
            }
        }

        if ($this->is_root() AND ! $include_root)
        {
            return $path;
        }

        // add self node
        if ($include_self AND $this->loaded())
        {
            $path[] = $this;
        }

        return $path;
    }

    /**
     * Gets number of children (direct descendants).
     *
     * @return int
     */
    public function get_number_children()
    {
        $children = $this->get_children();
        return $children === FALSE ? 0 : $children->count();
    }

    /**
     * Gets number of descendants (children and their children).
     *
     * @return int
     */
    public function get_number_descendants()
    {
        return ($this->{$this->right_key} - $this->{$this->left_key} - 1) / 2;
    }

    /**
     * Inserts node as parent of dest record.
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function insert_as_parent_of($node)
    {
        if ($this->is_valid_node())
        {
            throw new Database_Exception('Cannot insert existing node as parent');
        }

        // fix param
        $node = $this->get_node($node);

        if ($node->is_root())
        {
            throw new Database_Exception('Cannot insert as parent of root.');
        }

        $new_left = $node->{$this->left_key};
        $new_right = $node->{$this->right_key} + 2;
        $new_scope = $node->{$this->scope_key};
        $new_level = $node->{$this->depht_key};

        try
        {
            $this->_db->begin();

            // make space for new node
            $this->shift_rl_values($node->{$this->right_key} + 1, 2, $new_scope);
            // slide child nodes over one and down one to allow new parent to wrap them
            $this->and_scope(
                DB::update($this->_table_name)
                    ->value($this->left_key, DB::expr("{$this->left_key} + 1"))
                    ->value($this->right_key, DB::expr("{$this->right_key} + 1"))
                    ->value($this->depht_key, DB::expr("{$this->depht_key} + 1"))
                    ->where($this->left_key, '>=', $new_left)
                    ->where($this->right_key, '<=', $new_right),
                $new_scope
            )->execute($this->_db);
            $this->{$this->depht_key} = $new_level;
            $this->insert_node($new_left, $new_right, $new_scope);

            $this->_db->commit();
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Inserts node as previous sibling of dest record.
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function insert_as_prev_sibling_of($node)
    {
        if ($this->is_valid_node())
        {
            throw new Database_Exception('Cannot insert existing node as prev sibling of.');
        }

        // fix param
        $node = $this->get_node($node);

        $new_left = $node->{$this->left_key};
        $new_right = $node->{$this->left_key} + 1;
        $new_scope = $node->{$this->scope_key};

        try
        {
            $this->_db->begin();

            $this->shift_rl_values($new_left, 2, $new_scope);
            $this->{$this->depht_key} = $node->{$this->depht_key};
            $this->insert_node($new_left, $new_right, $new_scope);

            $this->_db->commit();

            // upgrade node right, left values
            $node->{$this->left_key} += 2;
            $node->{$this->right_key} += 2;
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Inserts node as next sibling of dest record
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function insert_as_next_sibling_of($node)
    {
        if ($this->is_valid_node())
        {
            throw new Database_Exception('Cannot insert existing node as next sibling of.');
        }

        // fix param
        $node = $this->get_node($node);

        $new_left = $node->{$this->right_key} + 1;
        $new_right = $node->{$this->right_key} + 2;
        $new_scope = $node->{$this->scope_key};

        try
        {
            $this->_db->begin();

            $this->shift_rl_values($new_left, 2, $new_scope);
            $this->{$this->depht_key} = $node->{$this->depht_key};
            $this->insert_node($new_left, $new_right, $new_scope);

            $this->_db->commit();
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Inserts node as first child of dest record
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     *
     * @return bool
     */
    public function insert_as_first_child_of($node)
    {
        if ($this->is_valid_node())
        {
            throw new Database_Exception('Cannot insert existing node as first child of.');
        }

        // fix param
        $node = $this->get_node($node);

        $new_left = $node->{$this->left_key} + 1;
        $new_right = $node->{$this->left_key} + 2;
        $new_scope = $node->{$this->scope_key};

        try
        {
            $this->_db->begin();

            $this->shift_rl_values($new_left, 2, $new_scope);
            $this->{$this->depht_key} = $node->{$this->depht_key} + 1;
            $this->insert_node($new_left, $new_right, $new_scope);

            $this->_db->commit();

            // upgrade node right value
            $node->{$this->right_key} += 2;
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Insert node as last child of $node
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     *
     * @return bool
     */
    public function insert_as_last_child_of($node)
    {
        if ($this->is_valid_node())
        {
            throw new Database_Exception('Cannot insert existing node as last child.');
        }

        // fix param
        $node = $this->get_node($node);

        $new_left = $node->{$this->right_key};
        $new_right = $node->{$this->right_key} + 1;
        $new_scope = $node->{$this->scope_key};

        try
        {
            $this->_db->begin();

            $this->shift_rl_values($new_left, 2, $new_scope);
            $this->{$this->depht_key} = $node->{$this->depht_key} + 1;
            $this->insert_node($new_left, $new_right, $new_scope);

            $this->_db->commit();

            // upgrade node right value
            $node->{$this->right_key} += 2;

        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Accomplishes moving of nodes between different trees.
     * Used by the move* methods if the root values of the two nodes are different.
     *
     * @param  ORM_Nested_Set $node
     * @param  int      $new_left_value
     * @param  int      $move_type
     * @return bool
     */
    protected function _move_between_trees(ORM_Nested_Set $node, $new_left_value, $move_type)
    {
        try
        {
            $this->_db->begin();

            // move between trees: Detach from old tree & insert into new tree
            $new_scope = $node->{$this->scope_key};
            $old_scope = $this->{$this->scope_key};
            $old_left = $this->{$this->left_key};
            $old_right = $this->{$this->right_key};
            $old_level = $this->{$this->depht_key};

            // prepare target tree for insertion, make room
            $this->shift_rl_values($new_left_value, $old_right - $old_left - 1, $new_scope);

            // set new root id for this node
            $this->{$this->scope_key} = $new_scope;
            $this->save();

            // insert this node as a new node
            $this->{$this->right_key} = 0;
            $this->{$this->left_key} = 0;

            switch ($move_type)
            {
                case 'move_as_prev_sibling_of':
                    $this->insert_as_prev_sibling_of($node);
                    break;
                case 'move_as_first_child_of':
                    $this->insert_as_first_child_of($node);
                    break;
                case 'move_as_next_sibling_of':
                    $this->insert_as_next_sibling_of($node);
                    break;
                case 'move_as_last_child_of':
                    $this->insert_as_last_child_of($node);
                    break;
                default:
                    throw new Database_Exception('Unknown move operation.');
            }

            $diff = $old_right - $old_left;
            $this->{$this->right_key} = $this->{$this->left_key} + $old_right - $old_left;
            $this->save();

            $new_level = $this->{$this->depht_key};
            $level_diff = $new_level - $old_level;

            // relocate descendants of the node
            $diff = $this->{$this->left_key} - $old_left;

            // update left, right, root, level for all descendants
            $update = DB::update($this->_table_name)
                ->value($this->left_key, DB::expr("{$this->left_key} + {$diff}"))
                ->value($this->right_key, DB::expr("{$this->right_key} + {$diff}"))
                ->value($this->depht_key, DB::expr("{$this->depht_key} + {$level_diff}"));
            if ($this->use_scope)
            {
                $update->value($this->scope_key, $new_scope);
            }
            $this->and_scope(
                $update->where($this->left_key, '>', $old_left)
                       ->where($this->right_key, '<', $old_right),
                $old_scope
            )->execute($this->_db);

            // close gap in old tree
            $first = $old_right + 1;
            $delta = $old_left - $old_right - 1;
            $this->shift_rl_values($first, $delta, $old_scope);

            $this->_db->commit();
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Moves node as prev sibling of $node
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function move_as_prev_sibling_of($node)
    {
        // fix param
        $node = $this->get_node($node);

        if ( ! $this->loaded())
        {
            return $this->insert_as_prev_sibling_of($node);
        }

        if ($this->_primary_key_value === $node->_primary_key_value)
        {
            throw new Database_Exception('Cannot move node as previous sibling of itself.');
        }

        if ($node->{$this->scope_key} != $this->{$this->scope_key})
        {
            // move between trees
            return $this->_move_between_trees($node, $node->{$this->left_key}, __FUNCTION__);
        }
        else
        {
            // move within the tree
            $old_level = $this->{$this->depht_key};
            $this->{$this->depht_key} = $node->{$this->depht_key};
            $this->update_node($node->{$this->left_key}, $this->{$this->depht_key} - $old_level);
        }

        return TRUE;
    }

    /**
     * Moves node as next sibling of dest record
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function move_as_next_sibling_of($node)
    {
        // fix param
        $node = $this->get_node($node);

        if ( ! $this->loaded())
        {
            return $this->insert_as_next_sibling_of($node);
        }

        if ($this->_primary_key_value === $node->_primary_key_value)
        {
            throw new Database_Exception('Cannot move node as next sibling of itself');
        }

        if ($node->{$this->scope_key} != $this->{$this->scope_key})
        {
            // move between trees
            return $this->_move_between_trees($node, $node->{$this->right_key} + 1, __FUNCTION__);
        }
        else
        {
            // move within tree
            $old_level = $this->{$this->depht_key};
            $this->{$this->depht_key} = $node->{$this->depht_key};
            $this->update_node($node->{$this->right_key} + 1, $this->{$this->depht_key} - $old_level);
        }

        return TRUE;
    }

    /**
     * Moves node as first child of dest record
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function move_as_first_child_of($node)
    {
        // fix param
        $node = $this->get_node($node);

        if ( ! $this->loaded())
        {
            return $this->insert_as_first_child_of($node);
        }

        if ($this->_primary_key_value === $node->_primary_key_value)
        {
            throw new Database_Exception('Cannot move node as first child of itself or into a descendant');
        }

        if ($node->{$this->scope_key} != $this->{$this->scope_key})
        {
            // move between trees
            return $this->_move_between_trees($node, $node->{$this->left_key} + 1, __FUNCTION__);
        }
        else
        {
            // move within tree
            $old_level = $this->{$this->depht_key};
            $this->{$this->depht_key} = $node->{$this->depht_key} + 1;
            $this->update_node($node->{$this->left_key} + 1, $this->{$this->depht_key} - $old_level);
        }

        return TRUE;
    }

    /**
     * Moves node as last child of dest record
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     *
     * @return bool
     */
    public function move_as_last_child_of($node)
    {
        // fix param
        $node = $this->get_node($node);

        if ( ! $this->loaded())
        {
            return $this->insert_as_last_child_of($node);
        }

        if ($this->_primary_key_value === $node->_primary_key_value)
        {
            throw new Database_Exception('Cannot move node as last child of itself or into a descendant');
        }

        if ($node->{$this->scope_key} != $this->{$this->scope_key})
        {
            // move between trees
            return $this->_move_between_trees($node, $node->{$this->right_key}, __FUNCTION__);
        }
        else
        {
            // move within tree
            $old_level = $this->{$this->depht_key};
            $this->{$this->depht_key} = $node->{$this->depht_key} + 1;
            $this->update_node($node->{$this->right_key}, $this->{$this->depht_key} - $old_level);
        }

        return TRUE;
    }

    /**
     * Makes this node a root node. Only used in multiple-root trees.
     *
     * @param  int|NULL $new_scope New scope value
     * @return bool
     */
    public function make_root($new_scope = NULL)
    {
        // @todo throw exception instead?
        if ($this->is_root())
        {
            return TRUE;
        }

        // check scope
        if ($this->use_scope AND empty($new_scope))
        {
            $new_scope = $this->get_next_scope();
        }

        $old_right = intval($this->{$this->right_key});
        $old_left = intval($this->{$this->left_key});
        $old_level = intval($this->{$this->depht_key});
        $old_scope = intval($this->{$this->scope_key});

        try
        {
            $this->_db->begin();

            // update descendants left, right, root, level values
            $diff = 1 - $old_left;

            if ($this->loaded())
            {
                $update = DB::update($this->_table_name)
                    ->value($this->left_key, DB::expr("{$this->left_key} + {$diff}"))
                    ->value($this->right_key, DB::expr("{$this->right_key} + {$diff}"))
                    ->value($this->depht_key, DB::expr("{$this->depht_key} - {$old_level}"));
                if ($this->use_scope)
                {
                    $update->value($this->scope_key, $new_scope)
                           ->where($this->scope_key, '=', $old_scope);
                }
                $update->where($this->left_key, '>', $old_left)
                    ->where($this->right_key, '<', $old_right)
                    ->execute($this->_db);

                // detach from old tree (close gap in old tree)
                $first = $old_right + 1;
                $delta = $old_left - $old_right - 1;
                $this->shift_rl_values($first, $delta, $old_scope);
            }

            // Set new left, right, root, level values for root node
            $this->{$this->left_key} = 1;
            $this->{$this->right_key} = $this->loaded() ? $old_right - $old_left + 1 : 2;
            $this->{$this->scope_key} = $new_scope;
            $this->{$this->depht_key} = 0;

            $this->save();

            $this->_db->commit();
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Inserts node as last child for this node.
     *
     * @param mixed $node Instance of ORM_Nested_Set or primary key value
     *
     * @return bool
     */
    public function add_child($node)
    {
        $node = $this->get_node($node);
        return $node->insert_as_last_child_of($this);
    }

    /**
     * Determines if node is leaf.
     *
     * @return bool
     */
    public function is_leaf()
    {
        return ($this->{$this->right_key} - $this->{$this->left_key}) == 1;
    }

    /**
     * Determines if node is root.
     *
     * @return bool
     */
    public function is_root()
    {
        return $this->{$this->left_key} == 1;
    }

    /**
     * Determines if node is equal to subject node
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function is_equal_to($node)
    {
        // fix param
        $node = $this->get_node($node);
        return $this->{$this->left_key} == $node->{$this->left_key} AND
               $this->{$this->right_key} == $node->{$this->right_key} AND
               ( ! $this->use_scope OR $this->{$this->scope_key} == $node->{$this->scope_key});
    }

    /**
     * Determines if node is child of subject node
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function is_descendant_of($node)
    {
        // fix param
        $node = $this->get_node($node);
        return $this->{$this->left_key} > $node->{$this->left_key} AND
               $this->{$this->right_key} < $node->{$this->right_key} AND
               ( ! $this->use_scope OR $this->{$this->scope_key} == $node->{$this->scope_key});
    }

    /**
     * Determines if node is child of or sibling to subject node
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function is_descendant_of_or_equal_to($node)
    {
        $node = $this->get_node($node);
        return $this->{$this->left_key} >= $node->{$this->left_key} AND
               $this->{$this->right_key} <= $node->{$this->right_key} AND
               ( ! $this->use_scope OR $this->{$this->scope_key} == $node->{$this->scope_key});
    }

    /**
     * Determines if node is ancestor of subject node
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return bool
     */
    public function is_ancestor_of($node)
    {
        // fix param
        $node = $this->get_node($node);
        return $node->{$this->left_key} > $this->{$this->left_key} AND
               $node->{$this->right_key} < $this->{$this->right_key} AND
               ( ! $this->use_scope OR $node->{$this->scope_key} == $this->{$this->scope_key});
    }

    /**
     * Determines if node is valid
     *
     * @return bool
     */
    public function is_valid_node()
    {
        return intval($this->{$this->right_key}) > intval($this->{$this->left_key});
    }

    /**
     * Deletes node and it's descendants.
     *
     * @return bool
     */
    public function delete()
    {
        try
        {
            $this->_db->begin();

            $this->and_scope(
                DB::delete($this->_table_name)
                    ->where($this->left_key, '>=', $this->{$this->left_key})
                    ->where($this->right_key, '<=', $this->{$this->right_key})
            )->execute($this->_db);

            $first = $this->{$this->right_key} + 1;
            $delta = $this->{$this->left_key} - $this->{$this->right_key} - 1;
            $this->shift_rl_values($first, $delta, $this->{$this->scope_key});

            $this->_db->commit();
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Check input param, and load node if needed
     *
     * @param  mixed $node Instance of ORM_Nested_Set or primary key value
     * @return ORM_Nested_Set
     */
    protected function get_node($node)
    {
        $old_node = $node;
        if ( ! ($node instanceof $this->_object_name))
        {
            $node = new $this->_object_name($node);
        }
        if ( ! $node->loaded())
        {
            throw new Database_Exception(
                'Cannot find node with :pk equal to :pk_value.',
                [':pk' => $this->_primary_key, ':pk_value' => $old_node]
            );
        }
        return $node;
    }

    /**
     * Sets node's left, right, parent, scope values and save's it
     *
     * @param int $left   Node left value
     * @param int $right  Node right value
     * @param int $scope  Node scope value
     *
     * @return ORM_Nested_Set
     */
    protected function insert_node($left = 0, $right = 0, $scope = 1)
    {
        $this->{$this->left_key} = $left;
        $this->{$this->right_key} = $right;
        $this->{$this->scope_key} = $scope;
        return $this->save();
    }

    /**
     * Move node's and its children to location $destLeft and updates rest of tree.
     *
     * @param int $node_left Destination left value
     * @param int $level_diff
     *
     * @return bool
     */
    protected function update_node($node_left, $level_diff)
    {
        $left = $this->{$this->left_key};
        $right = $this->{$this->right_key};
        $scope = $this->{$this->scope_key};

        $tree_size = $right - $left + 1;

        try
        {
            $this->_db->begin();

            // make room in the new branch
            $this->shift_rl_values($node_left, $tree_size, $scope);
            if ($left >= $node_left)
            {
                $left += $tree_size;
                $right += $tree_size;
            }
            // update level for descendants
            $this->and_scope(
                DB::update($this->_table_name)
                    ->value($this->depht_key, DB::expr("{$this->depht_key} + {$level_diff}"))
                    ->where($this->left_key, '>', $left)
                    ->where($this->right_key, '<', $right)
            )->execute($this->_db);
            // now there's enough room next to target to move the subtree
            $this->shift_rl_range($left, $right, $node_left - $left, $scope);
            // correct values after source (close gap in old tree)
            $this->shift_rl_values($right + 1, -$tree_size, $scope);
            $this->save();
            $this->reload();

            $this->_db->commit();
        }
        catch (Exception $e)
        {
            $this->_db->rollback();
            throw $e;
        }

        return TRUE;
    }

    /**
     * Adds '$delta' to all Left and Right values that are >= '$first'.
     * '$delta' can also be negative.
     * Note: This method does wrap its database queries in a transaction. This should be done
     * by the invoking code.
     *
     * @param int   $first First node to be shifted
     * @param int   $delta Value to be shifted by, can be negative
     * @param mixed $scope Scope value
     *
     * @return $this
     */
    protected function shift_rl_values($first, $delta, $scope)
    {
        $this->and_scope(
            DB::update($this->_table_name)
                ->value($this->left_key, DB::expr("{$this->left_key} + {$delta}"))
                ->where($this->left_key, '>=', $first),
            $scope
        )->execute($this->_db);
        $this->and_scope(
            DB::update($this->_table_name)
                ->value($this->right_key, DB::expr("{$this->right_key} + {$delta}"))
                ->where($this->right_key, '>=', $first),
            $scope
        )->execute($this->_db);
        return $this;
    }

    /**
     * Adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'.
     * '$delta' can also be negative.
     * Note: This method does wrap its database queries in a transaction. This should be done
     * by the invoking code.
     *
     * @param int   $first  First node to be shifted (L value)
     * @param int   $last   Last node to be shifted (L value)
     * @param int   $delta  Value to be shifted by, can be negative
     * @param mixed $scope  Scope value
     *
     * @return $this
     */
    protected function shift_rl_range($first, $last, $delta, $scope)
    {
        $this->and_scope(
            DB::update($this->_table_name)
                ->value($this->left_key, DB::expr("{$this->left_key} + {$delta}"))
                ->where($this->left_key, '>=', $first)
                ->where($this->left_key, '<=', $last),
            $scope
        )->execute($this->_db);
        $this->and_scope(
            DB::update($this->_table_name)
                ->value($this->right_key, DB::expr("{$this->right_key} + {$delta}"))
                ->where($this->right_key, '>=', $first)
                ->where($this->right_key, '<=', $last),
            $scope
        )->execute($this->_db);
        return $this;
    }

    /**
     * Add scope condition in query if needed.
     *
     * @param  ORM_MTTP $model
     * @param  mixed $scope|null
     * @return mixed
     */
    protected function and_scope(ORM_MTTP $model, $scope = NULL)
    {
        if ($this->use_scope)
        {
            $model->where(
                $this->scope_key, 
                '=', 
                is_null($scope) ? $this->{$this->scope_key} : $scope
            );
        }
        return $model;
    }

    /**
     * Calculate next scope value.
     *
     * @return int
     */
    protected function get_next_scope()
    {
        // returns available value for scope
        $scope = DB::select(DB::expr("MAX({$this->scope_key}) as scope"))
            ->from($this->_table_name)
            ->execute($this->_db)
            ->current();
        if (isset($scope['scope']) AND $scope['scope'] > 0)
        {
            return $scope['scope'] + 1;
        }
        return 1;
    }

    /**
     * Handles setting of column
     *
     * @param  string $column Column name
     * @param  mixed  $value  Column value
     * @return void
     */
    public function set($column, $value)
    {
        if ($column == $this->scope_key AND ! $this->use_scope)
        {
            return NULL;
        }
        parent::set($column, $value);
    }

    /**
     * Handles retrieval of all model values, relationships, and metadata.
     *
     * @param  string $column Column name
     * @return mixed
     */
    public function __get($column)
    {
        if ($column == $this->scope_key AND ! $this->use_scope)
        {
            return NULL;
        }
        return parent::__get($column);
    }
}
toitzi commented 5 years ago

@neo22s @jstrobel @svenbw @piotrbaczek Anything in work here or any suggestions? Is this something we will implement ?

neo22s commented 5 years ago

I do not understand what it does without reading the entire code. I do not have the time I am sorry I can not tell. :(