PageRenderTime 63ms CodeModel.GetById 37ms app.highlight 19ms RepoModel.GetById 1ms app.codeStats 0ms

/application/datamapper/nestedsets.php

https://bitbucket.org/alexdeoliveira/ignitercms
PHP | 1397 lines | 630 code | 182 blank | 585 comment | 104 complexity | 1a7f7d12d3e6167a93b2cdd3e78c578c MD5 | raw file
   1<?php
   2
   3/**
   4 * Nested Sets Extension for DataMapper classes.
   5 *
   6 * Nested Sets DataMapper model
   7 *
   8 * @license 	MIT License
   9 * @package		DMZ-Included-Extensions
  10 * @category	DMZ
  11 * @author  	WanWizard
  12 * @info		Based on nstrees by Rolf Brugger, edutech
  13 * 				http://www.edutech.ch/contribution/nstrees
  14 * @version 	1.0
  15 */
  16
  17// --------------------------------------------------------------------------
  18
  19/**
  20 * DMZ_Nestedsets Class
  21 *
  22 * @package		DMZ-Included-Extensions
  23 */
  24class DMZ_Nestedsets {
  25
  26	/**
  27	 * name of the tree node left index field
  28	 *
  29	 * @var    string
  30	 * @access private
  31	 */
  32	private $_leftindex = 'left_id';
  33
  34	/**
  35	 * name of the tree node right index field
  36	 *
  37	 * @var    string
  38	 * @access private
  39	 */
  40	private $_rightindex = 'right_id';
  41
  42	/**
  43	 * name of the tree root id field. Used when the tree contains multiple roots
  44	 *
  45	 * @var    string
  46	 * @access private
  47	 */
  48	private $_rootfield = 'root_id';
  49
  50	/**
  51	 * value of the root field we need to filter on
  52	 *
  53	 * @var    string
  54	 * @access private
  55	 */
  56	private $_rootindex = NULL;
  57
  58	/**
  59	 * name of the tree node symlink index field
  60	 *
  61	 * @var    string
  62	 * @access private
  63	 */
  64	private $_symlinkindex = 'symlink_id';
  65
  66	/**
  67	 * name of the tree node name field, used to build a path string
  68	 *
  69	 * @var    string
  70	 * @access private
  71	 */
  72	private $_nodename = NULL;
  73
  74	/**
  75	 * indicates with pointers need to be used
  76	 *
  77	 * @var    string
  78	 * @access private
  79	 */
  80	private $use_symlink_pointers = TRUE;
  81
  82	// -----------------------------------------------------------------
  83
  84	/**
  85	 * Class constructor
  86	 *
  87	 * @param	mixed	optional, array of load-time options or NULL
  88	 * @param	object	the DataMapper object
  89	 * @return	void
  90	 * @access	public
  91	 */
  92	function __construct( $options = array(), $object = NULL )
  93	{
  94		// do we have the datamapper object
  95		if ( ! is_null($object) )
  96		{
  97			// update the config
  98			$this->tree_config($object, $options);
  99		}
 100	}
 101
 102	// -----------------------------------------------------------------
 103
 104	/**
 105	 * runtime configuration of this nestedsets tree
 106	 *
 107	 * @param	object	the DataMapper object
 108	 * @param	mixed	optional, array of options or NULL
 109	 * @return	object	the updated DataMapper object
 110	 * @access	public
 111	 */
 112	function tree_config($object, $options = array() )
 113	{
 114		// make sure the load-time options parameter is an array
 115		if ( ! is_array($options) )
 116		{
 117			$options = array();
 118		}
 119
 120		// make sure the model options parameter is an array
 121		if ( ! isset($object->nestedsets) OR ! is_array($object->nestedsets) )
 122		{
 123			$object->nestedsets = array();
 124		}
 125
 126		// loop through all options
 127		foreach( array( $object->nestedsets, $options ) as $optarray )
 128		{
 129			foreach( $optarray as $key => $value )
 130			{
 131				switch ( $key )
 132				{
 133					case 'name':
 134						$this->_nodename = (string) $value;
 135						break;
 136					case 'symlink':
 137						$this->_symlinkindex = (string) $value;
 138						break;
 139					case 'left':
 140						$this->_leftindex = (string) $value;
 141						break;
 142					case 'right':
 143						$this->_rightindex = (string) $value;
 144						break;
 145					case 'root':
 146						$this->_rootfield = (string) $value;
 147						break;
 148					case 'value':
 149						$this->_rootindex = (int) $value;
 150						break;
 151					case 'follow':
 152						$this->use_symlink_pointers = (bool) $value;
 153						break;
 154					default:
 155						break;
 156				}
 157			}
 158		}
 159	}
 160
 161	// -----------------------------------------------------------------
 162
 163	/**
 164	 * select a specific root if the table contains multiple trees
 165	 *
 166	 * @param	object	the DataMapper object
 167	 * @return	object	the updated DataMapper object
 168	 * @access	public
 169	 */
 170	function select_root($object, $tree = NULL)
 171	{
 172		// set the filter value
 173		$this->_rootindex = $tree;
 174
 175		// return the object
 176		return $object;
 177	}
 178
 179	// -----------------------------------------------------------------
 180	// Tree constructors
 181	// -----------------------------------------------------------------
 182
 183	/**
 184	 * create a new tree root
 185	 *
 186	 * @param	object	the DataMapper object
 187	 * @return	object	the updated DataMapper object
 188	 * @access	public
 189	 */
 190	function new_root($object)
 191	{
 192		// set the pointers for the root object
 193		$object->id = NULL;
 194		$object->{$this->_leftindex} = 1;
 195		$object->{$this->_rightindex} = 2;
 196
 197		// add a root index if needed
 198		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 199		{
 200			$object->{$this->_rootfield} = $this->_rootindex;
 201		}
 202
 203		// create the new tree root, and return the updated object
 204		return $this->_insertNew($object);
 205	}
 206
 207	// -----------------------------------------------------------------
 208
 209	/**
 210	 * creates a new first child of 'node'
 211	 *
 212	 * @param	object	the DataMapper object
 213	 * @param	object	the parent node
 214	 * @return	object	the updated DataMapper object
 215	 * @access	public
 216	 */
 217	function new_first_child($object, $node = NULL)
 218	{
 219		// a node passed?
 220		if ( is_null($node) )
 221		{
 222			// no, use the object itself
 223			$node = $object->get_clone();
 224		}
 225
 226		// we need a valid node for this to work
 227		if ( ! $node->exists() )
 228		{
 229			return $node;
 230		}
 231
 232		// set the pointers for the root object
 233		$object->id = NULL;
 234		$object->{$this->_leftindex} = $node->{$this->_leftindex} + 1;
 235		$object->{$this->_rightindex} = $node->{$this->_leftindex} + 2;
 236
 237		// add a root index if needed
 238		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 239		{
 240			$object->{$this->_rootfield} = $this->_rootindex;
 241		}
 242
 243		// shift nodes to make room for the new child
 244		$this->_shiftRLValues($node, $object->{$this->_leftindex}, 2);
 245
 246		// create the new tree node, and return the updated object
 247		return $this->_insertNew($object);
 248	}
 249
 250	// -----------------------------------------------------------------
 251
 252	/**
 253	 * creates a new last child of 'node'
 254	 *
 255	 * @param	object	the DataMapper object
 256	 * @param	object	the parent node
 257	 * @return	object	the updated DataMapper object
 258	 * @access	public
 259	 */
 260	function new_last_child($object, $node = NULL)
 261	{
 262		// a node passed?
 263		if ( is_null($node) )
 264		{
 265			// no, use the object itself
 266			$node = $object->get_clone();
 267		}
 268
 269		// we need a valid node for this to work
 270		if ( ! $node->exists() )
 271		{
 272			return $node;
 273		}
 274
 275		// set the pointers for the root object
 276		$object->id = NULL;
 277		$object->{$this->_leftindex} = $node->{$this->_rightindex};
 278		$object->{$this->_rightindex} = $node->{$this->_rightindex} + 1;
 279
 280		// add a root index if needed
 281		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 282		{
 283			$object->{$this->_rootfield} = $this->_rootindex;
 284		}
 285
 286		// shift nodes to make room for the new child
 287		$this->_shiftRLValues($node, $object->{$this->_leftindex}, 2);
 288
 289		// create the new tree node, and return the updated object
 290		return $this->_insertNew($object);
 291	}
 292
 293	// -----------------------------------------------------------------
 294
 295	/**
 296	 * creates a new sibling before 'node'
 297	 *
 298	 * @param	object	the DataMapper object
 299	 * @param	object	the sibling node
 300	 * @return	object	the updated DataMapper object
 301	 * @access	public
 302	 */
 303	function new_previous_sibling($object, $node = NULL)
 304	{
 305		// a node passed?
 306		if ( is_null($node) )
 307		{
 308			// no, use the object itself
 309			$node = $object->get_clone();
 310		}
 311
 312		// we need a valid node for this to work
 313		if ( ! $node->exists() )
 314		{
 315			return $node;
 316		}
 317
 318		// set the pointers for the root object
 319		$object->id = NULL;
 320		$object->{$this->_leftindex} = $node->{$this->_leftindex};
 321		$object->{$this->_rightindex} = $node->{$this->_leftindex} + 1;
 322
 323		// add a root index if needed
 324		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 325		{
 326			$object->{$this->_rootfield} = $this->_rootindex;
 327		}
 328
 329		// shift nodes to make room for the new sibling
 330		$this->_shiftRLValues($node, $object->{$this->_leftindex}, 2);
 331
 332		// create the new tree node, and return the updated object
 333		return $this->_insertNew($object);
 334	}
 335
 336	// -----------------------------------------------------------------
 337
 338	/**
 339	 * creates a new sibling after 'node'
 340	 *
 341	 * @param	object	the DataMapper object
 342	 * @param	object	the sibling node
 343	 * @return	object	the updated DataMapper object
 344	 * @access	public
 345	 */
 346	function new_next_sibling($object, $node = NULL)
 347	{
 348		// a node passed?
 349		if ( is_null($node) )
 350		{
 351			// no, use the object itself
 352			$node = $object->get_clone();
 353		}
 354
 355		// we need a valid node for this to work
 356		if ( ! $node->exists() )
 357		{
 358			return $node;
 359		}
 360
 361		// set the pointers for the root object
 362		$object->id = NULL;
 363		$object->{$this->_leftindex} = $node->{$this->_rightindex} + 1;
 364		$object->{$this->_rightindex} = $node->{$this->_rightindex} + 2;
 365
 366		// add a root index if needed
 367		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 368		{
 369			$object->{$this->_rootfield} = $this->_rootindex;
 370		}
 371
 372		// shift nodes to make room for the new sibling
 373		$this->_shiftRLValues($node, $object->{$this->_leftindex}, 2);
 374
 375		// create the new tree node, and return the updated object
 376		return $this->_insertNew($object);
 377	}
 378
 379	// -----------------------------------------------------------------
 380	// Tree queries
 381	// -----------------------------------------------------------------
 382
 383
 384	/**
 385	 * returns the root of the (selected) tree
 386	 *
 387	 * @param	object	the DataMapper object
 388	 * @return	object	the updated DataMapper object
 389	 * @access	public
 390	 */
 391	function get_root($object)
 392	{
 393		// add a root index if needed
 394		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 395		{
 396			$object->db->where($this->_rootfield, $this->_rootindex);
 397		}
 398
 399		// get the tree's root node
 400		return $object->where($this->_leftindex, 1)->get();
 401	}
 402
 403	// -----------------------------------------------------------------
 404
 405	/**
 406	 * returns the parent of the child 'node'
 407	 *
 408	 * @param	object	the DataMapper object
 409	 * @param	object	the child node
 410	 * @return	object	the updated DataMapper object
 411	 * @access	public
 412	 */
 413	function get_parent($object, $node = NULL)
 414	{
 415		// a node passed?
 416		if ( is_null($node) )
 417		{
 418			// no, use the object itself
 419			$node =& $object;
 420		}
 421
 422		// we need a valid node for this to work
 423		if ( ! $node->exists() )
 424		{
 425			return $node;
 426		}
 427
 428		// add a root index if needed
 429		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 430		{
 431			$object->db->where($this->_rootfield, $this->_rootindex);
 432		}
 433
 434		// get the node's parent node
 435		$object->where($this->_leftindex . ' <', $node->{$this->_leftindex});
 436		$object->where($this->_rightindex . ' >', $node->{$this->_rightindex});
 437		return $object->order_by($this->_rightindex, 'asc')->limit(1)->get();
 438	}
 439
 440	// -----------------------------------------------------------------
 441
 442	/**
 443	 * returns the node with the requested left index pointer
 444	 *
 445	 * @param	object	the DataMapper object
 446	 * @param	integer	a node's left index value
 447	 * @return	object	the updated DataMapper object
 448	 * @access	public
 449	 */
 450	function get_node_where_left($object, $left_id)
 451	{
 452		// add a root index if needed
 453		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 454		{
 455			$object->db->where($this->_rootfield, $this->_rootindex);
 456		}
 457
 458		// get the node's parent node
 459		$object->where($this->_leftindex, $left_id);
 460		return $object->get();
 461	}
 462
 463	// -----------------------------------------------------------------
 464
 465	/**
 466	 * returns the node with the requested right index pointer
 467	 *
 468	 * @param	object	the DataMapper object
 469	 * @param	integer	a node's right index value
 470	 * @return	object	the updated DataMapper object
 471	 * @access	public
 472	 */
 473	function get_node_where_right($object, $right_id)
 474	{
 475		// add a root index if needed
 476		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 477		{
 478			$object->db->where($this->_rootfield, $this->_rootindex);
 479		}
 480
 481		// get the node's parent node
 482		$object->where($this->_rightindex, $right_id);
 483		return $object->get();
 484	}
 485
 486	// -----------------------------------------------------------------
 487
 488	/**
 489	 * returns the first child of the given node
 490	 *
 491	 * @param	object	the DataMapper object
 492	 * @param	object	the parent node
 493	 * @return	object	the updated DataMapper object
 494	 * @access	public
 495	 */
 496	function get_first_child($object, $node = NULL)
 497	{
 498		// a node passed?
 499		if ( is_null($node) )
 500		{
 501			// no, use the object itself
 502			$node =& $object;
 503		}
 504
 505		// we need a valid node for this to work
 506		if ( ! $node->exists() )
 507		{
 508			return $node;
 509		}
 510
 511		// add a root index if needed
 512		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 513		{
 514			$object->db->where($this->_rootfield, $this->_rootindex);
 515		}
 516
 517		// get the node's first child node
 518		$object->where($this->_leftindex, $node->{$this->_leftindex}+1);
 519		return $object->get();
 520	}
 521
 522	// -----------------------------------------------------------------
 523
 524	/**
 525	 * returns the last child of the given node
 526	 *
 527	 * @param	object	the DataMapper object
 528	 * @param	object	the parent node
 529	 * @return	object	the updated DataMapper object
 530	 * @access	public
 531	 */
 532	function get_last_child($object, $node = NULL)
 533	{
 534		// a node passed?
 535		if ( is_null($node) )
 536		{
 537			// no, use the object itself
 538			$node =& $object;
 539		}
 540
 541		// we need a valid node for this to work
 542		if ( ! $node->exists() )
 543		{
 544			return $node;
 545		}
 546
 547		// add a root index if needed
 548		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 549		{
 550			$object->db->where($this->_rootfield, $this->_rootindex);
 551		}
 552
 553		// get the node's last child node
 554		$object->where($this->_rightindex, $node->{$this->_rightindex}-1);
 555		return $object->get();
 556	}
 557
 558	// -----------------------------------------------------------------
 559
 560	/**
 561	 * returns the previous sibling of the given node
 562	 *
 563	 * @param	object	the DataMapper object
 564	 * @param	object	the sibling node
 565	 * @return	object	the updated DataMapper object
 566	 * @access	public
 567	 */
 568	function get_previous_sibling($object, $node = NULL)
 569	{
 570		// a node passed?
 571		if ( is_null($node) )
 572		{
 573			// no, use the object itself
 574			$node =& $object;
 575		}
 576
 577		// we need a valid node for this to work
 578		if ( ! $node->exists() )
 579		{
 580			return $node;
 581		}
 582
 583		// add a root index if needed
 584		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 585		{
 586			$object->db->where($this->_rootfield, $this->_rootindex);
 587		}
 588
 589		// get the node's previous sibling node
 590		$object->where($this->_rightindex, $node->{$this->_leftindex}-1);
 591		return $object->get();
 592	}
 593
 594	// -----------------------------------------------------------------
 595
 596	/**
 597	 * returns the next sibling of the given node
 598	 *
 599	 * @param	object	the DataMapper object
 600	 * @param	object	the sibling node
 601	 * @return	object	the updated DataMapper object
 602	 * @access	public
 603	 */
 604	function get_next_sibling($object, $node = NULL)
 605	{
 606		// a node passed?
 607		if ( is_null($node) )
 608		{
 609			// no, use the object itself
 610			$node =& $object;
 611		}
 612
 613		// we need a valid node for this to work
 614		if ( ! $node->exists() )
 615		{
 616			return $node;
 617		}
 618
 619		// add a root index if needed
 620		if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 621		{
 622			$object->db->where($this->_rootfield, $this->_rootindex);
 623		}
 624
 625		// get the node's next sibling node
 626		$object->where($this->_leftindex, $node->{$this->_rightindex}+1);
 627		return $object->get();
 628	}
 629
 630	// -----------------------------------------------------------------
 631	// Boolean tree functions
 632	// -----------------------------------------------------------------
 633
 634	/**
 635	 * check if the object is a valid tree node
 636	 *
 637	 * @param	object	the DataMapper object
 638	 * @return	boolean
 639	 * @access	public
 640	 */
 641	function is_valid_node($object)
 642	{
 643		if ( ! $object->exists() )
 644		{
 645			return FALSE;
 646		}
 647		elseif ( ! isset($object->{$this->_leftindex}) OR ! is_numeric($object->{$this->_leftindex}) OR $object->{$this->_leftindex} <=0 )
 648		{
 649			return FALSE;
 650		}
 651		elseif ( ! isset($object->{$this->_rightindex}) OR ! is_numeric($object->{$this->_rightindex}) OR $object->{$this->_rightindex} <=0  )
 652		{
 653			return FALSE;
 654		}
 655		elseif ( $object->{$this->_leftindex} >= $object->{$this->_rightindex} )
 656		{
 657			return FALSE;
 658		}
 659		elseif ( ! empty($this->_rootfield) && ! in_array($this->_rootfield, $object->fields) )
 660		{
 661			return FALSE;
 662		}
 663		elseif ( ! empty($this->_rootfield) && ( ! is_numeric($object->{$this->_rootfield}) OR $object->{$this->_rootfield} <=0  ) )
 664		{
 665			return FALSE;
 666		}
 667
 668		// all looks well...
 669		return TRUE;
 670	}
 671
 672	// -----------------------------------------------------------------
 673
 674	/**
 675	 * check if the object is a tree root
 676	 *
 677	 * @param	object	the DataMapper object
 678	 * @return	boolean
 679	 * @access	public
 680	 */
 681	function is_root($object)
 682	{
 683		return ( $object->exists() && $this->is_valid_node($object) && $object->{$this->_leftindex} == 1 );
 684	}
 685
 686	// -----------------------------------------------------------------
 687
 688	/**
 689	 * check if the object is a tree leaf (node with no children)
 690	 *
 691	 * @param	object	the DataMapper object
 692	 * @return	boolean
 693	 * @access	public
 694	 */
 695	function is_leaf($object)
 696	{
 697		return ( $object->exists() && $this->is_valid_node($object) && $object->{$this->_rightindex} - $object->{$this->_leftindex} == 1 );
 698	}
 699
 700	// -----------------------------------------------------------------
 701
 702	/**
 703	 * check if the object is a child node
 704	 *
 705	 * @param	object	the DataMapper object
 706	 * @return	boolean
 707	 * @access	public
 708	 */
 709	function is_child($object)
 710	{
 711		return ( $object->exists() && $this->is_valid_node($object) && $object->{$this->_leftindex} > 1 );
 712	}
 713
 714	// -----------------------------------------------------------------
 715
 716	/**
 717	 * check if the object is a child of node
 718	 *
 719	 * @param	object	the DataMapper object
 720	 * @param	object	the parent node
 721	 * @return	boolean
 722	 * @access	public
 723	 */
 724	function is_child_of($object, $node = NULL)
 725	{
 726		// validate the objects
 727		if ( ! $this->is_valid_node($object) OR ! $this->is_valid_node($node) )		{
 728			return FALSE;
 729		}
 730
 731		return ( $object->{$this->_leftindex} > $node->{$this->_leftindex} && $object->{$this->_rightindex} < $node->{$this->_rightindex} );
 732	}
 733
 734	// -----------------------------------------------------------------
 735
 736	/**
 737	 * check if the object is the parent of node
 738	 *
 739	 * @param	object	the DataMapper object
 740	 * @param	object	the parent node
 741	 * @return	boolean
 742	 * @access	public
 743	 */
 744	function is_parent_of($object, $node = NULL)
 745	{
 746		// validate the objects
 747		if ( ! $this->is_valid_node($object) OR ! $this->is_valid_node($node) )
 748		{
 749			return FALSE;
 750		}
 751
 752		// fetch the parent of our child node
 753		$parent = $node->get_clone()->get_parent();
 754
 755		return ( $parent->id === $object->id );
 756	}
 757
 758	// -----------------------------------------------------------------
 759
 760	/**
 761	 * check if the object has a parent
 762	 *
 763	 * Note: this is an alias for is_child()
 764	 *
 765	 * @param	object	the DataMapper object
 766	 * @return	boolean
 767	 * @access	public
 768	 */
 769	function has_parent($object)
 770	{
 771		return $this->is_child($object);
 772	}
 773
 774	// -----------------------------------------------------------------
 775
 776	/**
 777	 * check if the object has children
 778	 *
 779	 * Note: this is an alias for ! is_leaf()
 780	 *
 781	 * @param	object	the DataMapper object
 782	 * @return	boolean
 783	 * @access	public
 784	 */
 785	function has_children($object)
 786	{
 787		return $this->is_leaf($object) ? FALSE : TRUE;
 788	}
 789
 790	// -----------------------------------------------------------------
 791
 792	/**
 793	 * check if the object has a previous silbling
 794	 *
 795	 * @param	object	the DataMapper object
 796	 * @return	boolean
 797	 * @access	public
 798	 */
 799	function has_previous_sibling($object)
 800	{
 801		// fetch the result using a clone
 802		$node = $object->get_clone();
 803		return $this->is_valid_node($node->get_previous_sibling($object));
 804	}
 805
 806	// -----------------------------------------------------------------
 807
 808	/**
 809	 * check if the object has a next silbling
 810	 *
 811	 * @param	object	the DataMapper object
 812	 * @return	boolean
 813	 * @access	public
 814	 */
 815	function has_next_sibling($object)
 816	{
 817		// fetch the result using a clone
 818		$node = $object->get_clone();
 819		return $this->is_valid_node($node->get_next_sibling($object));
 820	}
 821
 822	// -----------------------------------------------------------------
 823	// Integer tree functions
 824	// -----------------------------------------------------------------
 825
 826	/**
 827	 * return the count of the objects children
 828	 *
 829	 * @param	object	the DataMapper object
 830	 * @return	integer
 831	 * @access	public
 832	 */
 833	function count_children($object)
 834	{
 835		return ( $object->exists() ? (($object->{$this->_rightindex} - $object->{$this->_leftindex} - 1) / 2) : FALSE );
 836	}
 837
 838	// -----------------------------------------------------------------
 839
 840	/**
 841	 * return the node level, where the root = 0
 842	 *
 843	 * @param	object	the DataMapper object
 844	 * @return	mixed	integer, of FALSE in case no valid object was passed
 845	 * @access	public
 846	 */
 847	function level($object)
 848	{
 849		if ( $object->exists() )
 850		{
 851			// add a root index if needed
 852			if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
 853			{
 854				$object->db->where($this->_rootfield, $this->_rootindex);
 855			}
 856
 857			$object->where($this->_leftindex.' <', $object->{$this->_leftindex});
 858			$object->where($this->_rightindex.' >', $object->{$this->_rightindex});
 859			return $object->count();
 860		}
 861		else
 862		{
 863			return FALSE;
 864		}
 865	}
 866
 867	// -----------------------------------------------------------------
 868	// Tree reorganisation
 869	// -----------------------------------------------------------------
 870
 871	/**
 872	 * move the object as next sibling of 'node'
 873	 *
 874	 * @param	object	the DataMapper object
 875	 * @param	object	the sibling node
 876	 * @return	object	the updated DataMapper object
 877	 * @access	public
 878	 */
 879	function make_next_sibling_of($object, $node)
 880	{
 881		if ( ! $this->is_root($node) )
 882		{
 883			return $this->_moveSubtree($object, $node, $node->{$this->_rightindex}+1);
 884		}
 885		else
 886		{
 887			return FALSE;
 888		}
 889	}
 890
 891	// -----------------------------------------------------------------
 892
 893	/**
 894	 * move the object as previous sibling of 'node'
 895	 *
 896	 * @param	object	the DataMapper object
 897	 * @param	object	the sibling node
 898	 * @return	object	the updated DataMapper object
 899	 * @access	public
 900	 */
 901	function make_previous_sibling_of($object, $node)
 902	{
 903		if ( ! $this->is_root($node) )
 904		{
 905			return $this->_moveSubtree($object, $node, $node->{$this->_leftindex});
 906		}
 907		else
 908		{
 909			return FALSE;
 910		}
 911	}
 912
 913	// -----------------------------------------------------------------
 914
 915	/**
 916	 * move the object as first child of 'node'
 917	 *
 918	 * @param	object	the DataMapper object
 919	 * @param	object	the sibling node
 920	 * @return	object	the updated DataMapper object
 921	 * @access	public
 922	 */
 923	function make_first_child_of($object, $node)
 924	{
 925		return $this->_moveSubtree($object, $node, $node->{$this->_leftindex}+1);
 926	}
 927
 928	// -----------------------------------------------------------------
 929
 930	/**
 931	 * move the object as last child of 'node'
 932	 *
 933	 * @param	object	the DataMapper object
 934	 * @param	object	the sibling node
 935	 * @return	object	the updated DataMapper object
 936	 * @access	public
 937	 */
 938	function make_last_child_of($object, $node)
 939	{
 940		return $this->_moveSubtree($object, $node, $node->{$this->_rightindex});
 941	}
 942
 943	// -----------------------------------------------------------------
 944	// Tree destructors
 945	// -----------------------------------------------------------------
 946
 947	/**
 948	 * deletes the entire tree structure including all records
 949	 *
 950	 * @param	object	the DataMapper object
 951	 * @param	mixed	optional, id of the tree to delete
 952	 * @return	object	the updated DataMapper object
 953	 * @access	public
 954	 */
 955	function remove_tree($object, $tree_id = NULL)
 956	{
 957		// if we have multiple roots
 958		if ( in_array($this->_rootfield, $object->fields) )
 959		{
 960			// was a tree id passed?
 961			if ( ! is_null($tree_id) )
 962			{
 963				// only delete the selected one
 964				$object->db->where($this->_rootfield, $tree_id)->delete($object->table);
 965			}
 966			elseif ( ! is_null($this->_rootindex) )
 967			{
 968				// only delete the selected one
 969				$object->db->where($this->_rootfield, $this->_rootindex)->delete($object->table);
 970			}
 971			else
 972			{
 973				// delete them all
 974				$object->db->truncate($object->table);
 975			}
 976		}
 977		else
 978		{
 979			// delete them all
 980			$object->db->truncate($object->table);
 981		}
 982
 983		// return the cleared object
 984		return $object->clear();
 985	}
 986
 987	// -----------------------------------------------------------------
 988
 989	/**
 990	 * deletes the current object, and all childeren
 991	 *
 992	 * @param	object	the DataMapper object
 993	 * @return	object	the updated DataMapper object
 994	 * @access	public
 995	 */
 996	function remove_node($object)
 997	{
 998		// we need a valid node to do this
 999		if ( $object->exists() )
1000		{
1001			// if we have multiple roots
1002			if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
1003			{
1004				// only delete the selected one
1005				$object->db->where($this->_rootfield, $this->_rootindex);
1006			}
1007
1008			// clone the object, we need to it shift later
1009			$clone = $object->get_clone();
1010
1011			// select the node and all children
1012			$object->db->where($this->_leftindex . ' >=', $object->{$this->_leftindex});
1013			$object->db->where($this->_rightindex . ' <=', $object->{$this->_rightindex});
1014
1015			// delete them all
1016			$object->db->delete($object->table);
1017
1018			// re-index the tree
1019			$this->_shiftRLValues($clone, $object->{$this->_rightindex} + 1, $clone->{$this->_leftindex} - $object->{$this->_rightindex} -1);
1020		}
1021
1022		// return the cleared object
1023		return $object->clear();
1024	}
1025
1026	// -----------------------------------------------------------------
1027	// dump methods
1028	// -----------------------------------------------------------------
1029
1030	/**
1031	 * returns the tree in a key-value format suitable for html dropdowns
1032	 *
1033	 * @param	object	the DataMapper object
1034	 * @param	string	optional, name of the column to use
1035	 * @param	boolean	if true, the object itself (root of the dump) will not be included
1036	 * @return	array
1037	 * @access	public
1038	 */
1039	public function dump_dropdown($object, $field = FALSE, $skip_root = TRUE)
1040	{
1041		// check if a specific field has been requested
1042		if ( empty($field) OR ! in_array($field, $object->fields) )
1043		{
1044			// no field given, check if a generic name is defined
1045			if ( ! empty($this->_nodename) )
1046			{
1047				// yes, so use it
1048				$field = $this->_nodename;
1049			}
1050			else
1051			{
1052				// can't continue without a name
1053				return FALSE;
1054			}
1055		}
1056
1057		// fetch the tree as an array
1058		$tree = $this->dump_tree($object, NULL, 'array', $skip_root);
1059
1060		// storage for the result
1061		$result = array();
1062
1063		if ( $tree )
1064		{
1065			// loop trough the tree
1066			foreach ( $tree as $key => $value )
1067			{
1068				$result[$value['__id']] = str_repeat('&nbsp;', ($value['__level']) * 3) . ($value['__level'] ? '&raquo; ' : '') . $value[$field];
1069			}
1070		}
1071
1072		// return the result
1073		return $result;
1074	}
1075
1076	// -----------------------------------------------------------------
1077
1078	/**
1079	 * dumps the entire tree in HTML or TAB formatted output
1080	 *
1081	 * @param	object	the DataMapper object
1082	 * @param	array	list of columns to include in the dump
1083	 * @param	string	type of output requested, possible values 'html', 'tab', 'csv', 'array' ('array' = default)
1084	 * @param	boolean	if true, the object itself (root of the dump) will not be included
1085	 * @return	mixed
1086	 * @access	public
1087	 */
1088	public function dump_tree($object, $attributes = NULL, $type = 'array', $skip_root = TRUE)
1089	{
1090		if ( $this->is_valid_node($object) )
1091		{
1092			// do we need a sub-selection of attributes?
1093			if ( is_array($attributes) )
1094			{
1095				// make sure required fields are present
1096				$fields = array_merge($attributes, array('id', $this->_leftindex, $this->_rightindex));
1097				if ( ! empty($this->_nodename) && ! isset($fields[$this->_nodename] ) )
1098				{
1099					$fields[] = $this->_nodename;
1100				}
1101				// add a select
1102				$object->db->select($fields);
1103			}
1104
1105			// create the where clause for this query
1106			if ( $skip_root === TRUE )
1107			{
1108				// select only all children
1109				$object->db->where($this->_leftindex . ' >', $object->{$this->_leftindex});
1110				$object->db->where($this->_rightindex . ' <', $object->{$this->_rightindex});
1111				$level = -1;
1112			}
1113			else
1114			{
1115				// select the node and all children
1116				$object->db->where($this->_leftindex . ' >=', $object->{$this->_leftindex});
1117				$object->db->where($this->_rightindex . ' <=', $object->{$this->_rightindex});
1118				$level = -2;
1119			}
1120
1121			// if we have multiple roots
1122			if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
1123			{
1124				// only delete the selected one
1125				$object->db->where($this->_rootfield, $this->_rootindex);
1126			}
1127
1128			// fetch the result
1129			$result = $object->db->order_by($this->_leftindex)->get($object->table)->result_array();
1130
1131			// store the last left pointer
1132			$last_left = $object->{$this->_leftindex};
1133
1134			// create the path
1135			if ( ! empty($this->_nodename) )
1136			{
1137				$path = array( $object->{$this->_nodename} );
1138			}
1139			else
1140			{
1141				$path = array();
1142			}
1143
1144			// add level and path to the result
1145			foreach ( $result as $key => $value )
1146			{
1147				// for now, just store the ID
1148				$result[$key]['__id'] = $value['id'];
1149
1150				// calculate the nest level of this node
1151				$level += $last_left - $value[$this->_leftindex] + 2;
1152				$last_left = $value[$this->_leftindex];
1153				$result[$key]['__level'] = $level;
1154
1155				// create the relative path to this node
1156				$result[$key]['__path'] = '';
1157				if ( ! empty($this->_nodename) )
1158				{
1159					$path[$level] = $value[$this->_nodename];
1160					for ( $i = 0; $i <= $level; $i++ )
1161					{
1162						$result[$key]['__path'] .= '/' . $path[$i];
1163					}
1164				}
1165			}
1166
1167			// convert the result to output
1168			if ( in_array($type, array('tab', 'csv', 'html')) )
1169			{
1170				// storage for the result
1171				$convert = '';
1172
1173				// loop through the elements
1174				foreach ( $result as $key => $value )
1175				{
1176					// prefix based on requested type
1177					switch ($type)
1178					{
1179						case 'tab';
1180							$convert .= str_repeat("\t", $value['__level'] * 4 );
1181							break;
1182						case 'csv';
1183							break;
1184						case 'html';
1185							$convert .= str_repeat("&nbsp;", $value['__level'] * 4 );
1186							break;
1187					}
1188
1189					// print the attributes requested
1190					if ( ! is_null($attributes) )
1191					{
1192						$att = reset($attributes);
1193						while($att){
1194							if ( is_numeric($value[$att]) )
1195							{
1196								$convert .= $value[$att];
1197							}
1198							else
1199							{
1200								$convert .= '"'.$value[$att].'"';
1201							}
1202							$att = next($attributes);
1203							if ($att)
1204							{
1205								$convert .= ($type == 'csv' ? "," : " ");
1206							}
1207						}
1208					}
1209
1210					// postfix based on requested type
1211					switch ($type)
1212					{
1213						case 'tab';
1214							$convert .= "\n";
1215							break;
1216						case 'csv';
1217							$convert .= "\n";
1218							break;
1219						case 'html';
1220							$convert .= "<br />";
1221							break;
1222					}
1223				}
1224				return $convert;
1225			}
1226			else
1227			{
1228				return $result;
1229			}
1230		}
1231
1232		return FALSE;
1233	}
1234
1235	// -----------------------------------------------------------------
1236	// internal methods
1237	// -----------------------------------------------------------------
1238
1239	/**
1240	 * makes room for a new node (or nodes) by shifting the left and right
1241	 * id's of nodes with larger values than our object by $delta
1242	 *
1243	 * note that $delta can also be negative!
1244	 *
1245	 * @param	object	the DataMapper object
1246	 * @param	integer	left value of the start node
1247	 * @param	integer	number of positions to shift
1248	 * @return	object	the updated DataMapper object
1249	 * @access	private
1250	 */
1251	private function _shiftRLValues($object, $first, $delta)
1252	{
1253		// we need a valid object
1254		if ( $object->exists() )
1255		{
1256			// if we have multiple roots
1257			if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
1258			{
1259				// select the correct one
1260				$object->where($this->_rootfield, $this->_rootindex);
1261			}
1262
1263			// set the delta
1264			$delta = $delta >= 0 ? (' + '.$delta) : (' - '.(abs($delta)));
1265
1266			// select the range
1267			$object->where($this->_leftindex.' >=', $first);
1268			$object->update(array($this->_leftindex => $this->_leftindex.$delta), FALSE);
1269
1270			// if we have multiple roots
1271			if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
1272			{
1273				// select the correct one
1274				$object->where($this->_rootfield, $this->_rootindex);
1275			}
1276
1277			// select the range
1278			$object->where($this->_rightindex.' >=', $first);
1279
1280			$object->update(array($this->_rightindex => $this->_rightindex.$delta), FALSE);
1281		}
1282
1283		// return the object
1284		return $object;
1285	}
1286
1287	// -----------------------------------------------------------------
1288
1289	/**
1290	 * shifts a range of nodes up or down the left and right index by $delta
1291	 *
1292	 * note that $delta can also be negative!
1293	 *
1294	 * @param	object	the DataMapper object
1295	 * @param	integer	left value of the start node
1296	 * @param	integer	right value of the end node
1297	 * @param	integer	number of positions to shift
1298	 * @return	object	the updated DataMapper object
1299	 * @access	private
1300	 */
1301	private function _shiftRLRange($object, $first, $last, $delta)
1302	{
1303		// we need a valid object
1304		if ( $object->exists() )
1305		{
1306			// if we have multiple roots
1307			if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) )
1308			{
1309				// select the correct one
1310				$object->where($this->_rootfield, $this->_rootindex);
1311			}
1312
1313			// select the range
1314			$object->where($this->_leftindex.' >=', $first);
1315			$object->where($this->_rightindex.' <=', $last);
1316
1317			// set the delta
1318			$delta = $delta >= 0 ? (' + '.$delta) : (' - '.(abs($delta)));
1319
1320			$object->update(array($this->_leftindex => $this->_leftindex.$delta, $this->_rightindex => $this->_rightindex.$delta), FALSE);
1321		}
1322
1323		// return the object
1324		return $object;
1325	}
1326
1327	// -----------------------------------------------------------------
1328
1329	/**
1330	 * inserts a new record into the tree
1331	 *
1332	 * @param	object	the DataMapper object
1333	 * @return	object	the updated DataMapper object
1334	 * @access	private
1335	 */
1336	private function _insertNew($object)
1337	{
1338		// for now, just save the object
1339		$object->save();
1340
1341		// return the object
1342		return $object;
1343	}
1344
1345	// -----------------------------------------------------------------
1346
1347	/**
1348	 * move a section of the tree to another location within the tree
1349	 *
1350	 * @param	object	the DataMapper object we're going to move
1351	 * @param	integer	the destination node's left id value
1352	 * @return	object	the updated DataMapper object
1353	 * @access	private
1354	 */
1355	private function _moveSubtree($object, $node, $destination_id)
1356	{
1357		// if we have multiple roots
1358		if ( in_array($this->_rootfield, $object->fields) )
1359		{
1360			// make sure both nodes are part of the same tree
1361			if ( $object->{$this->_rootfield} != $node->{$this->_rootfield} )
1362			{
1363				return FALSE;
1364			}
1365		}
1366
1367		// determine the size of the tree to move
1368		$treesize = $object->{$this->_rightindex} - $object->{$this->_leftindex} + 1;
1369
1370		// get the objects left- and right pointers
1371		$left_id = $object->{$this->_leftindex};
1372		$right_id = $object->{$this->_rightindex};
1373
1374		// shift to make some space
1375		$this->_shiftRLValues($node, $destination_id, $treesize);
1376
1377		// correct pointers if there were shifted to
1378		if ($object->{$this->_leftindex} >= $destination_id)
1379		{
1380			$left_id += $treesize;
1381			$right_id += $treesize;
1382		}
1383
1384		// enough room now, start the move
1385		$this->_shiftRLRange($node, $left_id, $right_id, $destination_id - $left_id);
1386
1387		// and correct index values after the source
1388		$this->_shiftRLValues($object, $right_id + 1, -$treesize);
1389
1390		// return the object
1391		return $object->get_by_id($object->id);
1392	}
1393
1394}
1395
1396/* End of file nestedsets.php */
1397/* Location: ./application/datamapper/nestedsets.php */