Verzeichnisstruktur phpBB-3.1.0


Veröffentlicht
27.10.2014

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

nestedset.php

Zuletzt modifiziert: 09.10.2024, 12:52 - Dateigröße: 27.21 KiB


 001  <?php
 002  /**
 003  *
 004  * This file is part of the phpBB Forum Software package.
 005  *
 006  * @copyright (c) phpBB Limited <https://www.phpbb.com>
 007  * @license GNU General Public License, version 2 (GPL-2.0)
 008  *
 009  * For full copyright and license information, please see
 010  * the docs/CREDITS.txt file.
 011  *
 012  */
 013   
 014  namespace phpbb\tree;
 015   
 016  abstract class nestedset implements \phpbb\tree\tree_interface
 017  {
 018      /** @var \phpbb\db\driver\driver_interface */
 019      protected $db;
 020   
 021      /** @var \phpbb\lock\db */
 022      protected $lock;
 023   
 024      /** @var string */
 025      protected $table_name;
 026   
 027      /**
 028      * Prefix for the language keys returned by exceptions
 029      * @var string
 030      */
 031      protected $message_prefix = '';
 032   
 033      /**
 034      * Column names in the table
 035      * @var string
 036      */
 037      protected $column_item_id = 'item_id';
 038      protected $column_left_id = 'left_id';
 039      protected $column_right_id = 'right_id';
 040      protected $column_parent_id = 'parent_id';
 041      protected $column_item_parents = 'item_parents';
 042   
 043      /**
 044      * Additional SQL restrictions
 045      * Allows to have multiple nested sets in one table
 046      * @var string
 047      */
 048      protected $sql_where = '';
 049   
 050      /**
 051      * List of item properties to be cached in the item_parents column
 052      * @var array
 053      */
 054      protected $item_basic_data = array('*');
 055   
 056      /**
 057      * Construct
 058      *
 059      * @param \phpbb\db\driver\driver_interface    $db        Database connection
 060      * @param \phpbb\lock\db        $lock    Lock class used to lock the table when moving forums around
 061      * @param string            $table_name            Table name
 062      * @param string            $message_prefix        Prefix for the messages thrown by exceptions
 063      * @param string            $sql_where            Additional SQL restrictions for the queries
 064      * @param array            $item_basic_data    Array with basic item data that is stored in item_parents
 065      * @param array            $columns            Array with column names to overwrite
 066      */
 067      public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\lock\db $lock, $table_name, $message_prefix = '', $sql_where = '', $item_basic_data = array(), $columns = array())
 068      {
 069          $this->db = $db;
 070          $this->lock = $lock;
 071   
 072          $this->table_name = $table_name;
 073          $this->message_prefix = $message_prefix;
 074          $this->sql_where = $sql_where;
 075          $this->item_basic_data = (!empty($item_basic_data)) ? $item_basic_data : array('*');
 076   
 077          if (!empty($columns))
 078          {
 079              foreach ($columns as $column => $name)
 080              {
 081                  $column_name = 'column_' . $column;
 082                  $this->$column_name = $name;
 083              }
 084          }
 085      }
 086   
 087      /**
 088      * Returns additional sql where restrictions
 089      *
 090      * @param string        $operator        SQL operator that needs to be prepended to sql_where,
 091      *                                    if it is not empty.
 092      * @param string        $column_prefix    Prefix that needs to be prepended to column names
 093      * @return string        Returns additional where statements to narrow down the tree,
 094      *                        prefixed with operator and prepended column_prefix to column names
 095      */
 096      public function get_sql_where($operator = 'AND', $column_prefix = '')
 097      {
 098          return (!$this->sql_where) ? '' : $operator . ' ' . sprintf($this->sql_where, $column_prefix);
 099      }
 100   
 101      /**
 102      * Acquires a lock on the item table
 103      *
 104      * @return bool    True if the lock was acquired, false if it has been acquired previously
 105      *
 106      * @throws \RuntimeException If the lock could not be acquired
 107      */
 108      protected function acquire_lock()
 109      {
 110          if ($this->lock->owns_lock())
 111          {
 112              return false;
 113          }
 114   
 115          if (!$this->lock->acquire())
 116          {
 117              throw new \RuntimeException($this->message_prefix . 'LOCK_FAILED_ACQUIRE');
 118          }
 119   
 120          return true;
 121      }
 122   
 123      /**
 124      * {@inheritdoc}
 125      */
 126      public function insert(array $additional_data)
 127      {
 128          $item_data = $this->reset_nestedset_values($additional_data);
 129   
 130          $sql = 'INSERT INTO ' . $this->table_name . ' ' . $this->db->sql_build_array('INSERT', $item_data);
 131          $this->db->sql_query($sql);
 132   
 133          $item_data[$this->column_item_id] = (int) $this->db->sql_nextid();
 134   
 135          return array_merge($item_data, $this->add_item_to_nestedset($item_data[$this->column_item_id]));
 136      }
 137   
 138      /**
 139      * Add an item which already has a database row at the end of the tree
 140      *
 141      * @param int    $item_id    The item to be added
 142      * @return array        Array with updated data, if the item was added successfully
 143      *                    Empty array otherwise
 144      */
 145      protected function add_item_to_nestedset($item_id)
 146      {
 147          $sql = 'SELECT MAX(' . $this->column_right_id . ') AS ' . $this->column_right_id . '
 148              FROM ' . $this->table_name . '
 149              ' . $this->get_sql_where('WHERE');
 150          $result = $this->db->sql_query($sql);
 151          $current_max_right_id = (int) $this->db->sql_fetchfield($this->column_right_id);
 152          $this->db->sql_freeresult($result);
 153   
 154          $update_item_data = array(
 155              $this->column_parent_id        => 0,
 156              $this->column_left_id        => $current_max_right_id + 1,
 157              $this->column_right_id        => $current_max_right_id + 2,
 158              $this->column_item_parents    => '',
 159          );
 160   
 161          $sql = 'UPDATE ' . $this->table_name . '
 162              SET ' . $this->db->sql_build_array('UPDATE', $update_item_data) . '
 163              WHERE ' . $this->column_item_id . ' = ' . (int) $item_id . '
 164                  AND ' . $this->column_parent_id . ' = 0
 165                  AND ' . $this->column_left_id . ' = 0
 166                  AND ' . $this->column_right_id . ' = 0';
 167          $this->db->sql_query($sql);
 168   
 169          return ($this->db->sql_affectedrows() == 1) ? $update_item_data : array();
 170      }
 171   
 172      /**
 173      * Remove an item from the tree without deleting it from the database
 174      *
 175      * Also removes all subitems from the tree without deleting them from the database either
 176      *
 177      * @param int    $item_id    The item to be deleted
 178      * @return array        Item ids that have been removed
 179      * @throws \OutOfBoundsException
 180      */
 181      protected function remove_item_from_nestedset($item_id)
 182      {
 183          $item_id = (int) $item_id;
 184          if (!$item_id)
 185          {
 186              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 187          }
 188   
 189          $items = $this->get_subtree_data($item_id);
 190          $item_ids = array_keys($items);
 191   
 192          if (empty($items) || !isset($items[$item_id]))
 193          {
 194              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 195          }
 196   
 197          $this->remove_subset($item_ids, $items[$item_id]);
 198   
 199          return $item_ids;
 200      }
 201   
 202      /**
 203      * {@inheritdoc}
 204      */
 205      public function delete($item_id)
 206      {
 207          $removed_items = $this->remove_item_from_nestedset($item_id);
 208   
 209          $sql = 'DELETE FROM ' . $this->table_name . '
 210              WHERE ' . $this->db->sql_in_set($this->column_item_id, $removed_items) . '
 211              ' . $this->get_sql_where('AND');
 212          $this->db->sql_query($sql);
 213   
 214          return $removed_items;
 215      }
 216   
 217      /**
 218      * {@inheritdoc}
 219      */
 220      public function move($item_id, $delta)
 221      {
 222          if ($delta == 0)
 223          {
 224              return false;
 225          }
 226   
 227          $this->acquire_lock();
 228   
 229          $action = ($delta > 0) ? 'move_up' : 'move_down';
 230          $delta = abs($delta);
 231   
 232          // Keep $this->get_sql_where() here, to ensure we are in the right tree.
 233          $sql = 'SELECT *
 234              FROM ' . $this->table_name . '
 235              WHERE ' . $this->column_item_id . ' = ' . (int) $item_id . '
 236                  ' . $this->get_sql_where();
 237          $result = $this->db->sql_query_limit($sql, $delta);
 238          $item = $this->db->sql_fetchrow($result);
 239          $this->db->sql_freeresult($result);
 240   
 241          if (!$item)
 242          {
 243              $this->lock->release();
 244              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 245          }
 246   
 247          /**
 248          * Fetch all the siblings between the item's current spot
 249          * and where we want to move it to. If there are less than $delta
 250          * siblings between the current spot and the target then the
 251          * item will move as far as possible
 252          */
 253          $sql = "SELECT {$this->column_item_id}, {$this->column_parent_id}, {$this->column_left_id}, {$this->column_right_id}, {$this->column_item_parents}
 254              FROM " . $this->table_name . '
 255              WHERE ' . $this->column_parent_id . ' = ' . (int) $item[$this->column_parent_id] . '
 256                  ' . $this->get_sql_where() . '
 257                  AND ';
 258   
 259          if ($action == 'move_up')
 260          {
 261              $sql .= $this->column_right_id . ' < ' . (int) $item[$this->column_right_id] . ' ORDER BY ' . $this->column_right_id . ' DESC';
 262          }
 263          else
 264          {
 265              $sql .= $this->column_left_id . ' > ' . (int) $item[$this->column_left_id] . ' ORDER BY ' . $this->column_left_id . ' ASC';
 266          }
 267   
 268          $result = $this->db->sql_query_limit($sql, $delta);
 269   
 270          $target = false;
 271          while ($row = $this->db->sql_fetchrow($result))
 272          {
 273              $target = $row;
 274          }
 275          $this->db->sql_freeresult($result);
 276   
 277          if (!$target)
 278          {
 279              $this->lock->release();
 280              // The item is already on top or bottom
 281              return false;
 282          }
 283   
 284          /**
 285          * $left_id and $right_id define the scope of the items that are affected by the move.
 286          * $diff_up and $diff_down are the values to substract or add to each item's left_id
 287          * and right_id in order to move them up or down.
 288          * $move_up_left and $move_up_right define the scope of the items that are moving
 289          * up. Other items in the scope of ($left_id, $right_id) are considered to move down.
 290          */
 291          if ($action == 'move_up')
 292          {
 293              $left_id = (int) $target[$this->column_left_id];
 294              $right_id = (int) $item[$this->column_right_id];
 295   
 296              $diff_up = (int) $item[$this->column_left_id] - (int) $target[$this->column_left_id];
 297              $diff_down = (int) $item[$this->column_right_id] + 1 - (int) $item[$this->column_left_id];
 298   
 299              $move_up_left = (int) $item[$this->column_left_id];
 300              $move_up_right = (int) $item[$this->column_right_id];
 301          }
 302          else
 303          {
 304              $left_id = (int) $item[$this->column_left_id];
 305              $right_id = (int) $target[$this->column_right_id];
 306   
 307              $diff_up = (int) $item[$this->column_right_id] + 1 - (int) $item[$this->column_left_id];
 308              $diff_down = (int) $target[$this->column_right_id] - (int) $item[$this->column_right_id];
 309   
 310              $move_up_left = (int) $item[$this->column_right_id] + 1;
 311              $move_up_right = (int) $target[$this->column_right_id];
 312          }
 313   
 314          // Now do the dirty job
 315          $sql = 'UPDATE ' . $this->table_name . '
 316              SET ' . $this->column_left_id . ' = ' . $this->column_left_id . ' + CASE
 317                  WHEN ' . $this->column_left_id . " BETWEEN {$move_up_left} AND {$move_up_right} THEN -{$diff_up}
 318                  ELSE {$diff_down}
 319              END,
 320              " . $this->column_right_id . ' = ' . $this->column_right_id . ' + CASE
 321                  WHEN ' . $this->column_right_id . " BETWEEN {$move_up_left} AND {$move_up_right} THEN -{$diff_up}
 322                  ELSE {$diff_down}
 323              END
 324              WHERE
 325                  " . $this->column_left_id . " BETWEEN {$left_id} AND {$right_id}
 326                  AND " . $this->column_right_id . " BETWEEN {$left_id} AND {$right_id}
 327                  " . $this->get_sql_where();
 328          $this->db->sql_query($sql);
 329   
 330          $this->lock->release();
 331   
 332          return true;
 333      }
 334   
 335      /**
 336      * {@inheritdoc}
 337      */
 338      public function move_down($item_id)
 339      {
 340          return $this->move($item_id, -1);
 341      }
 342   
 343      /**
 344      * {@inheritdoc}
 345      */
 346      public function move_up($item_id)
 347      {
 348          return $this->move($item_id, 1);
 349      }
 350   
 351      /**
 352      * {@inheritdoc}
 353      */
 354      public function move_children($current_parent_id, $new_parent_id)
 355      {
 356          $current_parent_id = (int) $current_parent_id;
 357          $new_parent_id = (int) $new_parent_id;
 358   
 359          if ($current_parent_id == $new_parent_id)
 360          {
 361              return false;
 362          }
 363   
 364          if (!$current_parent_id)
 365          {
 366              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 367          }
 368   
 369          $this->acquire_lock();
 370   
 371          $item_data = $this->get_subtree_data($current_parent_id);
 372          if (!isset($item_data[$current_parent_id]))
 373          {
 374              $this->lock->release();
 375              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 376          }
 377   
 378          $current_parent = $item_data[$current_parent_id];
 379          unset($item_data[$current_parent_id]);
 380          $move_items = array_keys($item_data);
 381   
 382          if (($current_parent[$this->column_right_id] - $current_parent[$this->column_left_id]) <= 1)
 383          {
 384              $this->lock->release();
 385              return false;
 386          }
 387   
 388          if (in_array($new_parent_id, $move_items))
 389          {
 390              $this->lock->release();
 391              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_PARENT');
 392          }
 393   
 394          $diff = sizeof($move_items) * 2;
 395          $sql_exclude_moved_items = $this->db->sql_in_set($this->column_item_id, $move_items, true);
 396   
 397          $this->db->sql_transaction('begin');
 398   
 399          $this->remove_subset($move_items, $current_parent, false, true);
 400   
 401          if ($new_parent_id)
 402          {
 403              // Retrieve new-parent again, it may have been changed...
 404              $sql = 'SELECT *
 405                  FROM ' . $this->table_name . '
 406                  WHERE ' . $this->column_item_id . ' = ' . $new_parent_id;
 407              $result = $this->db->sql_query($sql);
 408              $new_parent = $this->db->sql_fetchrow($result);
 409              $this->db->sql_freeresult($result);
 410   
 411              if (!$new_parent)
 412              {
 413                  $this->db->sql_transaction('rollback');
 414                  $this->lock->release();
 415                  throw new \OutOfBoundsException($this->message_prefix . 'INVALID_PARENT');
 416              }
 417   
 418              $new_right_id = $this->prepare_adding_subset($move_items, $new_parent, true);
 419   
 420              if ($new_right_id > $current_parent[$this->column_right_id])
 421              {
 422                  $diff = ' + ' . ($new_right_id - $current_parent[$this->column_right_id]);
 423              }
 424              else
 425              {
 426                  $diff = ' - ' . abs($new_right_id - $current_parent[$this->column_right_id]);
 427              }
 428          }
 429          else
 430          {
 431              $sql = 'SELECT MAX(' . $this->column_right_id . ') AS ' . $this->column_right_id . '
 432                  FROM ' . $this->table_name . '
 433                  WHERE ' . $sql_exclude_moved_items . '
 434                      ' . $this->get_sql_where('AND');
 435              $result = $this->db->sql_query($sql);
 436              $row = $this->db->sql_fetchrow($result);
 437              $this->db->sql_freeresult($result);
 438   
 439              $diff = ' + ' . ($row[$this->column_right_id] - $current_parent[$this->column_left_id]);
 440          }
 441   
 442          $sql = 'UPDATE ' . $this->table_name . '
 443              SET ' . $this->column_left_id . ' = ' . $this->column_left_id . $diff . ',
 444                  ' . $this->column_right_id . ' = ' . $this->column_right_id . $diff . ',
 445                  ' . $this->column_parent_id . ' = ' . $this->db->sql_case($this->column_parent_id . ' = ' . $current_parent_id, $new_parent_id, $this->column_parent_id) . ',
 446                  ' . $this->column_item_parents . " = ''
 447              WHERE " . $this->db->sql_in_set($this->column_item_id, $move_items) . '
 448                  ' . $this->get_sql_where('AND');
 449          $this->db->sql_query($sql);
 450   
 451          $this->db->sql_transaction('commit');
 452          $this->lock->release();
 453   
 454          return true;
 455      }
 456   
 457      /**
 458      * {@inheritdoc}
 459      */
 460      public function change_parent($item_id, $new_parent_id)
 461      {
 462          $item_id = (int) $item_id;
 463          $new_parent_id = (int) $new_parent_id;
 464   
 465          if ($item_id == $new_parent_id)
 466          {
 467              return false;
 468          }
 469   
 470          if (!$item_id)
 471          {
 472              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 473          }
 474   
 475          $this->acquire_lock();
 476   
 477          $item_data = $this->get_subtree_data($item_id);
 478          if (!isset($item_data[$item_id]))
 479          {
 480              $this->lock->release();
 481              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_ITEM');
 482          }
 483   
 484          $item = $item_data[$item_id];
 485          $move_items = array_keys($item_data);
 486   
 487          if (in_array($new_parent_id, $move_items))
 488          {
 489              $this->lock->release();
 490              throw new \OutOfBoundsException($this->message_prefix . 'INVALID_PARENT');
 491          }
 492   
 493          $diff = sizeof($move_items) * 2;
 494          $sql_exclude_moved_items = $this->db->sql_in_set($this->column_item_id, $move_items, true);
 495   
 496          $this->db->sql_transaction('begin');
 497   
 498          $this->remove_subset($move_items, $item, false, true);
 499   
 500          if ($new_parent_id)
 501          {
 502              // Retrieve new-parent again, it may have been changed...
 503              $sql = 'SELECT *
 504                  FROM ' . $this->table_name . '
 505                  WHERE ' . $this->column_item_id . ' = ' . $new_parent_id;
 506              $result = $this->db->sql_query($sql);
 507              $new_parent = $this->db->sql_fetchrow($result);
 508              $this->db->sql_freeresult($result);
 509   
 510              if (!$new_parent)
 511              {
 512                  $this->db->sql_transaction('rollback');
 513                  $this->lock->release();
 514                  throw new \OutOfBoundsException($this->message_prefix . 'INVALID_PARENT');
 515              }
 516   
 517              $new_right_id = $this->prepare_adding_subset($move_items, $new_parent, true);
 518   
 519              if ($new_right_id > (int) $item[$this->column_right_id])
 520              {
 521                  $diff = ' + ' . ($new_right_id - (int) $item[$this->column_right_id] - 1);
 522              }
 523              else
 524              {
 525                  $diff = ' - ' . abs($new_right_id - (int) $item[$this->column_right_id] - 1);
 526              }
 527          }
 528          else
 529          {
 530              $sql = 'SELECT MAX(' . $this->column_right_id . ') AS ' . $this->column_right_id . '
 531                  FROM ' . $this->table_name . '
 532                  WHERE ' . $sql_exclude_moved_items . '
 533                      ' . $this->get_sql_where('AND');
 534              $result = $this->db->sql_query($sql);
 535              $row = $this->db->sql_fetchrow($result);
 536              $this->db->sql_freeresult($result);
 537   
 538              $diff = ' + ' . ($row[$this->column_right_id] - (int) $item[$this->column_left_id] + 1);
 539          }
 540   
 541          $sql = 'UPDATE ' . $this->table_name . '
 542              SET ' . $this->column_left_id . ' = ' . $this->column_left_id . $diff . ',
 543                  ' . $this->column_right_id . ' = ' . $this->column_right_id . $diff . ',
 544                  ' . $this->column_parent_id . ' = ' . $this->db->sql_case($this->column_item_id . ' = ' . $item_id, $new_parent_id, $this->column_parent_id) . ',
 545                  ' . $this->column_item_parents . " = ''
 546              WHERE " . $this->db->sql_in_set($this->column_item_id, $move_items) . '
 547                  ' . $this->get_sql_where('AND');
 548          $this->db->sql_query($sql);
 549   
 550          $this->db->sql_transaction('commit');
 551          $this->lock->release();
 552   
 553          return true;
 554      }
 555   
 556      /**
 557      * {@inheritdoc}
 558      */
 559      public function get_path_and_subtree_data($item_id, $order_asc = true, $include_item = true)
 560      {
 561          $condition = 'i2.' . $this->column_left_id . ' BETWEEN i1.' . $this->column_left_id . ' AND i1.' . $this->column_right_id . '
 562              OR i1.' . $this->column_left_id . ' BETWEEN i2.' . $this->column_left_id . ' AND i2.' . $this->column_right_id;
 563   
 564          return $this->get_set_of_nodes_data($item_id, $condition, $order_asc, $include_item);
 565      }
 566   
 567      /**
 568      * {@inheritdoc}
 569      */
 570      public function get_path_data($item_id, $order_asc = true, $include_item = true)
 571      {
 572          $condition = 'i1.' . $this->column_left_id . ' BETWEEN i2.' . $this->column_left_id . ' AND i2.' . $this->column_right_id . '';
 573   
 574          return $this->get_set_of_nodes_data($item_id, $condition, $order_asc, $include_item);
 575      }
 576   
 577      /**
 578      * {@inheritdoc}
 579      */
 580      public function get_subtree_data($item_id, $order_asc = true, $include_item = true)
 581      {
 582          $condition = 'i2.' . $this->column_left_id . ' BETWEEN i1.' . $this->column_left_id . ' AND i1.' . $this->column_right_id . '';
 583   
 584          return $this->get_set_of_nodes_data($item_id, $condition, $order_asc, $include_item);
 585      }
 586   
 587      /**
 588      * Get items that are related to the given item by the condition
 589      *
 590      * @param int        $item_id        Id of the item to retrieve the node set from
 591      * @param string        $condition        Query string restricting the item list
 592      * @param bool        $order_asc        Order the items ascending by their left_id
 593      * @param bool        $include_item    Should the item matching the given item id be included in the list as well
 594      * @return array            Array of items (containing all columns from the item table)
 595      *                            ID => Item data
 596      */
 597      protected function get_set_of_nodes_data($item_id, $condition, $order_asc = true, $include_item = true)
 598      {
 599          $rows = array();
 600   
 601          $sql = 'SELECT i2.*
 602              FROM ' . $this->table_name . ' i1
 603              LEFT JOIN ' . $this->table_name . " i2
 604                  ON (($condition) " . $this->get_sql_where('AND', 'i2.') . ')
 605              WHERE i1.' . $this->column_item_id . ' = ' . (int) $item_id . '
 606                  ' . $this->get_sql_where('AND', 'i1.') . '
 607              ORDER BY i2.' . $this->column_left_id . ' ' . ($order_asc ? 'ASC' : 'DESC');
 608          $result = $this->db->sql_query($sql);
 609   
 610          while ($row = $this->db->sql_fetchrow($result))
 611          {
 612              if (!$include_item && $item_id == $row[$this->column_item_id])
 613              {
 614                  continue;
 615              }
 616   
 617              $rows[(int) $row[$this->column_item_id]] = $row;
 618          }
 619          $this->db->sql_freeresult($result);
 620   
 621          return $rows;
 622      }
 623   
 624      /**
 625      * Get basic data of all parent items
 626      *
 627      * Basic data is defined in the $item_basic_data property.
 628      * Data is cached in the item_parents column in the item table
 629      *
 630      * @param array    $item        The item to get the path from
 631      * @return array            Array of items (containing basic columns from the item table)
 632      *                            ID => Item data
 633      */
 634      public function get_path_basic_data(array $item)
 635      {
 636          $parents = array();
 637          if ($item[$this->column_parent_id])
 638          {
 639              if (!$item[$this->column_item_parents])
 640              {
 641                  $sql = 'SELECT ' . implode(', ', $this->item_basic_data) . '
 642                      FROM ' . $this->table_name . '
 643                      WHERE ' . $this->column_left_id . ' < ' . (int) $item[$this->column_left_id] . '
 644                          AND ' . $this->column_right_id . ' > ' . (int) $item[$this->column_right_id] . '
 645                          ' . $this->get_sql_where('AND') . '
 646                      ORDER BY ' . $this->column_left_id . ' ASC';
 647                  $result = $this->db->sql_query($sql);
 648   
 649                  while ($row = $this->db->sql_fetchrow($result))
 650                  {
 651                      $parents[$row[$this->column_item_id]] = $row;
 652                  }
 653                  $this->db->sql_freeresult($result);
 654   
 655                  $item_parents = serialize($parents);
 656   
 657                  $sql = 'UPDATE ' . $this->table_name . '
 658                      SET ' . $this->column_item_parents . " = '" . $this->db->sql_escape($item_parents) . "'
 659                      WHERE " . $this->column_parent_id . ' = ' . (int) $item[$this->column_parent_id];
 660                  $this->db->sql_query($sql);
 661              }
 662              else
 663              {
 664                  $parents = unserialize($item[$this->column_item_parents]);
 665              }
 666          }
 667   
 668          return $parents;
 669      }
 670   
 671      /**
 672      * Get all items from the tree
 673      *
 674      * @param bool        $order_asc        Order the items ascending by their left_id
 675      * @return array            Array of items (containing all columns from the item table)
 676      *                            ID => Item data
 677      */
 678      public function get_all_tree_data($order_asc = true)
 679      {
 680          $rows = array();
 681   
 682          $sql = 'SELECT *
 683              FROM ' . $this->table_name . ' ' .
 684              $this->get_sql_where('WHERE') . '
 685              ORDER BY ' . $this->column_left_id . ' ' . ($order_asc ? 'ASC' : 'DESC');
 686          $result = $this->db->sql_query($sql);
 687   
 688          while ($row = $this->db->sql_fetchrow($result))
 689          {
 690              $rows[(int) $row[$this->column_item_id]] = $row;
 691          }
 692          $this->db->sql_freeresult($result);
 693   
 694          return $rows;
 695      }
 696   
 697      /**
 698      * Remove a subset from the nested set
 699      *
 700      * @param array    $subset_items        Subset of items to remove
 701      * @param array    $bounding_item        Item containing the right bound of the subset
 702      * @param bool    $set_subset_zero    Should the parent, left and right id of the items be set to 0, or kept unchanged?
 703      *                                    In case of removing an item from the tree, we should the values to 0
 704      *                                    In case of moving an item, we shouldkeep the original values, in order to allow "+ diff" later
 705      * @return    null
 706      */
 707      protected function remove_subset(array $subset_items, array $bounding_item, $set_subset_zero = true)
 708      {
 709          $acquired_new_lock = $this->acquire_lock();
 710   
 711          $diff = sizeof($subset_items) * 2;
 712          $sql_subset_items = $this->db->sql_in_set($this->column_item_id, $subset_items);
 713          $sql_not_subset_items = $this->db->sql_in_set($this->column_item_id, $subset_items, true);
 714   
 715          $sql_is_parent = $this->column_left_id . ' <= ' . (int) $bounding_item[$this->column_right_id] . '
 716              AND ' . $this->column_right_id . ' >= ' . (int) $bounding_item[$this->column_right_id];
 717   
 718          $sql_is_right = $this->column_left_id . ' > ' . (int) $bounding_item[$this->column_right_id];
 719   
 720          $set_left_id = $this->db->sql_case($sql_is_right, $this->column_left_id . ' - ' . $diff, $this->column_left_id);
 721          $set_right_id = $this->db->sql_case($sql_is_parent . ' OR ' . $sql_is_right, $this->column_right_id . ' - ' . $diff, $this->column_right_id);
 722   
 723          if ($set_subset_zero)
 724          {
 725              $set_left_id = $this->db->sql_case($sql_subset_items, 0, $set_left_id);
 726              $set_right_id = $this->db->sql_case($sql_subset_items, 0, $set_right_id);
 727          }
 728   
 729          $sql = 'UPDATE ' . $this->table_name . '
 730              SET ' . (($set_subset_zero) ? $this->column_parent_id . ' = ' . $this->db->sql_case($sql_subset_items, 0, $this->column_parent_id) . ',' : '') . '
 731                  ' . $this->column_left_id . ' = ' . $set_left_id . ',
 732                  ' . $this->column_right_id . ' = ' . $set_right_id . '
 733              ' . ((!$set_subset_zero) ? ' WHERE ' . $sql_not_subset_items . ' ' . $this->get_sql_where('AND') : $this->get_sql_where('WHERE'));
 734          $this->db->sql_query($sql);
 735   
 736          if ($acquired_new_lock)
 737          {
 738              $this->lock->release();
 739          }
 740      }
 741   
 742      /**
 743      * Prepare adding a subset to the nested set
 744      *
 745      * @param array    $subset_items        Subset of items to add
 746      * @param array    $new_parent    Item containing the right bound of the new parent
 747      * @return    int        New right id of the parent item
 748      */
 749      protected function prepare_adding_subset(array $subset_items, array $new_parent)
 750      {
 751          $diff = sizeof($subset_items) * 2;
 752          $sql_not_subset_items = $this->db->sql_in_set($this->column_item_id, $subset_items, true);
 753   
 754          $set_left_id = $this->db->sql_case($this->column_left_id . ' > ' . (int) $new_parent[$this->column_right_id], $this->column_left_id . ' + ' . $diff, $this->column_left_id);
 755          $set_right_id = $this->db->sql_case($this->column_right_id . ' >= ' . (int) $new_parent[$this->column_right_id], $this->column_right_id . ' + ' . $diff, $this->column_right_id);
 756   
 757          $sql = 'UPDATE ' . $this->table_name . '
 758              SET ' . $this->column_left_id . ' = ' . $set_left_id . ',
 759                  ' . $this->column_right_id . ' = ' . $set_right_id . '
 760              WHERE ' . $sql_not_subset_items . '
 761                  ' . $this->get_sql_where('AND');
 762          $this->db->sql_query($sql);
 763   
 764          return $new_parent[$this->column_right_id] + $diff;
 765      }
 766   
 767      /**
 768      * Resets values required for the nested set system
 769      *
 770      * @param array    $item        Original item data
 771      * @return    array        Original item data + nested set defaults
 772      */
 773      protected function reset_nestedset_values(array $item)
 774      {
 775          $item_data = array_merge($item, array(
 776              $this->column_parent_id        => 0,
 777              $this->column_left_id        => 0,
 778              $this->column_right_id        => 0,
 779              $this->column_item_parents    => '',
 780          ));
 781   
 782          unset($item_data[$this->column_item_id]);
 783   
 784          return $item_data;
 785      }
 786   
 787      /**
 788      * Regenerate left/right ids from parent/child relationship
 789      *
 790      * This method regenerates the left/right ids for the tree based on
 791      * the parent/child relations. This function executes three queries per
 792      * item, so it should only be called, when the set has one of the following
 793      * problems:
 794      *    - The set has a duplicated value inside the left/right id chain
 795      *    - The set has a missing value inside the left/right id chain
 796      *    - The set has items that do not have a left/right id set
 797      *
 798      * When regenerating the items, the items are sorted by parent id and their
 799      * current left id, so the current child/parent relationships are kept
 800      * and running the function on a working set will not change the order.
 801      *
 802      * @param int    $new_id        First left_id to be used (should start with 1)
 803      * @param int    $parent_id    parent_id of the current set (default = 0)
 804      * @param bool    $reset_ids    Should we reset all left_id/right_id on the first call?
 805      * @return    int        $new_id        The next left_id/right_id that should be used
 806      */
 807      public function regenerate_left_right_ids($new_id, $parent_id = 0, $reset_ids = false)
 808      {
 809          if ($acquired_new_lock = $this->acquire_lock())
 810          {
 811              $this->db->sql_transaction('begin');
 812   
 813              if (!$reset_ids)
 814              {
 815                  $sql = 'UPDATE ' . $this->table_name . '
 816                      SET ' . $this->column_item_parents . " = ''
 817                      " . $this->get_sql_where('WHERE');
 818                  $this->db->sql_query($sql);
 819              }
 820          }
 821   
 822          if ($reset_ids)
 823          {
 824              $sql = 'UPDATE ' . $this->table_name . '
 825                  SET ' . $this->db->sql_build_array('UPDATE', array(
 826                      $this->column_left_id        => 0,
 827                      $this->column_right_id        => 0,
 828                      $this->column_item_parents    => '',
 829                  )) . '
 830                  ' . $this->get_sql_where('WHERE');
 831              $this->db->sql_query($sql);
 832          }
 833   
 834          $sql = 'SELECT *
 835              FROM ' . $this->table_name . '
 836              WHERE ' . $this->column_parent_id . ' = ' . (int) $parent_id . '
 837                  ' . $this->get_sql_where('AND') . '
 838              ORDER BY ' . $this->column_left_id . ', ' . $this->column_item_id . ' ASC';
 839          $result = $this->db->sql_query($sql);
 840          while ($row = $this->db->sql_fetchrow($result))
 841          {
 842              // First we update the left_id for this module
 843              if ($row[$this->column_left_id] != $new_id)
 844              {
 845                  $sql = 'UPDATE ' . $this->table_name . '
 846                      SET ' . $this->db->sql_build_array('UPDATE', array($this->column_left_id => $new_id)) . '
 847                      WHERE ' . $this->column_item_id . ' = ' . (int) $row[$this->column_item_id];
 848                  $this->db->sql_query($sql);
 849              }
 850              $new_id++;
 851   
 852              // Then we go through any children and update their left/right id's
 853              $new_id = $this->regenerate_left_right_ids($new_id, $row[$this->column_item_id]);
 854   
 855              // Then we come back and update the right_id for this module
 856              if ($row[$this->column_right_id] != $new_id)
 857              {
 858                  $sql = 'UPDATE ' . $this->table_name . '
 859                      SET ' . $this->db->sql_build_array('UPDATE', array($this->column_right_id => $new_id)) . '
 860                      WHERE ' . $this->column_item_id . ' = ' . (int) $row[$this->column_item_id];
 861                  $this->db->sql_query($sql);
 862              }
 863              $new_id++;
 864          }
 865          $this->db->sql_freeresult($result);
 866   
 867          if ($acquired_new_lock)
 868          {
 869              $this->db->sql_transaction('commit');
 870              $this->lock->release();
 871          }
 872   
 873          return $new_id;
 874      }
 875  }
 876