[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Joomla! Content Management System 5 * 6 * @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org> 7 * @license GNU General Public License version 2 or later; see LICENSE.txt 8 */ 9 10 namespace Joomla\CMS\Table; 11 12 use Joomla\CMS\Event\AbstractEvent; 13 use Joomla\Event\Dispatcher; 14 use Joomla\Event\Event; 15 use Joomla\Utilities\ArrayHelper; 16 17 // phpcs:disable PSR1.Files.SideEffects 18 \defined('JPATH_PLATFORM') or die; 19 // phpcs:enable PSR1.Files.SideEffects 20 21 /** 22 * Table class supporting modified pre-order tree traversal behavior. 23 * 24 * @since 1.7.0 25 */ 26 class Nested extends Table 27 { 28 /** 29 * Object property holding the primary key of the parent node. Provides adjacency list data for nodes. 30 * 31 * @var integer 32 * @since 1.7.0 33 */ 34 public $parent_id; 35 36 /** 37 * Object property holding the depth level of the node in the tree. 38 * 39 * @var integer 40 * @since 1.7.0 41 */ 42 public $level; 43 44 /** 45 * Object property holding the left value of the node for managing its placement in the nested sets tree. 46 * 47 * @var integer 48 * @since 1.7.0 49 */ 50 public $lft; 51 52 /** 53 * Object property holding the right value of the node for managing its placement in the nested sets tree. 54 * 55 * @var integer 56 * @since 1.7.0 57 */ 58 public $rgt; 59 60 /** 61 * Object property holding the alias of this node used to construct the full text path, forward-slash delimited. 62 * 63 * @var string 64 * @since 1.7.0 65 */ 66 public $alias; 67 68 /** 69 * Object property to hold the location type to use when storing the row. 70 * 71 * @var string 72 * @since 1.7.0 73 * @see Nested::$_validLocations 74 */ 75 protected $_location; 76 77 /** 78 * Object property to hold the primary key of the location reference node to use when storing the row. 79 * 80 * A combination of location type and reference node describes where to store the current node in the tree. 81 * 82 * @var integer 83 * @since 1.7.0 84 */ 85 protected $_location_id; 86 87 /** 88 * An array to cache values in recursive processes. 89 * 90 * @var array 91 * @since 1.7.0 92 */ 93 protected $_cache = array(); 94 95 /** 96 * Debug level 97 * 98 * @var integer 99 * @since 1.7.0 100 */ 101 protected $_debug = 0; 102 103 /** 104 * Cache for the root ID 105 * 106 * @var integer 107 * @since 3.3 108 */ 109 protected static $root_id = 0; 110 111 /** 112 * Array declaring the valid location values for moving a node 113 * 114 * @var array 115 * @since 3.7.0 116 */ 117 private $_validLocations = array('before', 'after', 'first-child', 'last-child'); 118 119 /** 120 * Sets the debug level on or off 121 * 122 * @param integer $level 0 = off, 1 = on 123 * 124 * @return void 125 * 126 * @since 1.7.0 127 */ 128 public function debug($level) 129 { 130 $this->_debug = (int) $level; 131 } 132 133 /** 134 * Method to get an array of nodes from a given node to its root. 135 * 136 * @param integer $pk Primary key of the node for which to get the path. 137 * @param boolean $diagnostic Only select diagnostic data for the nested sets. 138 * 139 * @return mixed An array of node objects including the start node. 140 * 141 * @since 1.7.0 142 * @throws \RuntimeException on database error 143 */ 144 public function getPath($pk = null, $diagnostic = false) 145 { 146 $k = $this->_tbl_key; 147 $pk = (\is_null($pk)) ? $this->$k : $pk; 148 149 // Get the path from the node to the root. 150 $select = ($diagnostic) ? 'p.' . $k . ', p.parent_id, p.level, p.lft, p.rgt' : 'p.*'; 151 $query = $this->_db->getQuery(true) 152 ->select($select) 153 ->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p') 154 ->where('n.lft BETWEEN p.lft AND p.rgt') 155 ->where('n.' . $k . ' = ' . (int) $pk) 156 ->order('p.lft'); 157 158 $this->_db->setQuery($query); 159 160 return $this->_db->loadObjectList(); 161 } 162 163 /** 164 * Method to get a node and all its child nodes. 165 * 166 * @param integer $pk Primary key of the node for which to get the tree. 167 * @param boolean $diagnostic Only select diagnostic data for the nested sets. 168 * 169 * @return mixed Boolean false on failure or array of node objects on success. 170 * 171 * @since 1.7.0 172 * @throws \RuntimeException on database error. 173 */ 174 public function getTree($pk = null, $diagnostic = false) 175 { 176 $k = $this->_tbl_key; 177 $pk = (\is_null($pk)) ? $this->$k : $pk; 178 179 // Get the node and children as a tree. 180 $select = ($diagnostic) ? 'n.' . $k . ', n.parent_id, n.level, n.lft, n.rgt' : 'n.*'; 181 $query = $this->_db->getQuery(true) 182 ->select($select) 183 ->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p') 184 ->where('n.lft BETWEEN p.lft AND p.rgt') 185 ->where('p.' . $k . ' = ' . (int) $pk) 186 ->order('n.lft'); 187 188 return $this->_db->setQuery($query)->loadObjectList(); 189 } 190 191 /** 192 * Method to determine if a node is a leaf node in the tree (has no children). 193 * 194 * @param integer $pk Primary key of the node to check. 195 * 196 * @return boolean True if a leaf node, false if not or null if the node does not exist. 197 * 198 * @note Since 3.0.0 this method returns null if the node does not exist. 199 * @since 1.7.0 200 * @throws \RuntimeException on database error. 201 */ 202 public function isLeaf($pk = null) 203 { 204 $k = $this->_tbl_key; 205 $pk = (\is_null($pk)) ? $this->$k : $pk; 206 $node = $this->_getNode($pk); 207 208 // Get the node by primary key. 209 if (empty($node)) { 210 // Error message set in getNode method. 211 return; 212 } 213 214 // The node is a leaf node. 215 return ($node->rgt - $node->lft) == 1; 216 } 217 218 /** 219 * Method to set the location of a node in the tree object. This method does not 220 * save the new location to the database, but will set it in the object so 221 * that when the node is stored it will be stored in the new location. 222 * 223 * @param integer $referenceId The primary key of the node to reference new location by. 224 * @param string $position Location type string. 225 * 226 * @return void 227 * 228 * @note Since 3.0.0 this method returns void and throws an \InvalidArgumentException when an invalid position is passed. 229 * @see Nested::$_validLocations 230 * @since 1.7.0 231 * @throws \InvalidArgumentException 232 */ 233 public function setLocation($referenceId, $position = 'after') 234 { 235 // Make sure the location is valid. 236 if (!\in_array($position, $this->_validLocations)) { 237 throw new \InvalidArgumentException( 238 sprintf('Invalid location "%1$s" given, valid values are %2$s', $position, implode(', ', $this->_validLocations)) 239 ); 240 } 241 242 // Set the location properties. 243 $this->_location = $position; 244 $this->_location_id = $referenceId; 245 } 246 247 /** 248 * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause. 249 * Negative numbers move the row up in the sequence and positive numbers move it down. 250 * 251 * @param integer $delta The direction and magnitude to move the row in the ordering sequence. 252 * @param string $where WHERE clause to use for limiting the selection of rows to compact the 253 * ordering values. 254 * 255 * @return mixed Boolean true on success. 256 * 257 * @since 1.7.0 258 */ 259 public function move($delta, $where = '') 260 { 261 $k = $this->_tbl_key; 262 $pk = $this->$k; 263 264 $query = $this->_db->getQuery(true) 265 ->select($k) 266 ->from($this->_tbl) 267 ->where('parent_id = ' . $this->parent_id); 268 269 if ($where) { 270 $query->where($where); 271 } 272 273 if ($delta > 0) { 274 $query->where('rgt > ' . $this->rgt) 275 ->order('rgt ASC'); 276 $position = 'after'; 277 } else { 278 $query->where('lft < ' . $this->lft) 279 ->order('lft DESC'); 280 $position = 'before'; 281 } 282 283 $this->_db->setQuery($query); 284 $referenceId = $this->_db->loadResult(); 285 286 if ($referenceId) { 287 return $this->moveByReference($referenceId, $position, $pk); 288 } else { 289 return false; 290 } 291 } 292 293 /** 294 * Method to move a node and its children to a new location in the tree. 295 * 296 * @param integer $referenceId The primary key of the node to reference new location by. 297 * @param string $position Location type string. ['before', 'after', 'first-child', 'last-child'] 298 * @param integer $pk The primary key of the node to move. 299 * @param boolean $recursiveUpdate Flag indicate that method recursiveUpdatePublishedColumn should be call. 300 * 301 * @return boolean True on success. 302 * 303 * @since 1.7.0 304 * @throws \RuntimeException on database error. 305 */ 306 public function moveByReference($referenceId, $position = 'after', $pk = null, $recursiveUpdate = true) 307 { 308 if ($this->_debug) { 309 echo "\nMoving ReferenceId:$referenceId, Position:$position, PK:$pk"; 310 } 311 312 $k = $this->_tbl_key; 313 $pk = (\is_null($pk)) ? $this->$k : $pk; 314 315 // Get the node by id. 316 if (!$node = $this->_getNode($pk)) { 317 // Error message set in getNode method. 318 return false; 319 } 320 321 // Get the ids of child nodes. 322 $query = $this->_db->getQuery(true) 323 ->select($k) 324 ->from($this->_tbl) 325 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 326 327 $children = $this->_db->setQuery($query)->loadColumn(); 328 329 if ($this->_debug) { 330 $this->_logtable(false); 331 } 332 333 // Cannot move the node to be a child of itself. 334 if (\in_array($referenceId, $children)) { 335 $this->setError( 336 new \UnexpectedValueException( 337 sprintf('%1$s::moveByReference() is trying to make record ID %2$d a child of itself.', \get_class($this), $pk) 338 ) 339 ); 340 341 return false; 342 } 343 344 // Lock the table for writing. 345 if (!$this->_lock()) { 346 return false; 347 } 348 349 /* 350 * Move the sub-tree out of the nested sets by negating its left and right values. 351 */ 352 $query->clear() 353 ->update($this->_tbl) 354 ->set('lft = lft * (-1), rgt = rgt * (-1)') 355 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 356 $this->_db->setQuery($query); 357 358 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 359 360 /* 361 * Close the hole in the tree that was opened by removing the sub-tree from the nested sets. 362 */ 363 364 // Compress the left values. 365 $query->clear() 366 ->update($this->_tbl) 367 ->set('lft = lft - ' . (int) $node->width) 368 ->where('lft > ' . (int) $node->rgt); 369 $this->_db->setQuery($query); 370 371 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 372 373 // Compress the right values. 374 $query->clear() 375 ->update($this->_tbl) 376 ->set('rgt = rgt - ' . (int) $node->width) 377 ->where('rgt > ' . (int) $node->rgt); 378 $this->_db->setQuery($query); 379 380 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 381 382 // We are moving the tree relative to a reference node. 383 if ($referenceId) { 384 // Get the reference node by primary key. 385 if (!$reference = $this->_getNode($referenceId)) { 386 // Error message set in getNode method. 387 $this->_unlock(); 388 389 return false; 390 } 391 392 // Get the reposition data for shifting the tree and re-inserting the node. 393 if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, $position)) { 394 // Error message set in getNode method. 395 $this->_unlock(); 396 397 return false; 398 } 399 } else { 400 // We are moving the tree to be the last child of the root node 401 // Get the last root node as the reference node. 402 $query->clear() 403 ->select($this->_tbl_key . ', parent_id, level, lft, rgt') 404 ->from($this->_tbl) 405 ->where('parent_id = 0') 406 ->order('lft DESC'); 407 408 $query->setLimit(1); 409 $this->_db->setQuery($query); 410 $reference = $this->_db->loadObject(); 411 412 if ($this->_debug) { 413 $this->_logtable(false); 414 } 415 416 // Get the reposition data for re-inserting the node after the found root. 417 if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, 'last-child')) { 418 // Error message set in getNode method. 419 $this->_unlock(); 420 421 return false; 422 } 423 } 424 425 /* 426 * Create space in the nested sets at the new location for the moved sub-tree. 427 */ 428 429 // Shift left values. 430 $query->clear() 431 ->update($this->_tbl) 432 ->set('lft = lft + ' . (int) $node->width) 433 ->where($repositionData->left_where); 434 $this->_db->setQuery($query); 435 436 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 437 438 // Shift right values. 439 $query->clear() 440 ->update($this->_tbl) 441 ->set('rgt = rgt + ' . (int) $node->width) 442 ->where($repositionData->right_where); 443 $this->_db->setQuery($query); 444 445 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 446 447 /* 448 * Calculate the offset between where the node used to be in the tree and 449 * where it needs to be in the tree for left ids (also works for right ids). 450 */ 451 $offset = $repositionData->new_lft - $node->lft; 452 $levelOffset = $repositionData->new_level - $node->level; 453 454 // Move the nodes back into position in the tree using the calculated offsets. 455 $query->clear() 456 ->update($this->_tbl) 457 ->set('rgt = ' . (int) $offset . ' - rgt') 458 ->set('lft = ' . (int) $offset . ' - lft') 459 ->set('level = level + ' . (int) $levelOffset) 460 ->where('lft < 0'); 461 $this->_db->setQuery($query); 462 463 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 464 465 // Set the correct parent id for the moved node if required. 466 if ($node->parent_id != $repositionData->new_parent_id) { 467 $query = $this->_db->getQuery(true) 468 ->update($this->_tbl); 469 470 // Update the title and alias fields if they exist for the table. 471 $fields = $this->getFields(); 472 473 if ($this->hasField('title') && $this->title !== null) { 474 $query->set('title = ' . $this->_db->quote($this->title)); 475 } 476 477 if (\array_key_exists('alias', $fields) && $this->alias !== null) { 478 $query->set('alias = ' . $this->_db->quote($this->alias)); 479 } 480 481 $query->set('parent_id = ' . (int) $repositionData->new_parent_id) 482 ->where($this->_tbl_key . ' = ' . (int) $node->$k); 483 $this->_db->setQuery($query); 484 485 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED'); 486 } 487 488 // Unlock the table for writing. 489 $this->_unlock(); 490 491 if ($this->hasField('published') && $recursiveUpdate) { 492 $this->recursiveUpdatePublishedColumn($node->$k); 493 } 494 495 // Set the object values. 496 $this->parent_id = $repositionData->new_parent_id; 497 $this->level = $repositionData->new_level; 498 $this->lft = $repositionData->new_lft; 499 $this->rgt = $repositionData->new_rgt; 500 501 return true; 502 } 503 504 /** 505 * Method to delete a node and, optionally, its child nodes from the table. 506 * 507 * @param integer $pk The primary key of the node to delete. 508 * @param boolean $children True to delete child nodes, false to move them up a level. 509 * 510 * @return boolean True on success. 511 * 512 * @since 1.7.0 513 */ 514 public function delete($pk = null, $children = true) 515 { 516 $k = $this->_tbl_key; 517 $pk = (\is_null($pk)) ? $this->$k : $pk; 518 519 // Pre-processing by observers 520 $event = new Event( 521 'onBeforeDelete', 522 [ 523 'pk' => $pk, 524 ] 525 ); 526 $this->getDispatcher()->dispatch('onBeforeDelete', $event); 527 528 // If tracking assets, remove the asset first. 529 if ($this->_trackAssets) { 530 $name = $this->_getAssetName(); 531 532 /** @var Asset $asset */ 533 $asset = Table::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); 534 535 if ($asset->loadByName($name)) { 536 // Delete the node in assets table. 537 if (!$asset->delete(null, $children)) { 538 $this->setError($asset->getError()); 539 540 return false; 541 } 542 } else { 543 $this->setError($asset->getError()); 544 545 return false; 546 } 547 } 548 549 // Lock the table for writing. 550 if (!$this->_lock()) { 551 // Error message set in lock method. 552 return false; 553 } 554 555 // Get the node by id. 556 $node = $this->_getNode($pk); 557 558 if (empty($node)) { 559 // Error message set in getNode method. 560 $this->_unlock(); 561 562 return false; 563 } 564 565 $query = $this->_db->getQuery(true); 566 567 // Should we delete all children along with the node? 568 if ($children) { 569 // Delete the node and all of its children. 570 $query->clear() 571 ->delete($this->_tbl) 572 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 573 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 574 575 // Compress the left values. 576 $query->clear() 577 ->update($this->_tbl) 578 ->set('lft = lft - ' . (int) $node->width) 579 ->where('lft > ' . (int) $node->rgt); 580 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 581 582 // Compress the right values. 583 $query->clear() 584 ->update($this->_tbl) 585 ->set('rgt = rgt - ' . (int) $node->width) 586 ->where('rgt > ' . (int) $node->rgt); 587 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 588 } else { 589 // Leave the children and move them up a level. 590 // Delete the node. 591 $query->clear() 592 ->delete($this->_tbl) 593 ->where('lft = ' . (int) $node->lft); 594 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 595 596 // Shift all node's children up a level. 597 $query->clear() 598 ->update($this->_tbl) 599 ->set('lft = lft - 1') 600 ->set('rgt = rgt - 1') 601 ->set('level = level - 1') 602 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 603 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 604 605 // Adjust all the parent values for direct children of the deleted node. 606 $query->clear() 607 ->update($this->_tbl) 608 ->set('parent_id = ' . (int) $node->parent_id) 609 ->where('parent_id = ' . (int) $node->$k); 610 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 611 612 // Shift all of the left values that are right of the node. 613 $query->clear() 614 ->update($this->_tbl) 615 ->set('lft = lft - 2') 616 ->where('lft > ' . (int) $node->rgt); 617 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 618 619 // Shift all of the right values that are right of the node. 620 $query->clear() 621 ->update($this->_tbl) 622 ->set('rgt = rgt - 2') 623 ->where('rgt > ' . (int) $node->rgt); 624 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED'); 625 } 626 627 // Unlock the table for writing. 628 $this->_unlock(); 629 630 // Post-processing by observers 631 $event = new Event( 632 'onAfterDelete', 633 [ 634 'pk' => $pk, 635 ] 636 ); 637 $this->getDispatcher()->dispatch('onAfterDelete', $event); 638 639 return true; 640 } 641 642 /** 643 * Checks that the object is valid and able to be stored. 644 * 645 * This method checks that the parent_id is non-zero and exists in the database. 646 * Note that the root node (parent_id = 0) cannot be manipulated with this class. 647 * 648 * @return boolean True if all checks pass. 649 * 650 * @since 1.7.0 651 */ 652 public function check() 653 { 654 try { 655 parent::check(); 656 } catch (\Exception $e) { 657 $this->setError($e->getMessage()); 658 659 return false; 660 } 661 662 $this->parent_id = (int) $this->parent_id; 663 664 // Set up a mini exception handler. 665 try { 666 // Check that the parent_id field is valid. 667 if ($this->parent_id == 0) { 668 throw new \UnexpectedValueException(sprintf('Invalid `parent_id` [%1$d] in %2$s::check()', $this->parent_id, \get_class($this))); 669 } 670 671 $query = $this->_db->getQuery(true) 672 ->select('1') 673 ->from($this->_tbl) 674 ->where($this->_tbl_key . ' = ' . $this->parent_id); 675 676 if (!$this->_db->setQuery($query)->loadResult()) { 677 throw new \UnexpectedValueException(sprintf('Invalid `parent_id` [%1$d] in %2$s::check()', $this->parent_id, \get_class($this))); 678 } 679 } catch (\UnexpectedValueException $e) { 680 // Validation error - record it and return false. 681 $this->setError($e); 682 683 return false; 684 } 685 686 return true; 687 } 688 689 /** 690 * Method to store a node in the database table. 691 * 692 * @param boolean $updateNulls True to update null values as well. 693 * 694 * @return boolean True on success. 695 * 696 * @since 1.7.0 697 */ 698 public function store($updateNulls = false) 699 { 700 $k = $this->_tbl_key; 701 702 // Pre-processing by observers 703 $event = AbstractEvent::create( 704 'onTableBeforeStore', 705 [ 706 'subject' => $this, 707 'updateNulls' => $updateNulls, 708 'k' => $k, 709 ] 710 ); 711 $this->getDispatcher()->dispatch('onTableBeforeStore', $event); 712 713 if ($this->_debug) { 714 echo "\n" . \get_class($this) . "::store\n"; 715 $this->_logtable(true, false); 716 } 717 718 /* 719 * If the primary key is empty, then we assume we are inserting a new node into the 720 * tree. From this point we would need to determine where in the tree to insert it. 721 */ 722 if (empty($this->$k)) { 723 /* 724 * We are inserting a node somewhere in the tree with a known reference 725 * node. We have to make room for the new node and set the left and right 726 * values before we insert the row. 727 */ 728 if ($this->_location_id >= 0) { 729 // Lock the table for writing. 730 if (!$this->_lock()) { 731 // Error message set in lock method. 732 return false; 733 } 734 735 // We are inserting a node relative to the last root node. 736 if ($this->_location_id == 0) { 737 // Get the last root node as the reference node. 738 $query = $this->_db->getQuery(true) 739 ->select($this->_tbl_key . ', parent_id, level, lft, rgt') 740 ->from($this->_tbl) 741 ->where('parent_id = 0') 742 ->order('lft DESC'); 743 744 $query->setLimit(1); 745 $this->_db->setQuery($query); 746 $reference = $this->_db->loadObject(); 747 748 if ($this->_debug) { 749 $this->_logtable(false); 750 } 751 } else { 752 // We have a real node set as a location reference. 753 // Get the reference node by primary key. 754 if (!$reference = $this->_getNode($this->_location_id)) { 755 // Error message set in getNode method. 756 $this->_unlock(); 757 758 return false; 759 } 760 } 761 762 // Get the reposition data for shifting the tree and re-inserting the node. 763 if (!($repositionData = $this->_getTreeRepositionData($reference, 2, $this->_location))) { 764 // Error message set in getNode method. 765 $this->_unlock(); 766 767 return false; 768 } 769 770 // Create space in the tree at the new location for the new node in left ids. 771 $query = $this->_db->getQuery(true) 772 ->update($this->_tbl) 773 ->set('lft = lft + 2') 774 ->where($repositionData->left_where); 775 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED'); 776 777 // Create space in the tree at the new location for the new node in right ids. 778 $query->clear() 779 ->update($this->_tbl) 780 ->set('rgt = rgt + 2') 781 ->where($repositionData->right_where); 782 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED'); 783 784 // Set the object values. 785 $this->parent_id = $repositionData->new_parent_id; 786 $this->level = $repositionData->new_level; 787 $this->lft = $repositionData->new_lft; 788 $this->rgt = $repositionData->new_rgt; 789 } else { 790 // Negative parent ids are invalid 791 $e = new \UnexpectedValueException(sprintf('%s::store() used a negative _location_id', \get_class($this))); 792 $this->setError($e); 793 794 return false; 795 } 796 } else { 797 /** 798 * If we have a given primary key then we assume we are simply updating this 799 * node in the tree. We should assess whether or not we are moving the node 800 * or just updating its data fields. 801 */ 802 // If the location has been set, move the node to its new location. 803 if ($this->_location_id > 0) { 804 // Skip recursiveUpdatePublishedColumn method, it will be called later. 805 if (!$this->moveByReference($this->_location_id, $this->_location, $this->$k, false)) { 806 // Error message set in move method. 807 return false; 808 } 809 } 810 811 // Lock the table for writing. 812 if (!$this->_lock()) { 813 // Error message set in lock method. 814 return false; 815 } 816 } 817 818 // We do not want parent::store to update observers since tables are locked and we are updating it from this 819 // level of store(): 820 821 $oldDispatcher = clone $this->getDispatcher(); 822 $blankDispatcher = new Dispatcher(); 823 $this->setDispatcher($blankDispatcher); 824 825 $result = parent::store($updateNulls); 826 827 // Restore previous callable dispatcher state: 828 $this->setDispatcher($oldDispatcher); 829 830 if ($result) { 831 if ($this->_debug) { 832 $this->_logtable(); 833 } 834 } 835 836 // Unlock the table for writing. 837 $this->_unlock(); 838 839 if ($result && $this->hasField('published')) { 840 $this->recursiveUpdatePublishedColumn($this->$k); 841 } 842 843 // Post-processing by observers 844 $event = AbstractEvent::create( 845 'onTableAfterStore', 846 [ 847 'subject' => $this, 848 'result' => &$result, 849 ] 850 ); 851 $this->getDispatcher()->dispatch('onTableAfterStore', $event); 852 853 return $result; 854 } 855 856 /** 857 * Method to set the publishing state for a node or list of nodes in the database 858 * table. The method respects rows checked out by other users and will attempt 859 * to checkin rows that it can after adjustments are made. The method will not 860 * allow you to set a publishing state higher than any ancestor node and will 861 * not allow you to set a publishing state on a node with a checked out child. 862 * 863 * @param mixed $pks An optional array of primary key values to update. If not 864 * set the instance property value is used. 865 * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published] 866 * @param integer $userId The user id of the user performing the operation. 867 * 868 * @return boolean True on success. 869 * 870 * @since 1.7.0 871 * @throws \UnexpectedValueException 872 */ 873 public function publish($pks = null, $state = 1, $userId = 0) 874 { 875 $k = $this->_tbl_key; 876 877 $query = $this->_db->getQuery(true); 878 $table = $this->_db->quoteName($this->_tbl); 879 $published = $this->_db->quoteName($this->getColumnAlias('published')); 880 $key = $this->_db->quoteName($k); 881 882 // Sanitize input. 883 $pks = ArrayHelper::toInteger($pks); 884 $userId = (int) $userId; 885 $state = (int) $state; 886 887 // If $state > 1, then we allow state changes even if an ancestor has lower state 888 // (for example, can change a child state to Archived (2) if an ancestor is Published (1) 889 $compareState = ($state > 1) ? 1 : $state; 890 891 // If there are no primary keys set check to see if the instance key is set. 892 if (empty($pks)) { 893 if ($this->$k) { 894 $pks = explode(',', $this->$k); 895 } else { 896 // Nothing to set publishing state on, return false. 897 $e = new \UnexpectedValueException(sprintf('%s::publish(%s, %d, %d) empty.', \get_class($this), implode(',', $pks), $state, $userId)); 898 $this->setError($e); 899 900 return false; 901 } 902 } 903 904 // Determine if there is checkout support for the table. 905 $checkoutSupport = ($this->hasField('checked_out') || $this->hasField('checked_out_time')); 906 907 // Iterate over the primary keys to execute the publish action if possible. 908 foreach ($pks as $pk) { 909 // Get the node by primary key. 910 if (!$node = $this->_getNode($pk)) { 911 // Error message set in getNode method. 912 return false; 913 } 914 915 // If the table has checkout support, verify no children are checked out. 916 if ($checkoutSupport) { 917 // Ensure that children are not checked out. 918 $query->clear() 919 ->select('COUNT(' . $k . ')') 920 ->from($this->_tbl) 921 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt) 922 ->where('(checked_out <> 0 AND checked_out <> ' . (int) $userId . ')'); 923 $this->_db->setQuery($query); 924 925 // Check for checked out children. 926 if ($this->_db->loadResult()) { 927 // @todo Convert to a conflict exception when available. 928 $e = new \RuntimeException(sprintf('%s::publish(%s, %d, %d) checked-out conflict.', \get_class($this), $pks[0], $state, $userId)); 929 930 $this->setError($e); 931 932 return false; 933 } 934 } 935 936 // If any parent nodes have lower published state values, we cannot continue. 937 if ($node->parent_id) { 938 // Get any ancestor nodes that have a lower publishing state. 939 $query->clear() 940 ->select('1') 941 ->from($table) 942 ->where('lft < ' . (int) $node->lft) 943 ->where('rgt > ' . (int) $node->rgt) 944 ->where('parent_id > 0') 945 ->where($published . ' < ' . (int) $compareState); 946 947 // Just fetch one row (one is one too many). 948 $query->setLimit(1); 949 $this->_db->setQuery($query); 950 951 if ($this->_db->loadResult()) { 952 $e = new \UnexpectedValueException( 953 sprintf('%s::publish(%s, %d, %d) ancestors have lower state.', \get_class($this), $pks[0], $state, $userId) 954 ); 955 $this->setError($e); 956 957 return false; 958 } 959 } 960 961 $this->recursiveUpdatePublishedColumn($pk, $state); 962 963 // If checkout support exists for the object, check the row in. 964 if ($checkoutSupport) { 965 $this->checkIn($pk); 966 } 967 } 968 969 // If the Table instance value is in the list of primary keys that were set, set the instance. 970 if (\in_array($this->$k, $pks)) { 971 $this->published = $state; 972 } 973 974 $this->setError(''); 975 976 return true; 977 } 978 979 /** 980 * Method to move a node one position to the left in the same level. 981 * 982 * @param integer $pk Primary key of the node to move. 983 * 984 * @return boolean True on success. 985 * 986 * @since 1.7.0 987 * @throws \RuntimeException on database error. 988 */ 989 public function orderUp($pk) 990 { 991 $k = $this->_tbl_key; 992 $pk = (\is_null($pk)) ? $this->$k : $pk; 993 994 // Lock the table for writing. 995 if (!$this->_lock()) { 996 // Error message set in lock method. 997 return false; 998 } 999 1000 // Get the node by primary key. 1001 $node = $this->_getNode($pk); 1002 1003 if (empty($node)) { 1004 // Error message set in getNode method. 1005 $this->_unlock(); 1006 1007 return false; 1008 } 1009 1010 // Get the left sibling node. 1011 $sibling = $this->_getNode($node->lft - 1, 'right'); 1012 1013 if (empty($sibling)) { 1014 // Error message set in getNode method. 1015 $this->_unlock(); 1016 1017 return false; 1018 } 1019 1020 try { 1021 // Get the primary keys of child nodes. 1022 $query = $this->_db->getQuery(true) 1023 ->select($this->_tbl_key) 1024 ->from($this->_tbl) 1025 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 1026 1027 $children = $this->_db->setQuery($query)->loadColumn(); 1028 1029 // Shift left and right values for the node and its children. 1030 $query->clear() 1031 ->update($this->_tbl) 1032 ->set('lft = lft - ' . (int) $sibling->width) 1033 ->set('rgt = rgt - ' . (int) $sibling->width) 1034 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 1035 $this->_db->setQuery($query)->execute(); 1036 1037 // Shift left and right values for the sibling and its children. 1038 $query->clear() 1039 ->update($this->_tbl) 1040 ->set('lft = lft + ' . (int) $node->width) 1041 ->set('rgt = rgt + ' . (int) $node->width) 1042 ->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt) 1043 ->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')'); 1044 $this->_db->setQuery($query)->execute(); 1045 } catch (\RuntimeException $e) { 1046 $this->_unlock(); 1047 throw $e; 1048 } 1049 1050 // Unlock the table for writing. 1051 $this->_unlock(); 1052 1053 return true; 1054 } 1055 1056 /** 1057 * Method to move a node one position to the right in the same level. 1058 * 1059 * @param integer $pk Primary key of the node to move. 1060 * 1061 * @return boolean True on success. 1062 * 1063 * @since 1.7.0 1064 * @throws \RuntimeException on database error. 1065 */ 1066 public function orderDown($pk) 1067 { 1068 $k = $this->_tbl_key; 1069 $pk = (\is_null($pk)) ? $this->$k : $pk; 1070 1071 // Lock the table for writing. 1072 if (!$this->_lock()) { 1073 // Error message set in lock method. 1074 return false; 1075 } 1076 1077 // Get the node by primary key. 1078 $node = $this->_getNode($pk); 1079 1080 if (empty($node)) { 1081 // Error message set in getNode method. 1082 $this->_unlock(); 1083 1084 return false; 1085 } 1086 1087 $query = $this->_db->getQuery(true); 1088 1089 // Get the right sibling node. 1090 $sibling = $this->_getNode($node->rgt + 1, 'left'); 1091 1092 if (empty($sibling)) { 1093 // Error message set in getNode method. 1094 $this->_unlock(); 1095 1096 return false; 1097 } 1098 1099 try { 1100 // Get the primary keys of child nodes. 1101 $query->clear() 1102 ->select($this->_tbl_key) 1103 ->from($this->_tbl) 1104 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 1105 $this->_db->setQuery($query); 1106 $children = $this->_db->loadColumn(); 1107 1108 // Shift left and right values for the node and its children. 1109 $query->clear() 1110 ->update($this->_tbl) 1111 ->set('lft = lft + ' . (int) $sibling->width) 1112 ->set('rgt = rgt + ' . (int) $sibling->width) 1113 ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt); 1114 $this->_db->setQuery($query)->execute(); 1115 1116 // Shift left and right values for the sibling and its children. 1117 $query->clear() 1118 ->update($this->_tbl) 1119 ->set('lft = lft - ' . (int) $node->width) 1120 ->set('rgt = rgt - ' . (int) $node->width) 1121 ->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt) 1122 ->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')'); 1123 $this->_db->setQuery($query)->execute(); 1124 } catch (\RuntimeException $e) { 1125 $this->_unlock(); 1126 throw $e; 1127 } 1128 1129 // Unlock the table for writing. 1130 $this->_unlock(); 1131 1132 return true; 1133 } 1134 1135 /** 1136 * Gets the ID of the root item in the tree 1137 * 1138 * @return mixed The primary id of the root row, or false if not found and the internal error is set. 1139 * 1140 * @since 1.7.0 1141 */ 1142 public function getRootId() 1143 { 1144 if ((int) self::$root_id > 0) { 1145 return self::$root_id; 1146 } 1147 1148 // Get the root item. 1149 $k = $this->_tbl_key; 1150 1151 // Test for a unique record with parent_id = 0 1152 $query = $this->_db->getQuery(true) 1153 ->select($k) 1154 ->from($this->_tbl) 1155 ->where('parent_id = 0'); 1156 1157 $result = $this->_db->setQuery($query)->loadColumn(); 1158 1159 if (\count($result) == 1) { 1160 self::$root_id = $result[0]; 1161 1162 return self::$root_id; 1163 } 1164 1165 // Test for a unique record with lft = 0 1166 $query->clear() 1167 ->select($k) 1168 ->from($this->_tbl) 1169 ->where('lft = 0'); 1170 1171 $result = $this->_db->setQuery($query)->loadColumn(); 1172 1173 if (\count($result) == 1) { 1174 self::$root_id = $result[0]; 1175 1176 return self::$root_id; 1177 } 1178 1179 $fields = $this->getFields(); 1180 1181 if (\array_key_exists('alias', $fields)) { 1182 // Test for a unique record alias = root 1183 $query->clear() 1184 ->select($k) 1185 ->from($this->_tbl) 1186 ->where('alias = ' . $this->_db->quote('root')); 1187 1188 $result = $this->_db->setQuery($query)->loadColumn(); 1189 1190 if (\count($result) == 1) { 1191 self::$root_id = $result[0]; 1192 1193 return self::$root_id; 1194 } 1195 } 1196 1197 $e = new \UnexpectedValueException(sprintf('%s::getRootId', \get_class($this))); 1198 $this->setError($e); 1199 self::$root_id = false; 1200 1201 return false; 1202 } 1203 1204 /** 1205 * Method to recursively rebuild the whole nested set tree. 1206 * 1207 * @param integer $parentId The root of the tree to rebuild. 1208 * @param integer $leftId The left id to start with in building the tree. 1209 * @param integer $level The level to assign to the current nodes. 1210 * @param string $path The path to the current nodes. 1211 * 1212 * @return integer 1 + value of root rgt on success, false on failure 1213 * 1214 * @since 1.7.0 1215 * @throws \RuntimeException on database error. 1216 */ 1217 public function rebuild($parentId = null, $leftId = 0, $level = 0, $path = '') 1218 { 1219 // If no parent is provided, try to find it. 1220 if ($parentId === null) { 1221 // Get the root item. 1222 $parentId = $this->getRootId(); 1223 1224 if ($parentId === false) { 1225 return false; 1226 } 1227 } 1228 1229 $query = $this->_db->getQuery(true); 1230 1231 // Build the structure of the recursive query. 1232 if (!isset($this->_cache['rebuild.sql'])) { 1233 $query->clear() 1234 ->select($this->_tbl_key . ', alias') 1235 ->from($this->_tbl) 1236 ->where('parent_id = %d'); 1237 1238 // If the table has an ordering field, use that for ordering. 1239 if ($this->hasField('ordering')) { 1240 $query->order('parent_id, ' . $this->_db->quoteName($this->getColumnAlias('ordering')) . ', lft'); 1241 } else { 1242 $query->order('parent_id, lft'); 1243 } 1244 1245 $this->_cache['rebuild.sql'] = (string) $query; 1246 } 1247 1248 // Make a shortcut to database object. 1249 1250 // Assemble the query to find all children of this node. 1251 $this->_db->setQuery(sprintf($this->_cache['rebuild.sql'], (int) $parentId)); 1252 1253 $children = $this->_db->loadObjectList(); 1254 1255 // The right value of this node is the left value + 1 1256 $rightId = $leftId + 1; 1257 1258 // Execute this function recursively over all children 1259 foreach ($children as $node) { 1260 /* 1261 * $rightId is the current right value, which is incremented on recursion return. 1262 * Increment the level for the children. 1263 * Add this item's alias to the path (but avoid a leading /) 1264 */ 1265 $rightId = $this->rebuild($node->{$this->_tbl_key}, $rightId, $level + 1, $path . (empty($path) ? '' : '/') . $node->alias); 1266 1267 // If there is an update failure, return false to break out of the recursion. 1268 if ($rightId === false) { 1269 return false; 1270 } 1271 } 1272 1273 // We've got the left value, and now that we've processed 1274 // the children of this node we also know the right value. 1275 $query->clear() 1276 ->update($this->_tbl) 1277 ->set('lft = ' . (int) $leftId) 1278 ->set('rgt = ' . (int) $rightId) 1279 ->set('level = ' . (int) $level) 1280 ->set('path = ' . $this->_db->quote($path)) 1281 ->where($this->_tbl_key . ' = ' . (int) $parentId); 1282 $this->_db->setQuery($query)->execute(); 1283 1284 // Return the right value of this node + 1. 1285 return $rightId + 1; 1286 } 1287 1288 /** 1289 * Method to rebuild the node's path field from the alias values of the nodes from the current node to the root node of the tree. 1290 * 1291 * @param integer $pk Primary key of the node for which to get the path. 1292 * 1293 * @return boolean True on success. 1294 * 1295 * @since 1.7.0 1296 */ 1297 public function rebuildPath($pk = null) 1298 { 1299 $fields = $this->getFields(); 1300 1301 // If there is no alias or path field, just return true. 1302 if (!\array_key_exists('alias', $fields) || !\array_key_exists('path', $fields)) { 1303 return true; 1304 } 1305 1306 $k = $this->_tbl_key; 1307 $pk = (\is_null($pk)) ? $this->$k : $pk; 1308 1309 // Get the aliases for the path from the node to the root node. 1310 $query = $this->_db->getQuery(true) 1311 ->select('p.alias') 1312 ->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p') 1313 ->where('n.lft BETWEEN p.lft AND p.rgt') 1314 ->where('n.' . $this->_tbl_key . ' = ' . (int) $pk) 1315 ->order('p.lft'); 1316 $this->_db->setQuery($query); 1317 1318 $segments = $this->_db->loadColumn(); 1319 1320 // Make sure to remove the root path if it exists in the list. 1321 if ($segments[0] === 'root') { 1322 array_shift($segments); 1323 } 1324 1325 // Build the path. 1326 $path = trim(implode('/', $segments), ' /\\'); 1327 1328 // Update the path field for the node. 1329 $query->clear() 1330 ->update($this->_tbl) 1331 ->set('path = ' . $this->_db->quote($path)) 1332 ->where($this->_tbl_key . ' = ' . (int) $pk); 1333 1334 $this->_db->setQuery($query)->execute(); 1335 1336 // Update the current record's path to the new one: 1337 $this->path = $path; 1338 1339 return true; 1340 } 1341 1342 /** 1343 * Method to reset class properties to the defaults set in the class 1344 * definition. It will ignore the primary key as well as any private class 1345 * properties (except $_errors). 1346 * 1347 * @return void 1348 * 1349 * @since 3.2.1 1350 */ 1351 public function reset() 1352 { 1353 parent::reset(); 1354 1355 // Reset the location properties. 1356 $this->setLocation(0); 1357 } 1358 1359 /** 1360 * Method to update order of table rows 1361 * 1362 * @param array $idArray id numbers of rows to be reordered. 1363 * @param array $lftArray lft values of rows to be reordered. 1364 * 1365 * @return integer|boolean 1 + value of root rgt on success, false on failure. 1366 * 1367 * @since 1.7.0 1368 * @throws \Exception on database error. 1369 */ 1370 public function saveorder($idArray = null, $lftArray = null) 1371 { 1372 try { 1373 $query = $this->_db->getQuery(true); 1374 1375 // Validate arguments 1376 if (\is_array($idArray) && \is_array($lftArray) && \count($idArray) == \count($lftArray)) { 1377 for ($i = 0, $count = \count($idArray); $i < $count; $i++) { 1378 // Do an update to change the lft values in the table for each id 1379 $query->clear() 1380 ->update($this->_tbl) 1381 ->where($this->_tbl_key . ' = ' . (int) $idArray[$i]) 1382 ->set('lft = ' . (int) $lftArray[$i]); 1383 1384 $this->_db->setQuery($query)->execute(); 1385 1386 if ($this->_debug) { 1387 $this->_logtable(); 1388 } 1389 } 1390 1391 return $this->rebuild(); 1392 } else { 1393 return false; 1394 } 1395 } catch (\Exception $e) { 1396 $this->_unlock(); 1397 throw $e; 1398 } 1399 } 1400 1401 /** 1402 * Method to recursive update published column for children rows. 1403 * 1404 * @param integer $pk Id number of row which published column was changed. 1405 * @param integer $newState An optional value for published column of row identified by $pk. 1406 * 1407 * @return boolean True on success. 1408 * 1409 * @since 3.7.0 1410 * @throws \RuntimeException on database error. 1411 */ 1412 protected function recursiveUpdatePublishedColumn($pk, $newState = null) 1413 { 1414 $query = $this->_db->getQuery(true); 1415 $table = $this->_db->quoteName($this->_tbl); 1416 $key = $this->_db->quoteName($this->_tbl_key); 1417 $published = $this->_db->quoteName($this->getColumnAlias('published')); 1418 1419 if ($newState !== null) { 1420 // Use a new published state in changed row. 1421 $newState = "(CASE WHEN p2.$key = " . (int) $pk . " THEN " . (int) $newState . " ELSE p2.$published END)"; 1422 } else { 1423 $newState = "p2.$published"; 1424 } 1425 1426 /** 1427 * We have to calculate the correct value for c2.published 1428 * based on p2.published and own c2.published column, 1429 * where (p2) is parent category is and (c2) current category 1430 * 1431 * p2.published <= c2.published AND p2.published > 0 THEN c2.published 1432 * 2 <= 2 THEN 2 (If archived in archived then archived) 1433 * 1 <= 2 THEN 2 (If archived in published then archived) 1434 * 1 <= 1 THEN 1 (If published in published then published) 1435 * 1436 * p2.published > c2.published AND c2.published > 0 THEN p2.published 1437 * 2 > 1 THEN 2 (If published in archived then archived) 1438 * 1439 * p2.published > c2.published THEN c2.published ELSE p2.published 1440 * 2 > -2 THEN -2 (If trashed in archived then trashed) 1441 * 2 > 0 THEN 0 (If unpublished in archived then unpublished) 1442 * 1 > 0 THEN 0 (If unpublished in published then unpublished) 1443 * 0 > -2 THEN -2 (If trashed in unpublished then trashed) 1444 * ELSE 1445 * 0 <= 2 THEN 0 (If archived in unpublished then unpublished) 1446 * 0 <= 1 THEN 0 (If published in unpublished then unpublished) 1447 * 0 <= 0 THEN 0 (If unpublished in unpublished then unpublished) 1448 * -2 <= -2 THEN -2 (If trashed in trashed then trashed) 1449 * -2 <= 0 THEN -2 (If unpublished in trashed then trashed) 1450 * -2 <= 1 THEN -2 (If published in trashed then trashed) 1451 * -2 <= 2 THEN -2 (If archived in trashed then trashed) 1452 */ 1453 1454 // Find node and all children keys 1455 $query->select("c.$key") 1456 ->from("$table AS node") 1457 ->leftJoin("$table AS c ON node.lft <= c.lft AND c.rgt <= node.rgt") 1458 ->where("node.$key = " . (int) $pk); 1459 1460 $pks = $this->_db->setQuery($query)->loadColumn(); 1461 1462 // Prepare a list of correct published states. 1463 $subquery = (string) $query->clear() 1464 ->select("c2.$key AS newId") 1465 ->select("CASE WHEN MIN($newState) > 0 THEN MAX($newState) ELSE MIN($newState) END AS newPublished") 1466 ->from("$table AS c2") 1467 ->innerJoin("$table AS p2 ON p2.lft <= c2.lft AND c2.rgt <= p2.rgt") 1468 ->where("c2.$key IN (" . implode(',', $pks) . ")") 1469 ->group("c2.$key"); 1470 1471 // Update and cascade the publishing state. 1472 $query->clear() 1473 ->update($table) 1474 ->innerJoin("($subquery) AS c2") 1475 ->set("$published = " . $this->_db->quoteName("c2.newpublished")) 1476 ->where("$key = c2.newId") 1477 ->where("$key IN (" . implode(',', $pks) . ")"); 1478 1479 $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED'); 1480 1481 return true; 1482 } 1483 1484 /** 1485 * Method to get nested set properties for a node in the tree. 1486 * 1487 * @param integer $id Value to look up the node by. 1488 * @param string $key An optional key to look up the node by (parent | left | right). 1489 * If omitted, the primary key of the table is used. 1490 * 1491 * @return mixed Boolean false on failure or node object on success. 1492 * 1493 * @since 1.7.0 1494 * @throws \RuntimeException on database error. 1495 */ 1496 protected function _getNode($id, $key = null) 1497 { 1498 // Determine which key to get the node base on. 1499 switch ($key) { 1500 case 'parent': 1501 $k = 'parent_id'; 1502 break; 1503 1504 case 'left': 1505 $k = 'lft'; 1506 break; 1507 1508 case 'right': 1509 $k = 'rgt'; 1510 break; 1511 1512 default: 1513 $k = $this->_tbl_key; 1514 break; 1515 } 1516 1517 // Get the node data. 1518 $query = $this->_db->getQuery(true) 1519 ->select($this->_tbl_key . ', parent_id, level, lft, rgt') 1520 ->from($this->_tbl) 1521 ->where($k . ' = ' . (int) $id); 1522 1523 $query->setLimit(1); 1524 $row = $this->_db->setQuery($query)->loadObject(); 1525 1526 // Check for no $row returned 1527 if (empty($row)) { 1528 $e = new \UnexpectedValueException(sprintf('%s::_getNode(%d, %s) failed.', \get_class($this), $id, $k)); 1529 $this->setError($e); 1530 1531 return false; 1532 } 1533 1534 // Do some simple calculations. 1535 $row->numChildren = (int) ($row->rgt - $row->lft - 1) / 2; 1536 $row->width = (int) $row->rgt - $row->lft + 1; 1537 1538 return $row; 1539 } 1540 1541 /** 1542 * Method to get various data necessary to make room in the tree at a location 1543 * for a node and its children. The returned data object includes conditions 1544 * for SQL WHERE clauses for updating left and right id values to make room for 1545 * the node as well as the new left and right ids for the node. 1546 * 1547 * @param object $referenceNode A node object with at least a 'lft' and 'rgt' with 1548 * which to make room in the tree around for a new node. 1549 * @param integer $nodeWidth The width of the node for which to make room in the tree. 1550 * @param string $position The position relative to the reference node where the room 1551 * should be made. 1552 * 1553 * @return mixed Boolean false on failure or data object on success. 1554 * 1555 * @since 1.7.0 1556 */ 1557 protected function _getTreeRepositionData($referenceNode, $nodeWidth, $position = 'before') 1558 { 1559 // Make sure the reference an object with a left and right id. 1560 if (!\is_object($referenceNode) || !(isset($referenceNode->lft) && isset($referenceNode->rgt))) { 1561 return false; 1562 } 1563 1564 // A valid node cannot have a width less than 2. 1565 if ($nodeWidth < 2) { 1566 return false; 1567 } 1568 1569 $k = $this->_tbl_key; 1570 $data = new \stdClass(); 1571 1572 // Run the calculations and build the data object by reference position. 1573 switch ($position) { 1574 case 'first-child': 1575 $data->left_where = 'lft > ' . $referenceNode->lft; 1576 $data->right_where = 'rgt >= ' . $referenceNode->lft; 1577 1578 $data->new_lft = $referenceNode->lft + 1; 1579 $data->new_rgt = $referenceNode->lft + $nodeWidth; 1580 $data->new_parent_id = $referenceNode->$k; 1581 $data->new_level = $referenceNode->level + 1; 1582 break; 1583 1584 case 'last-child': 1585 $data->left_where = 'lft > ' . ($referenceNode->rgt); 1586 $data->right_where = 'rgt >= ' . ($referenceNode->rgt); 1587 1588 $data->new_lft = $referenceNode->rgt; 1589 $data->new_rgt = $referenceNode->rgt + $nodeWidth - 1; 1590 $data->new_parent_id = $referenceNode->$k; 1591 $data->new_level = $referenceNode->level + 1; 1592 break; 1593 1594 case 'before': 1595 $data->left_where = 'lft >= ' . $referenceNode->lft; 1596 $data->right_where = 'rgt >= ' . $referenceNode->lft; 1597 1598 $data->new_lft = $referenceNode->lft; 1599 $data->new_rgt = $referenceNode->lft + $nodeWidth - 1; 1600 $data->new_parent_id = $referenceNode->parent_id; 1601 $data->new_level = $referenceNode->level; 1602 break; 1603 1604 default: 1605 case 'after': 1606 $data->left_where = 'lft > ' . $referenceNode->rgt; 1607 $data->right_where = 'rgt > ' . $referenceNode->rgt; 1608 1609 $data->new_lft = $referenceNode->rgt + 1; 1610 $data->new_rgt = $referenceNode->rgt + $nodeWidth; 1611 $data->new_parent_id = $referenceNode->parent_id; 1612 $data->new_level = $referenceNode->level; 1613 break; 1614 } 1615 1616 if ($this->_debug) { 1617 echo "\nRepositioning Data for $position\n-----------------------------------\nLeft Where: $data->left_where" 1618 . "\nRight Where: $data->right_where\nNew Lft: $data->new_lft\nNew Rgt: $data->new_rgt" 1619 . "\nNew Parent ID: $data->new_parent_id\nNew Level: $data->new_level\n"; 1620 } 1621 1622 return $data; 1623 } 1624 1625 /** 1626 * Method to create a log table in the buffer optionally showing the query and/or data. 1627 * 1628 * @param boolean $showData True to show data 1629 * @param boolean $showQuery True to show query 1630 * 1631 * @return void 1632 * 1633 * @codeCoverageIgnore 1634 * @since 1.7.0 1635 */ 1636 protected function _logtable($showData = true, $showQuery = true) 1637 { 1638 $sep = "\n" . str_pad('', 40, '-'); 1639 $buffer = ''; 1640 1641 if ($showQuery) { 1642 $buffer .= "\n" . htmlspecialchars($this->_db->getQuery(), ENT_QUOTES, 'UTF-8') . $sep; 1643 } 1644 1645 if ($showData) { 1646 $query = $this->_db->getQuery(true) 1647 ->select($this->_tbl_key . ', parent_id, lft, rgt, level') 1648 ->from($this->_tbl) 1649 ->order($this->_tbl_key); 1650 $this->_db->setQuery($query); 1651 1652 $rows = $this->_db->loadRowList(); 1653 $buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $this->_tbl_key, 'par', 'lft', 'rgt'); 1654 $buffer .= $sep; 1655 1656 foreach ($rows as $row) { 1657 $buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $row[0], $row[1], $row[2], $row[3]); 1658 } 1659 1660 $buffer .= $sep; 1661 } 1662 1663 echo $buffer; 1664 } 1665 1666 /** 1667 * Runs a query and unlocks the database on an error. 1668 * 1669 * @param mixed $query A string or DatabaseQuery object. 1670 * @param string $errorMessage Unused. 1671 * 1672 * @return void 1673 * 1674 * @note Since 3.0.0 this method returns void and will rethrow the database exception. 1675 * @since 1.7.0 1676 * @throws \Exception on database error. 1677 */ 1678 protected function _runQuery($query, $errorMessage) 1679 { 1680 // Prepare to catch an exception. 1681 try { 1682 $this->_db->setQuery($query)->execute(); 1683 1684 if ($this->_debug) { 1685 $this->_logtable(); 1686 } 1687 } catch (\Exception $e) { 1688 // Unlock the tables and rethrow. 1689 $this->_unlock(); 1690 1691 throw $e; 1692 } 1693 } 1694 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |