[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/src/Table/ -> Nested.php (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer