From d32455d043e493270fff161bdfc258247e25e147 Mon Sep 17 00:00:00 2001 From: origami11 Date: Fri, 17 Feb 2017 16:27:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81?= =?UTF-8?q?=20=D0=B4=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=BC=20=D0=B2=20=D0=B2?= =?UTF-8?q?=D0=B8=D0=B4=D0=B5=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=BC=D0=BD=D0=BE=D0=B6=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + composer.json | 15 ++ src/DBtree.php | 636 +++++++++++++++++++++++++++++++++++++++++++++++ src/Database.php | 135 ++++++++++ src/Sort.php | 79 ++++++ 5 files changed, 866 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 src/DBtree.php create mode 100644 src/Database.php create mode 100644 src/Sort.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7664704 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.bak \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6e4ea43 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "ctiso/tree", + "description": "nested set tree", + "authors": [ + { + "name": "Phedor Podlesnov", + "email": "phedor@edu.yar.ru" + } + ], + "autoload": { + "psr-0": { + "Tree": "src/" + } + } +} diff --git a/src/DBtree.php b/src/DBtree.php new file mode 100644 index 0000000..1a5792f --- /dev/null +++ b/src/DBtree.php @@ -0,0 +1,636 @@ + +// WWW: http://dev.e-taller.net/dbtree/ +// Category: Databases +// Description: PHP class implementing a Nested Sets approach to storing +// tree-like structures in database tables. This technique was +// introduced by Joe Celko and has some +// advantages over a widely used method called Adjacency Matrix. +//**************************************************************************** +// The lib is FREEWARE. That means you may use it anywhere you want, you may +// do anything with it. The Author mentioned above is NOT responsible for any +// consequences of using this library. +// If you don't agree with this, you are NOT ALLOWED to use the lib! +//**************************************************************************** +// You're welcome to send me all improvings, feature requests, bug reports... +//**************************************************************************** +// SAMPLE DB TABLE STRUCTURE: +// +// CREATE TABLE categories ( +// cat_id INT UNSIGNED NOT NULL AUTO_INCREMENT, +// cat_left INT UNSIGNED NOT NULL, +// cat_right INT UNSIGNED NOT NULL, +// cat_level INT UNSIGNED NOT NULL, +// PRIMARY KEY(cat_id), +// KEY(cat_left, cat_right, cat_level) +// ); +// +// This is believed to be the optimal Nested Sets use case. Use `one-to-one` +// relations on `cat_id` field between this `structure` table and +// another `data` table in your database. +// +// Don't forget to make a single call to clear() +// to set up the Root node in an empty table. +// +//**************************************************************************** +// NOTE: Although you may use this library to retrieve data from the table, +// it is recommended to write your own queries for doing that. +// The main purpose of the library is to provide a simpler way to +// create, update and delete records. Not to SELECT them. +//**************************************************************************** +// +// IMPORTANT! DO NOT create either UNIQUE or PRIMARY keys on the set of +// fields (`cat_left`, `cat_right` and `cat_level`)! +// Unique keys will destroy the Nested Sets structure! +// +//**************************************************************************** +// CHANGELOG: +// 16-Apr-2003 -=- 1.1 +// - Added moveAll() method +// - Added fourth parameter to the constructor +// - Renamed getElementInfo() to getNodeInfo() /keeping BC/ +// - Added "Sample Table Structure" comment +// - Now it trigger_error()'s in case of any serious error, because if not you +// will lose the Nested Sets structure in your table +// 19-Feb-2004 -=- 1.2 +// - Fixed a bug in moveAll() method? +// Thanks to Maxim Matyukhin for the patch. +// 13-Jul-2004 -=- 1.3 +// - Changed all die()'s for a more standard trigger_error() +// Thanks to Dmitry Romakhin for pointing out an issue with +// incorrect error message call. +// 09-Nov-2004 -=- 1.4 +// - Added insertNear() method. +// Thanks to Michael Krenz who sent its implementation. +// - Removed IGNORE keyword from UPDATE clauses in insert() methods. +// It was dumb to have it there all the time. Sorry. Anyway, you had +// to follow the important note mencioned above. If you hadn't, you're +// in problem. +// +//**************************************************************************** +// Note: For best viewing of the code Tab size 4 is recommended +//**************************************************************************** + +function _case($c, $then, $else) { + return " (CASE WHEN $c THEN $then ELSE $else END) "; +} + +function _between($a, $x, $y) { + return " " . $a . " BETWEEN " . $x . " AND " . $y; +} + +function _inside($a, $x, $y) { + return " " . $a . " > " . $x . " AND " . $a . " < " . $y; +} + +class Tree_DBTree +{ + var $db; // CDatabase class to plug to + var $table; // Table with Nested Sets implemented + var $id; // Name of the ID-auto_increment-field in the table. + // These 3 variables are names of fields which are needed to implement + // Nested Sets. All 3 fields should exist in your table! + // However, you may want to change their names here to avoid name collisions. + var $left = 'cat_left'; + var $right = 'cat_right'; + var $level = 'cat_level'; + var $qryParams = ''; + var $qryFields = ''; + var $qryTables = ''; + var $qryWhere = ''; + var $qryGroupBy = ''; + var $qryHaving = ''; + var $qryOrderBy = ''; + var $qryLimit = ''; + var $sqlNeedReset = true; + var $sql; // Last SQL query + + //************************************************************************ + // Constructor + // $DB : CDatabase class instance to link to + // $tableName : table in database where to implement nested sets + // $itemId : name of the field which will uniquely identify every record + // $fieldNames : optional configuration array to set field names. Example: + // array( + // 'left' => 'cat_left', + // 'right' => 'cat_right', + // 'level' => 'cat_level' + // ) + function CDBTree(&$DB, $tableName, $itemId, $seq, $fieldNames = array()) + { + + if (empty($tableName)) trigger_error("phpDbTree: Unknown table", E_USER_ERROR); + + if (empty($itemId)) trigger_error("phpDbTree: Unknown ID column", E_USER_ERROR); + $this->seq = $seq; + $this->db = $DB; + $this->table = $tableName; + $this->id = $itemId; + + if (is_array($fieldNames) && sizeof($fieldNames)) + foreach($fieldNames as $k => $v) $this->$k = $v; + } + + //************************************************************************ + // Returns a Left and Right IDs and Level of an element or false on error + // $ID : an ID of the element + function getElementInfo($ID) + { + return $this->getNodeInfo($ID); + } + + function getNodeInfo($ID) + { + $this->sql = 'SELECT ' . $this->left . ',' . $this->right . ',' . $this->level . ' FROM ' . $this->table . ' WHERE ' . $this->id . '=\'' . $ID . '\''; +// print_r($this->sql); + + if (($query = $this->db->query($this->sql)) && ($this->db->num_rows($query) == 1) && ($Data = $this->db->fetch_array($query))) return array( + (int)$Data[$this->left], + (int)$Data[$this->right], + (int)$Data[$this->level] + ); + else trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); // throw new Exception(); + } + + //************************************************************************ + // Clears table and creates 'root' node + // $data : optional argument with data for the root node + function clear($data = array()) + { + // clearing table + + if ((!$this->db->query('TRUNCATE ' . $this->table)) && (!$this->db->query('DELETE FROM ' . $this->table))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // preparing data to be inserted + + if (sizeof($data)) { + $fld_names = implode(',', array_keys($data)) . ','; + + if (sizeof($data)) $fld_values = '\'' . implode('\',\'', array_values($data)) . '\','; + } + $fld_names.= $this->left . ',' . $this->right . ',' . $this->level; + $fld_values.= '1,2,0'; + // inserting new record + $this->sql = 'INSERT INTO ' . $this->table . '(' . $fld_names . ') VALUES(' . $fld_values . ')'; + + if (!($this->db->query($this->sql))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + return $this->db->insert_id($this->seq); + } + + //************************************************************************ + // Updates a record + // $ID : element ID + // $data : array with data to update: array( => ) + function update($ID, $data) + { + $sql_set = ''; + + foreach($data as $k => $v) $sql_set.= ',' . $k . '=\'' . addslashes($v) . '\''; + return $this->db->query('UPDATE ' . $this->table . ' SET ' . substr($sql_set, 1) . ' WHERE ' . $this->id . '=\'' . $ID . '\''); + } + + //************************************************************************ + // Inserts a record into the table with nested sets + // $ID : an ID of the parent element + // $data : array with data to be inserted: array( => ) + // Returns : true on success, or false on error + function insert($ID, $data) + { + + if (!(list($leftId, $rightId, $level) = $this->getNodeInfo($ID))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // preparing data to be inserted + + if (sizeof($data)) { + $fld_names = implode(',', array_keys($data)) . ','; + $fld_values = '\'' . implode('\',\'', array_values($data)) . '\','; + } + $fld_names.= $this->left . ',' . $this->right . ',' . $this->level; + $fld_values.= ($rightId) . ',' . ($rightId+1) . ',' . ($level+1); + // creating a place for the record being inserted + + if ($ID) { + $this->sql = 'UPDATE ' . $this->table . ' SET ' . $this->left . '=' . _case($this->left . '>' . $rightId, $this->left . '+2', $this->left) . ',' . $this->right . '=' . _case($this->right . '>=' . $rightId, $this->right . '+2', $this->right) . 'WHERE ' . $this->right . '>=' . $rightId; + + if (!($this->db->query($this->sql))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + } + // inserting new record + $this->sql = 'INSERT INTO ' . $this->table . '(' . $fld_names . ') VALUES(' . $fld_values . ')'; + + if (!($this->db->query($this->sql))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + return $this->db->insert_id($this->seq); + } + + //************************************************************************ + // Inserts a record into the table with nested sets + // $ID : ID of the element after which (i.e. at the same level) the new element + // is to be inserted + // $data : array with data to be inserted: array( => ) + // Returns : true on success, or false on error + function insertNear($ID, $data) + { + + if (!(list($leftId, $rightId, $level) = $this->getNodeInfo($ID))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // preparing data to be inserted + + if (sizeof($data)) { + $fld_names = implode(',', array_keys($data)) . ','; + $fld_values = '\'' . implode('\',\'', array_values($data)) . '\','; + } + $fld_names.= $this->left . ',' . $this->right . ',' . $this->level; + $fld_values.= ($rightId+1) . ',' . ($rightId+2) . ',' . ($level); + // creating a place for the record being inserted + + if ($ID) { + $this->sql = 'UPDATE ' . $this->table . ' SET ' . $this->left . '=' . _case($this->left . '>' . $rightId, $this->left . '+2', $this->left) . $this->right . '=' . _case($this->right . '>' . $rightId, $this->right . '+2', $this->right) . 'WHERE ' . $this->right . '>' . $rightId; + + if (!($this->db->query($this->sql))) trigger_error("phpDbTree error:" . $this->db->error() , E_USER_ERROR); + } + // inserting new record + $this->sql = 'INSERT INTO ' . $this->table . '(' . $fld_names . ') VALUES(' . $fld_values . ')'; + + if (!($this->db->query($this->sql))) trigger_error("phpDbTree error:" . $this->db->error() , E_USER_ERROR); + return $this->db->insert_id($this->seq); + } + + //************************************************************************ + // Assigns a node with all its children to another parent + // $ID : node ID + // $newParentID : ID of new parent node + // Returns : false on error + function moveAll($ID, $newParentId) + { + + if (!(list($leftId, $rightId, $level) = $this->getNodeInfo($ID))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + + if (!(list($leftIdP, $rightIdP, $levelP) = $this->getNodeInfo($newParentId))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + + if ($ID == $newParentId || $leftId == $leftIdP || ($leftIdP >= $leftId && $leftIdP <= $rightId)) return false; + // whether it is being moved upwards along the path + + if ($leftIdP < $leftId && $rightIdP > $rightId && $levelP < $level-1) { + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->level . '=' . _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->level . sprintf('%+d', -($level-1) +$levelP) , $this->level) . "," + . $this->right . '=' . _case($this->right . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) , $this->right . '-' . ($rightId-$leftId+1) + , _case($this->left . ' BETWEEN ' . ($leftId) . ' AND ' . ($rightId) , $this->right . '+' . ((($rightIdP-$rightId-$level+$levelP) /2) *2+$level-$levelP-1) , $this->right)) . "," + . $this->left . '=' . _case($this->left . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) , $this->left . '-' . ($rightId-$leftId+1) , _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . ($rightId) , $this->left . '+' . ((($rightIdP-$rightId-$level+$levelP) /2) *2+$level-$levelP-1) , $this->left)) . 'WHERE ' . $this->left . ' BETWEEN ' . ($leftIdP+1) . ' AND ' . ($rightIdP-1); + + } elseif ($leftIdP < $leftId) { + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->level . '=' . _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->level . sprintf('%+d', -($level-1) +$levelP) , $this->level) . "," + . $this->left . '=' . _case($this->left . ' BETWEEN ' . $rightIdP . ' AND ' . ($leftId-1) , $this->left . '+' . ($rightId-$leftId+1) + , _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->left . '-' . ($leftId-$rightIdP) , $this->left)) . "," + . $this->right . '=' . _case($this->right . ' BETWEEN ' . $rightIdP . ' AND ' . $leftId, $this->right . '+' . ($rightId-$leftId+1) , _case($this->right . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->right . '-' . ($leftId-$rightIdP) , $this->right)) . 'WHERE ' . $this->left . ' BETWEEN ' . $leftIdP . ' AND ' . $rightId + // !!! added this line (Maxim Matyukhin) + . ' OR ' . $this->right . ' BETWEEN ' . $leftIdP . ' AND ' . $rightId; + } else { + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->level . '=' . _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->level . sprintf('%+d', -($level-1) +$levelP) , $this->level) . "," + . $this->left . '=' . _case($this->left . ' BETWEEN ' . $rightId . ' AND ' . $rightIdP, $this->left . '-' . ($rightId-$leftId+1) + , _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->left . '+' . ($rightIdP-1-$rightId), $this->left)) . ", " + . $this->right . '=' . _case($this->right . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) , $this->right . '-' . ($rightId-$leftId+1) , _case($this->right . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->right . '+' . ($rightIdP-1-$rightId) , $this->right)) . 'WHERE ' . $this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightIdP + // !!! added this line (Maxim Matyukhin) + . ' OR ' . $this->right . ' BETWEEN ' . $leftId . ' AND ' . $rightIdP; + } + return $this->db->query($this->sql) or trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + } + + // Перемещение всех детей ветки в другую ветку + function moveChildren($ID, $newParentId) + { + + if (!(list($leftId, $rightId, $level) = $this->getNodeInfo($ID))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + + if (!(list($leftIdP, $rightIdP, $levelP) = $this->getNodeInfo($newParentId))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + + if ($ID == $newParentId || $leftId == $leftIdP || ($leftIdP >= $leftId && $leftIdP <= $rightId)) return false; + // whether it is being moved upwards along the path + + if ($leftIdP < $leftId && $rightIdP > $rightId && $levelP < $level-1) { + // _update($this->table, array(), ) + $this->sql = 'UPDATE ' . $this->table . ' SET ' + // Меняем уровень + . $this->level . '=' . + _case(_between($this->left, $leftId, $rightId), + $this->level . sprintf('%+d', -($level-1) +$levelP) , $this->level) + // Меняем границы + . $this->left . '=' . + _case(_beetween($this->left, $rightId+1, $rightIdP-1), $this->left . '-' . $rightId-$leftId+1 , + _case(_between($this->left, $leftId, $rightId), $this->left . '+' . ((($rightIdP-$rightId-$level+$levelP) /2) *2+$level-$levelP-1) , $this->left)) + . $this->right . '=' . + _case(_between($this->right, $rightId+1, $rightIdP-1), $this->right . '-' . ($rightId-$leftId+1) , + _case(_between($this->left, $leftId, $rightId), $this->right . '+' . ((($rightIdP-$rightId-$level+$levelP) /2) *2+$level-$levelP-1) , $this->right)) + . 'WHERE ' . _between($this->left, ($leftIdP+1), ($rightIdP-1)); + + } elseif ($leftIdP < $leftId) { + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->level . '=' . + _case(_between($this->left, $leftId, $rightId), + $this->level . sprintf('%+d', -($level-1) +$levelP) , $this->level) + . $this->left . '=' . + _case(_between($this->left, $rightIdP, $leftId-1), $this->left . '+' . ($rightId-$leftId+1), + _case(_between($this->left, $leftId, $rightId), $this->left . '-' . ($leftId-$rightIdP) , $this->left)) + . $this->right . '=' . + _case(_between($this->right, $rightIdP, $leftId), $this->right . '+' . ($rightId-$leftId+1), + _case(_between($this->right, $leftId, $rightId), $this->right . '-' . ($leftId-$rightIdP) , $this->right)) + . 'WHERE ' . _between($this->left, $leftIdP, $rightId) + // !!! added this line (Maxim Matyukhin) + . ' OR ' . _between($this->right, $leftIdP, $rightId); + } else { + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->level . '=' + . _case(_between($this->left, $leftId, $rightId), + $this->level . sprintf('%+d', -($level-1) +$levelP) , $this->level) + . $this->left . '=' . + _case(_between($this->left, $rightId, $rightIdP), $this->left . '-' . ($rightId-$leftId+1), + _case(_between($this->left, $leftId, $rightId), $this->left . '+' . ($rightIdP-1-$rightId), $this->left)) + . $this->right . '=' . + _case(_between($this->right, $rightId+1, $rightIdP-1), $this->right . '-' . ($rightId-$leftId+1), + _case(_between($this->right, $leftId, $rightId), $this->right . '+' . ($rightIdP-1-$rightId) , $this->right)) + . 'WHERE ' . _between($this->left, $leftId, $rightIdP) + // !!! added this line (Maxim Matyukhin) + . ' OR ' . _between($this->right, $leftId, $rightIdP); + } + return $this->db->query($this->sql) or trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + + } + + //************************************************************************ + // Deletes a record wihtout deleting its children + // $ID : an ID of the element to be deleted + // Returns : true on success, or false on error + function delete($ID) + { + + if (!(list($leftId, $rightId, $level) = $this->getNodeInfo($ID))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // Deleting record + $this->sql = 'DELETE FROM ' . $this->table . ' WHERE ' . $this->id . '=\'' . $ID . '\''; + + if (!$this->db->query($this->sql)) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // Clearing blank spaces in a tree + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->left . '=' . _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->left . '-1', $this->left) . ", " + . $this->right . '=' . _case($this->right . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->right . '-1', $this->right) . ", " + . $this->level . '=' . _case($this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId, $this->level . '-1', $this->level) . ", " + . $this->left . '=' . _case($this->left . '>' . $rightId, $this->left . '-2', $this->left) . ", " + . $this->right . '=' . _case($this->right . '>' . $rightId, $this->right . '-2', $this->right) + . ' WHERE ' . $this->right . '>' . $leftId; + + if (!$this->db->query($this->sql)) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + return true; + } + + //************************************************************************ + // Deletes a record with all its children + // $ID : an ID of the element to be deleted + // Returns : true on success, or false on error + function deleteAll($ID) + { + + if (!(list($leftId, $rightId, $level) = $this->getNodeInfo($ID))) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // Deleteing record(s) + $this->sql = 'DELETE FROM ' . $this->table . ' WHERE ' . $this->left . ' BETWEEN ' . $leftId . ' AND ' . $rightId; + + if (!$this->db->query($this->sql)) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // Clearing blank spaces in a tree + $deltaId = ($rightId-$leftId) +1; + $this->sql = 'UPDATE ' . $this->table . ' SET ' + . $this->left . '=' . _case($this->left . '>' . $leftId, $this->left . '-' . $deltaId, $this->left) . ", " + . $this->right . '=' . _case($this->right . '>' . $leftId, $this->right . '-' . $deltaId, $this->right) + . ' WHERE ' . $this->right . '>' . $rightId; + + if (!$this->db->query($this->sql)) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + return true; + } + + //************************************************************************ + // Enumerates children of an element + // $ID : an ID of an element which children to be enumerated + // $start_level : relative level from which start to enumerate children + // $end_level : the last relative level at which enumerate children + // 1. If $end_level isn't given, only children of + // $start_level levels are enumerated + // 2. Level values should always be greater than zero. + // Level 1 means direct children of the element + // Returns : a result id for using with other DB functions + function enumChildrenAll($ID) + { + return $this->enumChildren($ID, 1, 0); + } + + function enumChildren($ID, $start_level = 1, $end_level = 1) + { + if ($start_level < 0) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + // We could use sprintf() here, but it'd be too slow + $whereSql1 = ' AND ' . $this->table . '.' . $this->level; + $whereSql2 = '_' . $this->table . '.' . $this->level . '+'; + + if (!$end_level) $whereSql = $whereSql1 . '>=' . $whereSql2 . (int)$start_level; + else { + $whereSql = ($end_level <= $start_level) ? $whereSql1 . '=' . $whereSql2 . (int)$start_level : ' AND ' . $this->table . '.' . $this->level . ' BETWEEN _' . $this->table . '.' . $this->level . '+' . (int)$start_level . ' AND _' . $this->table . '.' . $this->level . '+' . (int)$end_level; + } + $this->sql = $this->sqlComposeSelect(array( + '', // Params + '', // Fields + $this->table . ' _' . $this->table . ', ' . $this->table, // Tables + '_' . $this->table . '.' . $this->id . '=\'' . $ID . '\'' . ' AND ' . $this->table . '.' . $this->left . ' BETWEEN _' . $this->table . '.' . $this->left . ' AND _' . $this->table . '.' . $this->right . $whereSql + )); + return $this->db->query($this->sql); + } + + function enumChildrenArray($ID, $start_level = 1, $end_level = 1) + { + return $this->db->result2array($this->enumChildren($ID, $start_level, $end_level)); + } + + //************************************************************************ + // Enumerates the PATH from an element to its top level parent + // $ID : an ID of an element + // $showRoot : whether to show root node in a path + // Returns : a result id for using with other DB functions + function enumPath($ID, $showRoot = false) + { + $this->sql = $this->sqlComposeSelect(array( + '', // Params + '', // Fields + $this->table . ' _' . $this->table . ', ' . $this->table, // Tables + '_' . $this->table . '.' . $this->id . '=\'' . $ID . '\'' . ' AND _' . $this->table . '.' . $this->left . ' BETWEEN ' . $this->table . '.' . $this->left . ' AND ' . $this->table . '.' . $this->right . (($showRoot) ? '' : ' AND ' . $this->table . '.' . $this->level . '>0') , // Where + '', // GroupBy + '', // Having + $this->table . '.' . $this->left // OrderBy + + )); + return $this->db->query($this->sql); + } + + function enumPathArray($ID, $showRoot = false) + { + return $this->db->result2array($this->enumPath($ID, $showRoot)); + } + + //************************************************************************ + // Returns query result to fetch data of the element's parent + // $ID : an ID of an element which parent to be retrieved + // $level : Relative level of parent + // Returns : a result id for using with other DB functions + function getParent($ID, $level = 1) + { + + if ($level < 1) trigger_error("phpDbTree error: " . $this->db->error() , E_USER_ERROR); + $this->sql = $this->sqlComposeSelect(array( + '', // Params + '', // Fields + $this->table . ' _' . $this->table . ', ' . $this->table, // Tables + '_' . $this->table . '.' . $this->id . '=\'' . $ID . '\'' . ' AND _' . $this->table . '.' . $this->left . ' BETWEEN ' . $this->table . '.' . $this->left . ' AND ' . $this->table . '.' . $this->right . ' AND ' . $this->table . '.' . $this->level . '=_' . $this->table . '.' . $this->level . '-' . (int)$level // Where + + )); + $result = $this->db->result2array($this->db->query($this->sql)); + return (int)$result[0][$this->id]; + } + //************************************************************************ + + function sqlReset() + { + $this->qryParams = ''; + $this->qryFields = ''; + $this->qryTables = ''; + $this->qryWhere = ''; + $this->qryGroupBy = ''; + $this->qryHaving = ''; + $this->qryOrderBy = ''; + $this->qryLimit = ''; + return true; + } + + //************************************************************************ + function sqlSetReset($resetMode) + { + $this->sqlNeedReset = ($resetMode) ? true : false; + } + + //************************************************************************ + function sqlParams($param = '') + { + return (empty($param)) ? $this->qryParams : $this->qryParams = $param; + } + + function sqlFields($param = '') + { + return (empty($param)) ? $this->qryFields : $this->qryFields = $param; + } + + function sqlSelect($param = '') + { + return $this->sqlFields($param); + } + + function sqlTables($param = '') + { + return (empty($param)) ? $this->qryTables : $this->qryTables = $param; + } + + function sqlFrom($param = '') + { + return $this->sqlTables($param); + } + + function sqlWhere($param = '') + { + return (empty($param)) ? $this->qryWhere : $this->qryWhere = $param; + } + + function sqlGroupBy($param = '') + { + return (empty($param)) ? $this->qryGroupBy : $this->qryGroupBy = $param; + } + + function sqlHaving($param = '') + { + return (empty($param)) ? $this->qryHaving : $this->qryHaving = $param; + } + + function sqlOrderBy($param = '') + { + return (empty($param)) ? $this->qryOrderBy : $this->qryOrderBy = $param; + } + + function sqlLimit($param = '') + { + return (empty($param)) ? $this->qryLimit : $this->qryLimit = $param; + } + + //************************************************************************ + function sqlComposeSelect($arSql) + { + $joinTypes = array( + 'join' => 1, + 'cross' => 1, + 'inner' => 1, + 'straight' => 1, + 'left' => 1, + 'natural' => 1, + 'right' => 1 + ); + $this->sql = 'SELECT ' . $arSql[0] . ' '; + + if (!empty($this->qryParams)) $this->sql.= $this->sqlParams . ' '; + + if (empty($arSql[1]) && empty($this->qryFields)) $this->sql.= $this->table . '.' . $this->id; + else { + + if (!empty($arSql[1])) $this->sql.= $arSql[1]; + + if (!empty($this->qryFields)) $this->sql.= ((empty($arSql[1])) ? '' : ',') . $this->qryFields; + } + $this->sql.= ' FROM '; +// $tblAr = array(0 => 'join'); + $isJoin = ($tblAr = explode(' ', trim($this->qryTables))) + && /*($joinTypes[strtolower($tblAr[0]) ])*/ 1; + + if (empty($arSql[2]) && empty($this->qryTables)) $this->sql.= $this->table; + else { + + if (!empty($arSql[2])) $this->sql.= $arSql[2]; + + if (!empty($this->qryTables)) { + + if (!empty($arSql[2])) $this->sql.= (($isJoin) ? ' ' : ','); + elseif ($isJoin) $this->sql.= $this->table . ' '; + $this->sql.= $this->qryTables; + } + } + + if ((!empty($arSql[3])) || (!empty($this->qryWhere))) { + $this->sql.= ' WHERE ' . $arSql[3] . ' '; + + if (!empty($this->qryWhere)) $this->sql.= (empty($arSql[3])) ? $this->qryWhere : 'AND(' . $this->qryWhere . ')'; + } + + if ((!empty($arSql[4])) || (!empty($this->qryGroupBy))) { + $this->sql.= ' GROUP BY ' . $arSql[4] . ' '; + + if (!empty($this->qryGroupBy)) $this->sql.= (empty($arSql[4])) ? $this->qryGroupBy : ',' . $this->qryGroupBy; + } + + if ((!empty($arSql[5])) || (!empty($this->qryHaving))) { + $this->sql.= ' HAVING ' . $arSql[5] . ' '; + + if (!empty($this->qryHaving)) $this->sql.= (empty($arSql[5])) ? $this->qryHaving : 'AND(' . $this->qryHaving . ')'; + } + + if ((!empty($arSql[6])) || (!empty($this->qryOrderBy))) { + $this->sql.= ' ORDER BY ' . (isset($arSql[6]) ? $arSql[6] : '') . ' '; + + if (!empty($this->qryOrderBy)) $this->sql.= (empty($arSql[6])) ? $this->qryOrderBy : ',' . $this->qryOrderBy; + } + + if (!empty($arSql[7])) $this->sql.= ' LIMIT ' . $arSql[7]; + elseif (!empty($this->qryLimit)) $this->sql.= ' LIMIT ' . $this->qryLimit; + + if ($this->sqlNeedReset) $this->sqlReset(); + return $this->sql; + } + //************************************************************************ + +} diff --git a/src/Database.php b/src/Database.php new file mode 100644 index 0000000..9c29f18 --- /dev/null +++ b/src/Database.php @@ -0,0 +1,135 @@ + +// Category: Databases +//**************************************************************************** +// The lib is FREEWARE. This means you may use it anywhere you want, you may +// do anything with it. The Author mentioned above is NOT responsible for any +// consequences of using this library. +// If you don't agree with this, you MAY NOT use the lib! +//**************************************************************************** +// All improvings, feature requests, bug reports, etc. are gladly accepted. +//**************************************************************************** +// Note: For best viewing of the code Tab size 4 is recommended +//**************************************************************************** + +class Tree_Database +{ + var $link; + var $db; + var $host, $user, $pass; + + function CDatabase($db, $host = "localhost", $user = "", $pass = "") + { + $this->db = $db; + $this->host = $host; + $this->user = $user; + $this->pass = $pass; + $str = "host={$host} port=5432 dbname={$db} user={$user} password={$pass}"; + $this->link = pg_connect($str); + } + + function query($sql) + { + if (!$this->link) return 0; + return pg_query($this->link, $sql); + } + + function affected_rows() + { + return pg_affected_rows($this->link); + } + + function num_rows($q) + { + return pg_num_rows($q); + } + + function fetch_array($q) // fetchAll + { + return pg_fetch_array($q, NULL); + } + + function fetch_object($q) // fetchObject + { + return pg_fetch_object($q); + } + /* function data_seek($q, $n) { + return pg_data_seek($q, $n); + } + */ + + function free_result($q) + { + return pg_free_result($q); + } + + function insert_id($seq) + { + $query = "SELECT currval('$seq')"; + $res = $this->query($query); + $row = pg_fetch_array($res, NULL, PGSQL_ASSOC); + pg_free_result($res); + return ($row) ? $row['currval'] : 0; + } + + function error() + { + return pg_last_error($this->link); + } + + function error_die($msg = '') + { + die(((empty($msg)) ? '' : $msg . ': ') . $this->error()); + } + + function sql2var($sql) + { + + if ((empty($sql)) || (!($query = $this->query($sql)))) return false; + + if ($this->num_rows($query) < 1) return false; + return $this->result2var($query); + } + + function result2var($q) + { + + if (!($Data = $this->fetch_array($q))) return false; + $this->free_result($q); + + foreach($Data as $k => $v) $GLOBALS[$k] = $v; + return true; + } + + /*function sql2array($sql, $keyField = '') + { + + if ((empty($sql)) || (!($query = $this->query($sql)))) return false; + + if ($this->num_rows($query) < 1) return false; + return $this->result2array($query, $keyField); + }*/ + + function result2array($q, $keyField = '') + { + $Result = array(); + while ($Data = $this->fetch_array($q)) + if (empty($keyField)) $Result[] = $Data; + else $Result[$Data[$keyField]] = $Data; + $this->free_result($q); + return $Result; + } + + /*function list_tables() + { + return mysql_list_tables($this->db, $this->link); + } + + function list_fields($table_name) + { + return mysql_list_fields($this->db, $table_name, $this->link); + }*/ +}; diff --git a/src/Sort.php b/src/Sort.php new file mode 100644 index 0000000..540f936 --- /dev/null +++ b/src/Sort.php @@ -0,0 +1,79 @@ +sortBy($data, 'name'); + */ +class Tree_Sort { + private $data = array(); + private $result = array(); + private $sortBy = ''; + + public function __construct() { + } + + // Преобразуем Nested Set в дерево и сортируем + private function listTree(array $tree, $offset, $level) { + $result = array(); + for ($i = $offset; $i < sizeof($tree); $i++) { + $leaf = $tree[$i]; + $clevel = $leaf['cat_level']; + if ($clevel == $level) { + $result [] = array($i); + } else if ($clevel > $level) { + list($subtree, $i) = $this->listTree($tree, $i, $clevel); + $i--; + $result[sizeof($result) - 1][1] = $subtree; + } else { + $this->sortList($result, $tree); + return array($result, $i); + } + } + $this->sortList($result, $tree); + return array($result, $i); + } + + // Сравнение двух элементов + private function compare($a, $b) { + $a1 = $this->data[$a[0]][$this->sortBy]; + $b1 = $this->data[$b[0]][$this->sortBy]; + return strcmp($a1, $b1); + } + + // Сортировка списка + private function sortList(array &$list, $data) { + usort($list, array($this, 'compare')); + } + + // Создает дерево в виде списка + private function reorder(array $tree) { + foreach($tree as $node) { + $this->result[] = $this->data[$node[0]]; + if (isset($node[1])) { + $this->reorder($node[1]); + } + } + } + + public function sortBy0(array $data, $sortBy) + { + $this->data = $data; + $this->sortBy = $sortBy; + $order = $this->listTree($data, 0, 0); + return $order[0]; + } + + // Сортировка по заданному полю + public function sortBy(array $data, $sortBy) { + $this->data = $data; + $this->sortBy = $sortBy; + $order = $this->listTree($data, 0, 0); + $this->reorder($order[0]); + return $this->result; + } +}