2212 lines
		
	
	
		
			58 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			2212 lines
		
	
	
		
			58 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * @package     FrameworkOnFramework
 | |
|  * @subpackage  table
 | |
|  * @copyright   Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
 | |
|  * @license     GNU General Public License version 2 or later; see LICENSE.txt
 | |
|  */
 | |
| 
 | |
| // Protect from unauthorized access
 | |
| defined('F0F_INCLUDED') or die;
 | |
| 
 | |
| /**
 | |
|  * A class to manage tables holding nested sets (hierarchical data)
 | |
|  *
 | |
|  * @property int    $lft   Left value (for nested set implementation)
 | |
|  * @property int    $rgt   Right value (for nested set implementation)
 | |
|  * @property string $hash  Slug hash (optional; for faster searching)
 | |
|  * @property string $slug  Node's slug (optional)
 | |
|  * @property string $title Title of the node (optional)
 | |
|  */
 | |
| class F0FTableNested extends F0FTable
 | |
| {
 | |
| 	/** @var int The level (depth) of this node in the tree */
 | |
| 	protected $treeDepth = null;
 | |
| 
 | |
| 	/** @var F0FTableNested The root node in the tree */
 | |
| 	protected $treeRoot = null;
 | |
| 
 | |
| 	/** @var F0FTableNested The parent node of ourselves */
 | |
| 	protected $treeParent = null;
 | |
| 
 | |
| 	/** @var bool Should I perform a nested get (used to query ascendants/descendants) */
 | |
| 	protected $treeNestedGet = false;
 | |
| 
 | |
| 	/** @var   array  A collection of custom, additional where clauses to apply during buildQuery */
 | |
| 	protected $whereClauses = array();
 | |
| 
 | |
| 	/**
 | |
| 	 * Public constructor. Overrides the parent constructor, making sure there are lft/rgt columns which make it
 | |
| 	 * compatible with nested sets.
 | |
| 	 *
 | |
| 	 * @param   string          $table  Name of the database table to model.
 | |
| 	 * @param   string          $key    Name of the primary key field in the table.
 | |
| 	 * @param   F0FDatabaseDriver &$db    Database driver
 | |
| 	 * @param   array           $config The configuration parameters array
 | |
| 	 *
 | |
| 	 * @throws \RuntimeException When lft/rgt columns are not found
 | |
| 	 */
 | |
| 	public function __construct($table, $key, &$db, $config = array())
 | |
| 	{
 | |
| 		parent::__construct($table, $key, $db, $config);
 | |
| 
 | |
| 		if (!$this->hasField('lft') || !$this->hasField('rgt'))
 | |
| 		{
 | |
| 			throw new \RuntimeException("Table " . $this->getTableName() . " is not compatible with F0FTableNested: it does not have lft/rgt columns");
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Overrides the automated table checks to handle the 'hash' column for faster searching
 | |
| 	 *
 | |
| 	 * @return boolean
 | |
| 	 */
 | |
| 	public function check()
 | |
| 	{
 | |
| 		// Create a slug if there is a title and an empty slug
 | |
| 		if ($this->hasField('title') && $this->hasField('slug') && empty($this->slug))
 | |
| 		{
 | |
| 			$this->slug = F0FStringUtils::toSlug($this->title);
 | |
| 		}
 | |
| 
 | |
| 		// Create the SHA-1 hash of the slug for faster searching (make sure the hash column is CHAR(64) to take
 | |
| 		// advantage of MySQL's optimised searching for fixed size CHAR columns)
 | |
| 		if ($this->hasField('hash') && $this->hasField('slug'))
 | |
| 		{
 | |
| 			$this->hash = sha1($this->slug);
 | |
| 		}
 | |
| 
 | |
| 		// Reset cached values
 | |
| 		$this->resetTreeCache();
 | |
| 
 | |
| 		return parent::check();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Delete a node, either the currently loaded one or the one specified in $id. If an $id is specified that node
 | |
| 	 * is loaded before trying to delete it. In the end the data model is reset. If the node has any children nodes
 | |
| 	 * they will be removed before the node itself is deleted.
 | |
| 	 *
 | |
| 	 * @param   integer $oid       The primary key value of the item to delete
 | |
| 	 *
 | |
| 	 * @throws  UnexpectedValueException
 | |
| 	 *
 | |
| 	 * @return  boolean  True on success
 | |
| 	 */
 | |
| 	public function delete($oid = null)
 | |
| 	{
 | |
| 		// Load the specified record (if necessary)
 | |
| 		if (!empty($oid))
 | |
| 		{
 | |
| 			$this->load($oid);
 | |
| 		}
 | |
| 
 | |
|         $k  = $this->_tbl_key;
 | |
|         $pk = (!$oid) ? $this->$k : $oid;
 | |
| 
 | |
|         // If no primary key is given, return false.
 | |
|         if (!$pk)
 | |
|         {
 | |
|             throw new UnexpectedValueException('Null primary key not allowed.');
 | |
|         }
 | |
| 
 | |
|         // Execute the logic only if I have a primary key, otherwise I could have weird results
 | |
|         // Perform the checks on the current node *BEFORE* starting to delete the children
 | |
|         if (!$this->onBeforeDelete($oid))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         $result = true;
 | |
| 
 | |
| 		// Recursively delete all children nodes as long as we are not a leaf node and $recursive is enabled
 | |
| 		if (!$this->isLeaf())
 | |
| 		{
 | |
| 			// Get all sub-nodes
 | |
| 			$table = $this->getClone();
 | |
| 			$table->bind($this->getData());
 | |
| 			$subNodes = $table->getDescendants();
 | |
| 
 | |
| 			// Delete all subnodes (goes through the model to trigger the observers)
 | |
| 			if (!empty($subNodes))
 | |
| 			{
 | |
| 				/** @var F0FTableNested $item */
 | |
| 				foreach ($subNodes as $item)
 | |
| 				{
 | |
|                     // We have to pass the id, so we are getting it again from the database.
 | |
|                     // We have to do in this way, since a previous child could have changed our lft and rgt values
 | |
| 					if(!$item->delete($item->$k))
 | |
|                     {
 | |
|                         // A subnode failed or prevents the delete, continue deleting other nodes,
 | |
|                         // but preserve the current node (ie the parent)
 | |
|                         $result = false;
 | |
|                     }
 | |
| 				};
 | |
| 
 | |
|                 // Load it again, since while deleting a children we could have updated ourselves, too
 | |
|                 $this->load($pk);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
|         if($result)
 | |
|         {
 | |
|             // Delete the row by primary key.
 | |
|             $query = $this->_db->getQuery(true);
 | |
|             $query->delete();
 | |
|             $query->from($this->_tbl);
 | |
|             $query->where($this->_tbl_key . ' = ' . $this->_db->q($pk));
 | |
| 
 | |
|             $this->_db->setQuery($query)->execute();
 | |
| 
 | |
|             $result = $this->onAfterDelete($oid);
 | |
|         }
 | |
| 
 | |
| 		return $result;
 | |
| 	}
 | |
| 
 | |
|     protected function onAfterDelete($oid)
 | |
|     {
 | |
|         $db = $this->getDbo();
 | |
| 
 | |
|         $myLeft  = $this->lft;
 | |
|         $myRight = $this->rgt;
 | |
| 
 | |
|         $fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
|         $fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
|         // Move all siblings to the left
 | |
|         $width = $this->rgt - $this->lft + 1;
 | |
| 
 | |
|         // Wrap everything in a transaction
 | |
|         $db->transactionStart();
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             // Shrink lft values
 | |
|             $query = $db->getQuery(true)
 | |
|                         ->update($db->qn($this->getTableName()))
 | |
|                         ->set($fldLft . ' = ' . $fldLft . ' - '.$width)
 | |
|                         ->where($fldLft . ' > ' . $db->q($myLeft));
 | |
|             $db->setQuery($query)->execute();
 | |
| 
 | |
|             // Shrink rgt values
 | |
|             $query = $db->getQuery(true)
 | |
|                         ->update($db->qn($this->getTableName()))
 | |
|                         ->set($fldRgt . ' = ' . $fldRgt . ' - '.$width)
 | |
|                         ->where($fldRgt . ' > ' . $db->q($myRight));
 | |
|             $db->setQuery($query)->execute();
 | |
| 
 | |
|             // Commit the transaction
 | |
|             $db->transactionCommit();
 | |
|         }
 | |
|         catch (\Exception $e)
 | |
|         {
 | |
|             // Roll back the transaction on error
 | |
|             $db->transactionRollback();
 | |
| 
 | |
|             throw $e;
 | |
|         }
 | |
| 
 | |
|         return parent::onAfterDelete($oid);
 | |
|     }
 | |
| 
 | |
| 	/**
 | |
| 	 * Not supported in nested sets
 | |
| 	 *
 | |
| 	 * @param   string $where Ignored
 | |
| 	 *
 | |
| 	 * @return  void
 | |
| 	 *
 | |
| 	 * @throws  RuntimeException
 | |
| 	 */
 | |
| 	public function reorder($where = '')
 | |
| 	{
 | |
| 		throw new RuntimeException('reorder() is not supported by F0FTableNested');
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Not supported in nested sets
 | |
| 	 *
 | |
| 	 * @param   integer $delta Ignored
 | |
| 	 * @param   string  $where Ignored
 | |
| 	 *
 | |
| 	 * @return  void
 | |
| 	 *
 | |
| 	 * @throws  RuntimeException
 | |
| 	 */
 | |
| 	public function move($delta, $where = '')
 | |
| 	{
 | |
| 		throw new RuntimeException('move() is not supported by F0FTableNested');
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Create a new record with the provided data. It is inserted as the last child of the current node's parent
 | |
| 	 *
 | |
| 	 * @param   array $data The data to use in the new record
 | |
| 	 *
 | |
| 	 * @return  static  The new node
 | |
| 	 */
 | |
| 	public function create($data)
 | |
| 	{
 | |
| 		$newNode = $this->getClone();
 | |
| 		$newNode->reset();
 | |
| 		$newNode->bind($data);
 | |
| 
 | |
| 		if ($this->isRoot())
 | |
| 		{
 | |
| 			return $newNode->insertAsChildOf($this);
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			return $newNode->insertAsChildOf($this->getParent());
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Makes a copy of the record, inserting it as the last child of the given node's parent.
 | |
| 	 *
 | |
| 	 * @param   integer|array  $cid  The primary key value (or values) or the record(s) to copy. 
 | |
| 	 *                               If null, the current record will be copied
 | |
| 	 * 
 | |
| 	 * @return self|F0FTableNested	 The last copied node
 | |
| 	 */
 | |
| 	public function copy($cid = null)
 | |
| 	{
 | |
| 		//We have to cast the id as array, or the helper function will return an empty set
 | |
| 		if($cid)
 | |
| 		{
 | |
| 			$cid = (array) $cid;
 | |
| 		}
 | |
| 
 | |
|         F0FUtilsArray::toInteger($cid);
 | |
| 		$k = $this->_tbl_key;
 | |
| 
 | |
| 		if (count($cid) < 1)
 | |
| 		{
 | |
| 			if ($this->$k)
 | |
| 			{
 | |
| 				$cid = array($this->$k);
 | |
| 			}
 | |
| 			else
 | |
| 			{
 | |
| 				// Even if it's null, let's still create the record
 | |
| 				$this->create($this->getData());
 | |
| 				
 | |
| 				return $this;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		foreach ($cid as $item)
 | |
| 		{
 | |
| 			// Prevent load with id = 0
 | |
| 
 | |
| 			if (!$item)
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			$this->load($item);
 | |
| 			
 | |
| 			$this->create($this->getData());
 | |
| 		}
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Method to reset class properties to the defaults set in the class
 | |
| 	 * definition. It will ignore the primary key as well as any private class
 | |
| 	 * properties.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function reset()
 | |
| 	{
 | |
| 		$this->resetTreeCache();
 | |
| 
 | |
| 		parent::reset();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Insert the current node as a tree root. It is a good idea to never use this method, instead providing a root node
 | |
| 	 * in your schema installation and then sticking to only one root.
 | |
| 	 *
 | |
| 	 * @return self
 | |
| 	 */
 | |
| 	public function insertAsRoot()
 | |
| 	{
 | |
|         // You can't insert a node that is already saved i.e. the table has an id
 | |
|         if($this->getId())
 | |
|         {
 | |
|             throw new RuntimeException(__METHOD__.' can be only used with new nodes');
 | |
|         }
 | |
| 
 | |
| 		// First we need to find the right value of the last parent, a.k.a. the max(rgt) of the table
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		// Get the lft/rgt names
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$query = $db->getQuery(true)
 | |
| 			->select('MAX(' . $fldRgt . ')')
 | |
| 			->from($db->qn($this->getTableName()));
 | |
| 		$maxRgt = $db->setQuery($query, 0, 1)->loadResult();
 | |
| 
 | |
| 		if (empty($maxRgt))
 | |
| 		{
 | |
| 			$maxRgt = 0;
 | |
| 		}
 | |
| 
 | |
| 		$this->lft = ++$maxRgt;
 | |
| 		$this->rgt = ++$maxRgt;
 | |
| 
 | |
| 		$this->store();
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Insert the current node as the first (leftmost) child of a parent node.
 | |
| 	 *
 | |
| 	 * WARNING: If it's an existing node it will be COPIED, not moved.
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $parentNode The node which will become our parent
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function insertAsFirstChildOf(F0FTableNested &$parentNode)
 | |
| 	{
 | |
|         if($parentNode->lft >= $parentNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the parent node');
 | |
|         }
 | |
| 
 | |
| 		// Get a reference to the database
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		// Get the field names
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 
 | |
|         // Nullify the PK, so a new record will be created
 | |
|         $pk = $this->getKeyName();
 | |
|         $this->$pk = null;
 | |
| 
 | |
| 		// Get the value of the parent node's rgt
 | |
| 		$myLeft = $parentNode->lft;
 | |
| 
 | |
| 		// Update my lft/rgt values
 | |
| 		$this->lft = $myLeft + 1;
 | |
| 		$this->rgt = $myLeft + 2;
 | |
| 
 | |
| 		// Update parent node's right (we added two elements in there, remember?)
 | |
| 		$parentNode->rgt += 2;
 | |
| 
 | |
| 		// Wrap everything in a transaction
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			// Make a hole (2 queries)
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($fldLft . ' = ' . $fldLft . '+2')
 | |
| 				->where($fldLft . ' > ' . $db->q($myLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($fldRgt . ' = ' . $fldRgt . '+ 2')
 | |
| 				->where($fldRgt . '>' . $db->q($myLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Insert the new node
 | |
| 			$this->store();
 | |
| 
 | |
| 			// Commit the transaction
 | |
| 			$db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			// Roll back the transaction on error
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Insert the current node as the last (rightmost) child of a parent node.
 | |
| 	 *
 | |
| 	 * WARNING: If it's an existing node it will be COPIED, not moved.
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $parentNode The node which will become our parent
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function insertAsLastChildOf(F0FTableNested &$parentNode)
 | |
| 	{
 | |
|         if($parentNode->lft >= $parentNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the parent node');
 | |
|         }
 | |
| 
 | |
| 		// Get a reference to the database
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		// Get the field names
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 
 | |
|         // Nullify the PK, so a new record will be created
 | |
|         $pk = $this->getKeyName();
 | |
|         $this->$pk = null;
 | |
| 
 | |
| 		// Get the value of the parent node's lft
 | |
| 		$myRight = $parentNode->rgt;
 | |
| 
 | |
| 		// Update my lft/rgt values
 | |
| 		$this->lft = $myRight;
 | |
| 		$this->rgt = $myRight + 1;
 | |
| 
 | |
| 		// Update parent node's right (we added two elements in there, remember?)
 | |
| 		$parentNode->rgt += 2;
 | |
| 
 | |
| 		// Wrap everything in a transaction
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			// Make a hole (2 queries)
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($fldRgt . ' = ' . $fldRgt . '+2')
 | |
| 				->where($fldRgt . '>=' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($fldLft . ' = ' . $fldLft . '+2')
 | |
| 				->where($fldLft . '>' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Insert the new node
 | |
| 			$this->store();
 | |
| 
 | |
| 			// Commit the transaction
 | |
| 			$db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			// Roll back the transaction on error
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for insertAsLastchildOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $parentNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 */
 | |
| 	public function insertAsChildOf(F0FTableNested &$parentNode)
 | |
| 	{
 | |
| 		return $this->insertAsLastChildOf($parentNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Insert the current node to the left of (before) a sibling node
 | |
| 	 *
 | |
| 	 * WARNING: If it's an existing node it will be COPIED, not moved.
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $siblingNode We will be inserted before this node
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function insertLeftOf(F0FTableNested &$siblingNode)
 | |
| 	{
 | |
|         if($siblingNode->lft >= $siblingNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the sibling node');
 | |
|         }
 | |
| 
 | |
| 		// Get a reference to the database
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		// Get the field names
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 
 | |
|         // Nullify the PK, so a new record will be created
 | |
|         $pk = $this->getKeyName();
 | |
|         $this->$pk = null;
 | |
| 
 | |
| 		// Get the value of the parent node's rgt
 | |
| 		$myLeft = $siblingNode->lft;
 | |
| 
 | |
| 		// Update my lft/rgt values
 | |
| 		$this->lft = $myLeft;
 | |
| 		$this->rgt = $myLeft + 1;
 | |
| 
 | |
| 		// Update sibling's lft/rgt values
 | |
| 		$siblingNode->lft += 2;
 | |
| 		$siblingNode->rgt += 2;
 | |
| 
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			$db->setQuery(
 | |
| 				$db->getQuery(true)
 | |
| 					->update($db->qn($this->getTableName()))
 | |
| 					->set($fldLft . ' = ' . $fldLft . '+2')
 | |
| 					->where($fldLft . ' >= ' . $db->q($myLeft))
 | |
| 			)->execute();
 | |
| 
 | |
| 			$db->setQuery(
 | |
| 				$db->getQuery(true)
 | |
| 					->update($db->qn($this->getTableName()))
 | |
| 					->set($fldRgt . ' = ' . $fldRgt . '+2')
 | |
| 					->where($fldRgt . ' > ' . $db->q($myLeft))
 | |
| 			)->execute();
 | |
| 
 | |
| 			$this->store();
 | |
| 
 | |
|             // Commit the transaction
 | |
|             $db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Insert the current node to the right of (after) a sibling node
 | |
| 	 *
 | |
| 	 * WARNING: If it's an existing node it will be COPIED, not moved.
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $siblingNode We will be inserted after this node
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function insertRightOf(F0FTableNested &$siblingNode)
 | |
| 	{
 | |
|         if($siblingNode->lft >= $siblingNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the sibling node');
 | |
|         }
 | |
| 
 | |
| 		// Get a reference to the database
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		// Get the field names
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 
 | |
|         // Nullify the PK, so a new record will be created
 | |
|         $pk = $this->getKeyName();
 | |
|         $this->$pk = null;
 | |
| 
 | |
| 		// Get the value of the parent node's lft
 | |
| 		$myRight = $siblingNode->rgt;
 | |
| 
 | |
| 		// Update my lft/rgt values
 | |
| 		$this->lft = $myRight + 1;
 | |
| 		$this->rgt = $myRight + 2;
 | |
| 
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			$db->setQuery(
 | |
| 				$db->getQuery(true)
 | |
| 					->update($db->qn($this->getTableName()))
 | |
| 					->set($fldRgt . ' = ' . $fldRgt . '+2')
 | |
| 					->where($fldRgt . ' > ' . $db->q($myRight))
 | |
| 			)->execute();
 | |
| 
 | |
| 			$db->setQuery(
 | |
| 				$db->getQuery(true)
 | |
| 					->update($db->qn($this->getTableName()))
 | |
| 					->set($fldLft . ' = ' . $fldLft . '+2')
 | |
| 					->where($fldLft . ' > ' . $db->q($myRight))
 | |
| 			)->execute();
 | |
| 
 | |
| 			$this->store();
 | |
| 
 | |
|             // Commit the transaction
 | |
|             $db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for insertRightOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $siblingNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 */
 | |
| 	public function insertAsSiblingOf(F0FTableNested &$siblingNode)
 | |
| 	{
 | |
| 		return $this->insertRightOf($siblingNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Move the current node (and its subtree) one position to the left in the tree, i.e. before its left-hand sibling
 | |
| 	 *
 | |
|      * @throws  RuntimeException
 | |
|      *
 | |
| 	 * @return $this
 | |
| 	 */
 | |
| 	public function moveLeft()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		// If it is a root node we will not move the node (roots don't participate in tree ordering)
 | |
| 		if ($this->isRoot())
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		// Are we already the leftmost node?
 | |
| 		$parentNode = $this->getParent();
 | |
| 
 | |
| 		if ($parentNode->lft == ($this->lft - 1))
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		// Get the sibling to the left
 | |
| 		$db = $this->getDbo();
 | |
| 		$table = $this->getClone();
 | |
| 		$table->reset();
 | |
| 		$leftSibling = $table->whereRaw($db->qn($this->getColumnAlias('rgt')) . ' = ' . $db->q($this->lft - 1))
 | |
| 			->get(0, 1)->current();
 | |
| 
 | |
| 		// Move the node
 | |
| 		if (is_object($leftSibling) && ($leftSibling instanceof F0FTableNested))
 | |
| 		{
 | |
| 			return $this->moveToLeftOf($leftSibling);
 | |
| 		}
 | |
| 
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Move the current node (and its subtree) one position to the right in the tree, i.e. after its right-hand sibling
 | |
|      *
 | |
|      * @throws RuntimeException
 | |
| 	 *
 | |
| 	 * @return $this
 | |
| 	 */
 | |
| 	public function moveRight()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		// If it is a root node we will not move the node (roots don't participate in tree ordering)
 | |
| 		if ($this->isRoot())
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		// Are we already the rightmost node?
 | |
| 		$parentNode = $this->getParent();
 | |
| 
 | |
| 		if ($parentNode->rgt == ($this->rgt + 1))
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		// Get the sibling to the right
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$table = $this->getClone();
 | |
| 		$table->reset();
 | |
| 		$rightSibling = $table->whereRaw($db->qn($this->getColumnAlias('lft')) . ' = ' . $db->q($this->rgt + 1))
 | |
| 			->get(0, 1)->current();
 | |
| 
 | |
| 		// Move the node
 | |
| 		if (is_object($rightSibling) && ($rightSibling instanceof F0FTableNested))
 | |
| 		{
 | |
| 			return $this->moveToRightOf($rightSibling);
 | |
| 		}
 | |
| 
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Moves the current node (and its subtree) to the left of another node. The other node can be in a different
 | |
| 	 * position in the tree or even under a different root.
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $siblingNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function moveToLeftOf(F0FTableNested $siblingNode)
 | |
| 	{
 | |
|         // Sanity checks on current and sibling node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($siblingNode->lft >= $siblingNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the sibling node');
 | |
|         }
 | |
| 
 | |
| 		$db    = $this->getDbo();
 | |
| 		$left  = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$right = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		// Get node metrics
 | |
| 		$myLeft  = $this->lft;
 | |
| 		$myRight = $this->rgt;
 | |
| 		$myWidth = $myRight - $myLeft + 1;
 | |
| 
 | |
| 		// Get sibling metrics
 | |
| 		$sibLeft = $siblingNode->lft;
 | |
| 
 | |
| 		// Start the transaction
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			// Temporary remove subtree being moved
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set("$left = " . $db->q(0) . " - $left")
 | |
| 				->set("$right = " . $db->q(0) . " - $right")
 | |
| 				->where($left . ' >= ' . $db->q($myLeft))
 | |
| 				->where($right . ' <= ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Close hole left behind
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
 | |
| 				->where($left . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
 | |
| 				->where($right . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Make a hole for the new items
 | |
| 			$newSibLeft = ($sibLeft > $myRight) ? $sibLeft - $myWidth : $sibLeft;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
 | |
| 				->where($right . ' >= ' . $db->q($newSibLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
 | |
| 				->where($left . ' >= ' . $db->q($newSibLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Move node and subnodes
 | |
| 			$moveRight = $newSibLeft - $myLeft;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
 | |
| 				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
 | |
| 				->where($left . ' <= 0 - ' . $db->q($myLeft))
 | |
| 				->where($right . ' >= 0 - ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Commit the transaction
 | |
| 			$db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
|         // Let's load the record again to fetch the new values for lft and rgt
 | |
|         $this->load();
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Moves the current node (and its subtree) to the right of another node. The other node can be in a different
 | |
| 	 * position in the tree or even under a different root.
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $siblingNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function moveToRightOf(F0FTableNested $siblingNode)
 | |
| 	{
 | |
|         // Sanity checks on current and sibling node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($siblingNode->lft >= $siblingNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the sibling node');
 | |
|         }
 | |
| 
 | |
| 		$db    = $this->getDbo();
 | |
| 		$left  = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$right = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		// Get node metrics
 | |
| 		$myLeft  = $this->lft;
 | |
| 		$myRight = $this->rgt;
 | |
| 		$myWidth = $myRight - $myLeft + 1;
 | |
| 
 | |
| 		// Get parent metrics
 | |
| 		$sibRight = $siblingNode->rgt;
 | |
| 
 | |
| 		// Start the transaction
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			// Temporary remove subtree being moved
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set("$left = " . $db->q(0) . " - $left")
 | |
| 				->set("$right = " . $db->q(0) . " - $right")
 | |
| 				->where($left . ' >= ' . $db->q($myLeft))
 | |
| 				->where($right . ' <= ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Close hole left behind
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
 | |
| 				->where($left . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
 | |
| 				->where($right . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Make a hole for the new items
 | |
| 			$newSibRight = ($sibRight > $myRight) ? $sibRight - $myWidth : $sibRight;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
 | |
| 				->where($left . ' > ' . $db->q($newSibRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
 | |
| 				->where($right . ' > ' . $db->q($newSibRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Move node and subnodes
 | |
| 			$moveRight = ($sibRight > $myRight) ? $sibRight - $myRight : $sibRight - $myRight + $myWidth;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
 | |
| 				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
 | |
| 				->where($left . ' <= 0 - ' . $db->q($myLeft))
 | |
| 				->where($right . ' >= 0 - ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Commit the transaction
 | |
| 			$db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
|         // Let's load the record again to fetch the new values for lft and rgt
 | |
|         $this->load();
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for moveToRightOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $siblingNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 */
 | |
| 	public function makeNextSiblingOf(F0FTableNested $siblingNode)
 | |
| 	{
 | |
| 		return $this->moveToRightOf($siblingNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for makeNextSiblingOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $siblingNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 */
 | |
| 	public function makeSiblingOf(F0FTableNested $siblingNode)
 | |
| 	{
 | |
| 		return $this->makeNextSiblingOf($siblingNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for moveToLeftOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $siblingNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 */
 | |
| 	public function makePreviousSiblingOf(F0FTableNested $siblingNode)
 | |
| 	{
 | |
| 		return $this->moveToLeftOf($siblingNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Moves a node and its subtree as a the first (leftmost) child of $parentNode
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $parentNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 */
 | |
| 	public function makeFirstChildOf(F0FTableNested $parentNode)
 | |
| 	{
 | |
|         // Sanity checks on current and sibling node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($parentNode->lft >= $parentNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the parent node');
 | |
|         }
 | |
| 
 | |
| 		$db    = $this->getDbo();
 | |
| 		$left  = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$right = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		// Get node metrics
 | |
| 		$myLeft  = $this->lft;
 | |
| 		$myRight = $this->rgt;
 | |
| 		$myWidth = $myRight - $myLeft + 1;
 | |
| 
 | |
| 		// Get parent metrics
 | |
| 		$parentLeft = $parentNode->lft;
 | |
| 
 | |
| 		// Start the transaction
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			// Temporary remove subtree being moved
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set("$left = " . $db->q(0) . " - $left")
 | |
| 				->set("$right = " . $db->q(0) . " - $right")
 | |
| 				->where($left . ' >= ' . $db->q($myLeft))
 | |
| 				->where($right . ' <= ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Close hole left behind
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
 | |
| 				->where($left . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
 | |
| 				->where($right . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Make a hole for the new items
 | |
| 			$newParentLeft = ($parentLeft > $myRight) ? $parentLeft - $myWidth : $parentLeft;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
 | |
| 				->where($right . ' >= ' . $db->q($newParentLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
 | |
| 				->where($left . ' > ' . $db->q($newParentLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Move node and subnodes
 | |
| 			$moveRight = $newParentLeft - $myLeft + 1;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
 | |
| 				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
 | |
| 				->where($left . ' <= 0 - ' . $db->q($myLeft))
 | |
| 				->where($right . ' >= 0 - ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Commit the transaction
 | |
| 			$db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
|         // Let's load the record again to fetch the new values for lft and rgt
 | |
|         $this->load();
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Moves a node and its subtree as a the last (rightmost) child of $parentNode
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $parentNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 *
 | |
| 	 * @throws Exception
 | |
| 	 * @throws RuntimeException
 | |
| 	 */
 | |
| 	public function makeLastChildOf(F0FTableNested $parentNode)
 | |
| 	{
 | |
|         // Sanity checks on current and sibling node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($parentNode->lft >= $parentNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the parent node');
 | |
|         }
 | |
| 
 | |
| 		$db    = $this->getDbo();
 | |
| 		$left  = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$right = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		// Get node metrics
 | |
| 		$myLeft  = $this->lft;
 | |
| 		$myRight = $this->rgt;
 | |
| 		$myWidth = $myRight - $myLeft + 1;
 | |
| 
 | |
| 		// Get parent metrics
 | |
| 		$parentRight = $parentNode->rgt;
 | |
| 
 | |
| 		// Start the transaction
 | |
| 		$db->transactionStart();
 | |
| 
 | |
| 		try
 | |
| 		{
 | |
| 			// Temporary remove subtree being moved
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set("$left = " . $db->q(0) . " - $left")
 | |
| 				->set("$right = " . $db->q(0) . " - $right")
 | |
| 				->where($left . ' >= ' . $db->q($myLeft))
 | |
| 				->where($right . ' <= ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Close hole left behind
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
 | |
| 				->where($left . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
 | |
| 				->where($right . ' > ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Make a hole for the new items
 | |
| 			$newLeft = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
 | |
| 				->where($left . ' >= ' . $db->q($newLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
 | |
| 				->where($right . ' >= ' . $db->q($newLeft));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Move node and subnodes
 | |
| 			$moveRight = ($parentRight > $myRight) ? $parentRight - $myRight - 1 : $parentRight - $myRight - 1 + $myWidth;
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->update($db->qn($this->getTableName()))
 | |
| 				->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
 | |
| 				->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
 | |
| 				->where($left . ' <= 0 - ' . $db->q($myLeft))
 | |
| 				->where($right . ' >= 0 - ' . $db->q($myRight));
 | |
| 			$db->setQuery($query)->execute();
 | |
| 
 | |
| 			// Commit the transaction
 | |
| 			$db->transactionCommit();
 | |
| 		}
 | |
| 		catch (\Exception $e)
 | |
| 		{
 | |
| 			$db->transactionRollback();
 | |
| 
 | |
| 			throw $e;
 | |
| 		}
 | |
| 
 | |
|         // Let's load the record again to fetch the new values for lft and rgt
 | |
|         $this->load();
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for makeLastChildOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $parentNode
 | |
| 	 *
 | |
| 	 * @return $this for chaining
 | |
| 	 */
 | |
| 	public function makeChildOf(F0FTableNested $parentNode)
 | |
| 	{
 | |
| 		return $this->makeLastChildOf($parentNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Makes the current node a root (and moving its entire subtree along the way). This is achieved by moving the node
 | |
| 	 * to the right of its root node
 | |
| 	 *
 | |
| 	 * @return  $this  for chaining
 | |
| 	 */
 | |
| 	public function makeRoot()
 | |
| 	{
 | |
| 		// Make sure we are not a root
 | |
| 		if ($this->isRoot())
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		// Get a reference to my root
 | |
| 		$myRoot = $this->getRoot();
 | |
| 
 | |
| 		// Double check I am not a root
 | |
| 		if ($this->equals($myRoot))
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		// Move myself to the right of my root
 | |
| 		$this->moveToRightOf($myRoot);
 | |
| 		$this->treeDepth = 0;
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Gets the level (depth) of this node in the tree. The result is cached in $this->treeDepth for faster retrieval.
 | |
| 	 *
 | |
|      * @throws  RuntimeException
 | |
|      *
 | |
| 	 * @return int|mixed
 | |
| 	 */
 | |
| 	public function getLevel()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		if (is_null($this->treeDepth))
 | |
| 		{
 | |
| 			$db = $this->getDbo();
 | |
| 
 | |
| 			$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 			$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->select('(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'))
 | |
| 				->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
 | |
| 				->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
 | |
| 				->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
 | |
| 				->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
 | |
| 				->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
 | |
| 				->group($db->qn('node') . '.' . $fldLft)
 | |
| 				->order($db->qn('node') . '.' . $fldLft . ' ASC');
 | |
| 
 | |
| 			$this->treeDepth = $db->setQuery($query, 0, 1)->loadResult();
 | |
| 		}
 | |
| 
 | |
| 		return $this->treeDepth;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the immediate parent of the current node
 | |
| 	 *
 | |
|      * @throws RuntimeException
 | |
|      *
 | |
| 	 * @return F0FTableNested
 | |
| 	 */
 | |
| 	public function getParent()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		if ($this->isRoot())
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		if (empty($this->treeParent) || !is_object($this->treeParent) || !($this->treeParent instanceof F0FTableNested))
 | |
| 		{
 | |
| 			$db = $this->getDbo();
 | |
| 
 | |
| 			$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 			$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 			$query = $db->getQuery(true)
 | |
| 				->select($db->qn('parent') . '.' . $fldLft)
 | |
| 				->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
 | |
| 				->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
 | |
| 				->where($db->qn('node') . '.' . $fldLft . ' > ' . $db->qn('parent') . '.' . $fldLft)
 | |
| 				->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
 | |
| 				->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
 | |
| 				->order($db->qn('parent') . '.' . $fldLft . ' DESC');
 | |
| 			$targetLft = $db->setQuery($query, 0, 1)->loadResult();
 | |
| 
 | |
| 			$table = $this->getClone();
 | |
| 			$table->reset();
 | |
| 			$this->treeParent = $table
 | |
| 				->whereRaw($fldLft . ' = ' . $db->q($targetLft))
 | |
| 				->get()->current();
 | |
| 		}
 | |
| 
 | |
| 		return $this->treeParent;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Is this a top-level root node?
 | |
| 	 *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isRoot()
 | |
| 	{
 | |
| 		// If lft=1 it is necessarily a root node
 | |
| 		if ($this->lft == 1)
 | |
| 		{
 | |
| 			return true;
 | |
| 		}
 | |
| 
 | |
| 		// Otherwise make sure its level is 0
 | |
| 		return $this->getLevel() == 0;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Is this a leaf node (a node without children)?
 | |
| 	 *
 | |
|      * @throws  RuntimeException
 | |
|      *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isLeaf()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		return ($this->rgt - 1) == $this->lft;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Is this a child node (not root)?
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isChild()
 | |
| 	{
 | |
| 		return !$this->isRoot();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns true if we are a descendant of $otherNode
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $otherNode
 | |
| 	 *
 | |
|      * @throws  RuntimeException
 | |
|      *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isDescendantOf(F0FTableNested $otherNode)
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($otherNode->lft >= $otherNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the other node');
 | |
|         }
 | |
| 
 | |
| 		return ($otherNode->lft < $this->lft) && ($otherNode->rgt > $this->rgt);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns true if $otherNode is ourselves or if we are a descendant of $otherNode
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $otherNode
 | |
| 	 *
 | |
|      * @throws  RuntimeException
 | |
|      *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isSelfOrDescendantOf(F0FTableNested $otherNode)
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($otherNode->lft >= $otherNode->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the other node');
 | |
|         }
 | |
| 
 | |
| 		return ($otherNode->lft <= $this->lft) && ($otherNode->rgt >= $this->rgt);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns true if we are an ancestor of $otherNode
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $otherNode
 | |
| 	 *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isAncestorOf(F0FTableNested $otherNode)
 | |
| 	{
 | |
| 		return $otherNode->isDescendantOf($this);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns true if $otherNode is ourselves or we are an ancestor of $otherNode
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $otherNode
 | |
| 	 *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function isSelfOrAncestorOf(F0FTableNested $otherNode)
 | |
| 	{
 | |
| 		return $otherNode->isSelfOrDescendantOf($this);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Is $node this very node?
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $node
 | |
| 	 *
 | |
|      * @throws  RuntimeException
 | |
|      *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function equals(F0FTableNested &$node)
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
|         if($node->lft >= $node->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the other node');
 | |
|         }
 | |
| 
 | |
| 		return (
 | |
| 			($this->getId() == $node->getId())
 | |
| 			&& ($this->lft == $node->lft)
 | |
| 			&& ($this->rgt == $node->rgt)
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Alias for isDescendantOf
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
| 	 * @param F0FTableNested $otherNode
 | |
|      *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function insideSubtree(F0FTableNested $otherNode)
 | |
| 	{
 | |
| 		return $this->isDescendantOf($otherNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns true if both this node and $otherNode are root, leaf or child (same tree scope)
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $otherNode
 | |
| 	 *
 | |
| 	 * @return bool
 | |
| 	 */
 | |
| 	public function inSameScope(F0FTableNested $otherNode)
 | |
| 	{
 | |
| 		if ($this->isLeaf())
 | |
| 		{
 | |
| 			return $otherNode->isLeaf();
 | |
| 		}
 | |
| 		elseif ($this->isRoot())
 | |
| 		{
 | |
| 			return $otherNode->isRoot();
 | |
| 		}
 | |
| 		elseif ($this->isChild())
 | |
| 		{
 | |
| 			return $otherNode->isChild();
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			return false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return all ancestor nodes and ourselves
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeAncestorsAndSelf()
 | |
| 	{
 | |
| 		$this->treeNestedGet = true;
 | |
| 
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' >= ' . $db->qn('node') . '.' . $fldLft);
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' <= ' . $db->qn('node') . '.' . $fldRgt);
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return all ancestor nodes but not ourselves
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeAncestors()
 | |
| 	{
 | |
| 		$this->treeNestedGet = true;
 | |
| 
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' > ' . $db->qn('node') . '.' . $fldLft);
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' < ' . $db->qn('node') . '.' . $fldRgt);
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return all sibling nodes and ourselves
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeSiblingsAndSelf()
 | |
| 	{
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$parent = $this->getParent();
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->q($parent->lft));
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldRgt . ' < ' . $db->q($parent->rgt));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return all sibling nodes but not ourselves
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeSiblings()
 | |
| 	{
 | |
| 		$this->scopeSiblingsAndSelf();
 | |
| 		$this->scopeWithoutSelf();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return only leaf nodes
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeLeaves()
 | |
| 	{
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' = ' . $db->qn('node') . '.' . $fldRgt . ' - ' . $db->q(1));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return all descendants (even subtrees of subtrees!) and ourselves
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeDescendantsAndSelf()
 | |
| 	{
 | |
| 		$this->treeNestedGet = true;
 | |
| 
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft);
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt);
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will return all descendants (even subtrees of subtrees!) but not ourselves
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeDescendants()
 | |
| 	{
 | |
| 		$this->treeNestedGet = true;
 | |
| 
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->qn('parent') . '.' . $fldLft);
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' < ' . $db->qn('parent') . '.' . $fldRgt);
 | |
| 		$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will only return immediate descendants (first level children) of the current node
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeImmediateDescendants()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		$subQuery = $db->getQuery(true)
 | |
| 			->select(array(
 | |
| 				$db->qn('node') . '.' . $fldLft,
 | |
| 				'(COUNT(*) - 1) AS ' . $db->qn('depth')
 | |
| 			))
 | |
| 			->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
 | |
| 			->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
 | |
| 			->group($db->qn('node') . '.' . $fldLft)
 | |
| 			->order($db->qn('node') . '.' . $fldLft . ' ASC');
 | |
| 
 | |
| 		$query = $db->getQuery(true)
 | |
| 			->select(array(
 | |
| 				$db->qn('node') . '.' . $fldLft,
 | |
| 				'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - (' .
 | |
| 				$db->qn('sub_tree') . '.' . $db->qn('depth') . ' + 1)) AS ' . $db->qn('depth')
 | |
| 			))
 | |
| 			->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
 | |
| 			->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
 | |
| 			->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('sub_parent'))
 | |
| 			->join('CROSS', '(' . $subQuery . ') AS ' . $db->qn('sub_tree'))
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('sub_parent') . '.' . $fldLft)
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('sub_parent') . '.' . $fldRgt)
 | |
| 			->where($db->qn('sub_parent') . '.' . $fldLft . ' = ' . $db->qn('sub_tree') . '.' . $fldLft)
 | |
| 			->group($db->qn('node') . '.' . $fldLft)
 | |
| 			->having(array(
 | |
| 				$db->qn('depth') . ' > ' . $db->q(0),
 | |
| 				$db->qn('depth') . ' <= ' . $db->q(1),
 | |
| 			))
 | |
| 			->order($db->qn('node') . '.' . $fldLft . ' ASC');
 | |
| 
 | |
| 		$leftValues = $db->setQuery($query)->loadColumn();
 | |
| 
 | |
| 		if (empty($leftValues))
 | |
| 		{
 | |
| 			$leftValues = array(0);
 | |
| 		}
 | |
| 
 | |
| 		array_walk($leftValues, function (&$item, $key) use (&$db)
 | |
| 		{
 | |
| 			$item = $db->q($item);
 | |
| 		});
 | |
| 
 | |
| 		$this->whereRaw($db->qn('node') . '.' . $fldLft . ' IN (' . implode(',', $leftValues) . ')');
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will not return the selected node if it's part of the query results
 | |
| 	 *
 | |
| 	 * @param F0FTableNested $node The node to exclude from the results
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function withoutNode(F0FTableNested $node)
 | |
| 	{
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 
 | |
| 		$this->whereRaw('NOT(' . $db->qn('node') . '.' . $fldLft . ' = ' . $db->q($node->lft) . ')');
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will not return ourselves if it's part of the query results
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeWithoutSelf()
 | |
| 	{
 | |
| 		$this->withoutNode($this);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * get() will not return our root if it's part of the query results
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	protected function scopeWithoutRoot()
 | |
| 	{
 | |
| 		$rootNode = $this->getRoot();
 | |
| 		$this->withoutNode($rootNode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the root node of the tree this node belongs to
 | |
| 	 *
 | |
| 	 * @return self
 | |
| 	 *
 | |
| 	 * @throws \RuntimeException
 | |
| 	 */
 | |
| 	public function getRoot()
 | |
| 	{
 | |
|         // Sanity checks on current node position
 | |
|         if($this->lft >= $this->rgt)
 | |
|         {
 | |
|             throw new RuntimeException('Invalid position values for the current node');
 | |
|         }
 | |
| 
 | |
| 		// If this is a root node return itself (there is no such thing as the root of a root node)
 | |
| 		if ($this->isRoot())
 | |
| 		{
 | |
| 			return $this;
 | |
| 		}
 | |
| 
 | |
| 		if (empty($this->treeRoot) || !is_object($this->treeRoot) || !($this->treeRoot instanceof F0FTableNested))
 | |
| 		{
 | |
| 			$this->treeRoot = null;
 | |
| 
 | |
| 			// First try to get the record with the minimum ID
 | |
| 			$db = $this->getDbo();
 | |
| 
 | |
| 			$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 			$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 			$subQuery = $db->getQuery(true)
 | |
| 				->select('MIN(' . $fldLft . ')')
 | |
| 				->from($db->qn($this->getTableName()));
 | |
| 
 | |
| 			try
 | |
| 			{
 | |
| 				$table = $this->getClone();
 | |
| 				$table->reset();
 | |
| 				$root = $table
 | |
| 					->whereRaw($fldLft . ' = (' . (string)$subQuery . ')')
 | |
| 					->get(0, 1)->current();
 | |
| 
 | |
| 				if ($this->isDescendantOf($root))
 | |
| 				{
 | |
| 					$this->treeRoot = $root;
 | |
| 				}
 | |
| 			}
 | |
| 			catch (\RuntimeException $e)
 | |
| 			{
 | |
| 				// If there is no root found throw an exception. Basically: your table is FUBAR.
 | |
| 				throw new \RuntimeException("No root found for table " . $this->getTableName() . ", node lft=" . $this->lft);
 | |
| 			}
 | |
| 
 | |
| 			// If the above method didn't work, get all roots and select the one with the appropriate lft/rgt values
 | |
| 			if (is_null($this->treeRoot))
 | |
| 			{
 | |
| 				// Find the node with depth = 0, lft < our lft and rgt > our right. That's our root node.
 | |
| 				$query = $db->getQuery(true)
 | |
| 					->select(array(
 | |
|                         $db->qn('node') . '.' . $fldLft,
 | |
| 						'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth')
 | |
| 					))
 | |
| 					->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
 | |
| 					->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
 | |
| 					->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
 | |
| 					->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
 | |
| 					->where($db->qn('node') . '.' . $fldLft . ' < ' . $db->q($this->lft))
 | |
| 					->where($db->qn('node') . '.' . $fldRgt . ' > ' . $db->q($this->rgt))
 | |
| 					->having($db->qn('depth') . ' = ' . $db->q(0))
 | |
| 					->group($db->qn('node') . '.' . $fldLft);
 | |
| 
 | |
| 				// Get the lft value
 | |
| 				$targetLeft = $db->setQuery($query)->loadResult();
 | |
| 
 | |
| 				if (empty($targetLeft))
 | |
| 				{
 | |
| 					// If there is no root found throw an exception. Basically: your table is FUBAR.
 | |
| 					throw new \RuntimeException("No root found for table " . $this->getTableName() . ", node lft=" . $this->lft);
 | |
| 				}
 | |
| 
 | |
| 				try
 | |
| 				{
 | |
| 					$table = $this->getClone();
 | |
| 					$table->reset();
 | |
| 					$this->treeRoot = $table
 | |
| 						->whereRaw($fldLft . ' = ' . $db->q($targetLeft))
 | |
| 						->get(0, 1)->current();
 | |
| 				}
 | |
| 				catch (\RuntimeException $e)
 | |
| 				{
 | |
| 					// If there is no root found throw an exception. Basically: your table is FUBAR.
 | |
| 					throw new \RuntimeException("No root found for table " . $this->getTableName() . ", node lft=" . $this->lft);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return $this->treeRoot;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all ancestors to this node and the node itself. In other words it gets the full path to the node and the node
 | |
| 	 * itself.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getAncestorsAndSelf()
 | |
| 	{
 | |
| 		$this->scopeAncestorsAndSelf();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all ancestors to this node and the node itself, but not the root node. If you want to
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getAncestorsAndSelfWithoutRoot()
 | |
| 	{
 | |
| 		$this->scopeAncestorsAndSelf();
 | |
| 		$this->scopeWithoutRoot();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all ancestors to this node but not the node itself. In other words it gets the path to the node, without the
 | |
| 	 * node itself.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getAncestors()
 | |
| 	{
 | |
| 		$this->scopeAncestorsAndSelf();
 | |
| 		$this->scopeWithoutSelf();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all ancestors to this node but not the node itself and its root.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getAncestorsWithoutRoot()
 | |
| 	{
 | |
| 		$this->scopeAncestors();
 | |
| 		$this->scopeWithoutRoot();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all sibling nodes, including ourselves
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getSiblingsAndSelf()
 | |
| 	{
 | |
| 		$this->scopeSiblingsAndSelf();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all sibling nodes, except ourselves
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getSiblings()
 | |
| 	{
 | |
| 		$this->scopeSiblings();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all leaf nodes in the tree. You may want to use the scopes to narrow down the search in a specific subtree or
 | |
| 	 * path.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getLeaves()
 | |
| 	{
 | |
| 		$this->scopeLeaves();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get all descendant (children) nodes and ourselves.
 | |
| 	 *
 | |
| 	 * Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getDescendantsAndSelf()
 | |
| 	{
 | |
| 		$this->scopeDescendantsAndSelf();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get only our descendant (children) nodes, not ourselves.
 | |
| 	 *
 | |
| 	 * Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getDescendants()
 | |
| 	{
 | |
| 		$this->scopeDescendants();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the immediate descendants (children). Unlike getDescendants it only goes one level deep into the tree
 | |
| 	 * structure. Descendants of descendant nodes will not be returned.
 | |
| 	 *
 | |
|      * @codeCoverageIgnore
 | |
|      *
 | |
| 	 * @return F0FDatabaseIterator
 | |
| 	 */
 | |
| 	public function getImmediateDescendants()
 | |
| 	{
 | |
| 		$this->scopeImmediateDescendants();
 | |
| 
 | |
| 		return $this->get();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns a hashed array where each element's key is the value of the $key column (default: the ID column of the
 | |
| 	 * table) and its value is the value of the $column column (default: title). Each nesting level will have the value
 | |
| 	 * of the $column column prefixed by a number of $separator strings, as many as its nesting level (depth).
 | |
| 	 *
 | |
| 	 * This is useful for creating HTML select elements showing the hierarchy in a human readable format.
 | |
| 	 *
 | |
| 	 * @param string $column
 | |
| 	 * @param null   $key
 | |
| 	 * @param string $seperator
 | |
| 	 *
 | |
| 	 * @return array
 | |
| 	 */
 | |
| 	public function getNestedList($column = 'title', $key = null, $seperator = '  ')
 | |
| 	{
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$fldLft = $db->qn($this->getColumnAlias('lft'));
 | |
| 		$fldRgt = $db->qn($this->getColumnAlias('rgt'));
 | |
| 
 | |
| 		if (empty($key) || !$this->hasField($key))
 | |
| 		{
 | |
| 			$key = $this->getKeyName();
 | |
| 		}
 | |
| 
 | |
| 		if (empty($column))
 | |
| 		{
 | |
| 			$column = 'title';
 | |
| 		}
 | |
| 
 | |
| 		$fldKey = $db->qn($this->getColumnAlias($key));
 | |
| 		$fldColumn = $db->qn($this->getColumnAlias($column));
 | |
| 
 | |
| 		$query = $db->getQuery(true)
 | |
| 			->select(array(
 | |
| 				$db->qn('node') . '.' . $fldKey,
 | |
| 				$db->qn('node') . '.' . $fldColumn,
 | |
| 				'(COUNT(' . $db->qn('parent') . '.' . $fldKey . ') - 1) AS ' . $db->qn('depth')
 | |
| 			))
 | |
| 			->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
 | |
| 			->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
 | |
| 			->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
 | |
| 			->group($db->qn('node') . '.' . $fldLft)
 | |
| 			->order($db->qn('node') . '.' . $fldLft . ' ASC');
 | |
| 
 | |
| 		$tempResults = $db->setQuery($query)->loadAssocList();
 | |
| 		$ret = array();
 | |
| 
 | |
| 		if (!empty($tempResults))
 | |
| 		{
 | |
| 			foreach ($tempResults as $row)
 | |
| 			{
 | |
| 				$ret[$row[$key]] = str_repeat($seperator, $row['depth']) . $row[$column];
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return $ret;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Locate a node from a given path, e.g. "/some/other/leaf"
 | |
| 	 *
 | |
| 	 * Notes:
 | |
| 	 * - This will only work when you have a "slug" and a "hash" field in your table.
 | |
| 	 * - If the path starts with "/" we will use the root with lft=1. Otherwise the first component of the path is
 | |
| 	 *   supposed to be the slug of the root node.
 | |
| 	 * - If the root node is not found you'll get null as the return value
 | |
| 	 * - You will also get null if any component of the path is not found
 | |
| 	 *
 | |
| 	 * @param string $path The path to locate
 | |
| 	 *
 | |
| 	 * @return F0FTableNested|null The found node or null if nothing is found
 | |
| 	 */
 | |
| 	public function findByPath($path)
 | |
| 	{
 | |
| 		// @todo
 | |
| 	}
 | |
| 
 | |
| 	public function isValid()
 | |
| 	{
 | |
| 		// @todo
 | |
| 	}
 | |
| 
 | |
| 	public function rebuild()
 | |
| 	{
 | |
| 		// @todo
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Resets cached values used to speed up querying the tree
 | |
| 	 *
 | |
| 	 * @return  static  for chaining
 | |
| 	 */
 | |
| 	protected function resetTreeCache()
 | |
| 	{
 | |
| 		$this->treeDepth = null;
 | |
| 		$this->treeRoot = null;
 | |
| 		$this->treeParent = null;
 | |
| 		$this->treeNestedGet = false;
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add custom, pre-compiled WHERE clauses for use in buildQuery. The raw WHERE clause you specify is added as is to
 | |
| 	 * the query generated by buildQuery. You are responsible for quoting and escaping the field names and data found
 | |
| 	 * inside the WHERE clause.
 | |
| 	 *
 | |
| 	 * @param   string $rawWhereClause The raw WHERE clause to add
 | |
| 	 *
 | |
| 	 * @return  $this  For chaining
 | |
| 	 */
 | |
| 	public function whereRaw($rawWhereClause)
 | |
| 	{
 | |
| 		$this->whereClauses[] = $rawWhereClause;
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Builds the query for the get() method
 | |
| 	 *
 | |
| 	 * @return F0FDatabaseQuery
 | |
| 	 */
 | |
| 	protected function buildQuery()
 | |
| 	{
 | |
| 		$db = $this->getDbo();
 | |
| 
 | |
| 		$query = $db->getQuery(true)
 | |
| 			->select($db->qn('node') . '.*')
 | |
| 			->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'));
 | |
| 
 | |
| 		if ($this->treeNestedGet)
 | |
| 		{
 | |
| 			$query
 | |
| 				->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'));
 | |
| 		}
 | |
| 
 | |
| 		// Apply custom WHERE clauses
 | |
| 		if (count($this->whereClauses))
 | |
| 		{
 | |
| 			foreach ($this->whereClauses as $clause)
 | |
| 			{
 | |
| 				$query->where($clause);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return $query;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns a database iterator to retrieve records. Use the scope methods and the whereRaw method to define what
 | |
| 	 * exactly will be returned.
 | |
| 	 *
 | |
| 	 * @param   integer $limitstart How many items to skip from the start, only when $overrideLimits = true
 | |
| 	 * @param   integer $limit      How many items to return, only when $overrideLimits = true
 | |
| 	 *
 | |
| 	 * @return  F0FDatabaseIterator  The data collection
 | |
| 	 */
 | |
| 	public function get($limitstart = 0, $limit = 0)
 | |
| 	{
 | |
| 		$limitstart = max($limitstart, 0);
 | |
| 		$limit = max($limit, 0);
 | |
| 
 | |
| 		$query = $this->buildQuery();
 | |
| 		$db = $this->getDbo();
 | |
| 		$db->setQuery($query, $limitstart, $limit);
 | |
| 		$cursor = $db->execute();
 | |
| 
 | |
| 		$dataCollection = F0FDatabaseIterator::getIterator($db->name, $cursor, null, $this->config['_table_class']);
 | |
| 
 | |
| 		return $dataCollection;
 | |
| 	}
 | |
| }
 |