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