From 0d39c58830d8433142f91fbb308f3c588fb62e81 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Fri, 2 Jul 2010 08:56:59 +0200 Subject: [PATCH] introduced nested set version of DatabaseObject --- backend/lib/model/group.php | 38 ++-- backend/lib/model/nesteddatabaseobject.php | 223 ++++++++++++++------- backend/lib/view/jsonview.php | 31 ++- backend/lib/view/xmlview.php | 2 +- 4 files changed, 198 insertions(+), 96 deletions(-) diff --git a/backend/lib/model/group.php b/backend/lib/model/group.php index 7ae4cc4..23e67bf 100644 --- a/backend/lib/model/group.php +++ b/backend/lib/model/group.php @@ -43,27 +43,31 @@ class Group extends NestedDatabaseObject { return Channel::getByFilter(array('group.id' => $groups)); } - static protected function buildFilterQuery($filters, $conjunction, $columns = array('id')) { // TODO rework for nested sets - $sql = 'SELECT ' . self::table . '.* FROM ' . self::table; - - // join users - if (preg_match('/^user\.([a-z_]+)$/', $filters)) { - $sql .= ' LEFT JOIN users_in_groups ON users_in_groups.group_id = ' . self::table . '.id'; - $sql .= ' LEFT JOIN users ON users.id = users_in_groups.user_id'; + /* + * data filtering + */ + static public function getByFilter($filters = array(), $conjunction = true) { + $joins = array(); + foreach ($filters as $column => $value) { + if (!key_exists('users', $joins) && preg_match('/^user\.([a-z_]+)$/', $column)) { + $joins['users_in_groups'] = array('type' => 'left', 'table' => 'users_in_groups', 'condition' => 'users_in_groups.group_id = ' . self::table . '.id'); + $joins['users'] = array('type' => 'left', 'table' => 'users AS user', 'condition' => 'user.id = users_in_groups.user_id'); + } - $filters = preg_replace('/^user\.([a-z_]+)$/', 'users.$1', $filters); + if (!key_exists('channels', $joins) && preg_match('/^channel\.([a-z_]+)$/', $column)) { + $joins['channels_in_groups'] = array('type' => 'left', 'table' => 'channels_in_groups', 'condition' => 'channels_in_groups.group_id = ' . self::table . '.id'); + $joins['channels'] = array('type' => 'left', 'table' => 'channels AS channel', 'condition' => 'channels.id = channels_in_groups.channel_id'); + } } - // join channels // TODO preg_filter or preg_grep? - if (preg_match('/^channel\.([a-z_]+)$/', $filters)) { - $sql .= ' LEFT JOIN channels_in_groups ON channels_in_groups.group_id = ' . self::table . '.id'; - $sql .= ' LEFT JOIN channels ON channels.id = channels_in_groups.channel_id'; - - $filters = preg_replace('/^channel\.([a-z_]+)$/', 'channels.$1', $filters); + $result = Database::getConnection()->select(self::table, array(self::table . '.*'), $filters, $conjunction, $joins); + + $instances = array(); + foreach ($result as $object) { + $instances[$object['id']] = static::factory($object); } - - $sql .= static::buildFilterCondition($filters, $conjunction); - return $sql; + + return $instances; } } diff --git a/backend/lib/model/nesteddatabaseobject.php b/backend/lib/model/nesteddatabaseobject.php index 8e922a2..b5da971 100644 --- a/backend/lib/model/nesteddatabaseobject.php +++ b/backend/lib/model/nesteddatabaseobject.php @@ -21,95 +21,168 @@ /* * DatabaseObject which is structured by nested sets - * @url // TODO add url + * TODO use database transactions */ abstract class NestedDatabaseObject extends DatabaseObject { - public function __set($key, $value) { - if ($key == 'left' || $key == 'right') { - throw new NestedDatabaseException('nested set fields are read only! please make use of move() or delete() instead'); - } - - parent::__set($key, $value); - } - - public function addChild(NestedDatabaseObject $child) { - if (isset($child->id)) { - throw new NestedDatabaseException('Object is already part of the tree'); - } - - // TODO start transaction - $this->dbh->execute('UPDATE ' . static::table . ' SET left = left + 2 WHERE left > ' . $this->right); - $this->dbh->execute('UPDATE ' . static::table . ' SET right = right + 2 WHERE right >= ' . $this->right); - - $child->left = $this->right; - $child->right = $this->right + 1; - - $this->right += 2; - - $child->insert(); - } - - public function getChilds() { - $sql = 'SELECT * FROM ' . static::table . ' WHERE left > ' . $this->left . ' && left < ' . $this->right; - $result = $this->dbh->query($sql); - - $objs = array(); - foreach ($result as $obj) { - $objs[$obj['id']] = static::factory($obj); - } - - return $groups; - } - + public $level; // shouldn't be altered! use move or delete instead! + public $children; + /* - * deletes subset under $this + * inserts or updates a tree to the database */ - public function delete() { - $move = floor(($this->right - $this->left) / 2); - $move = 2 * (1 + $move); - - // TODO start transaction - - // delete nodes - $result = $this->dbh->query('SELECT * FROM ' . static::table . ' WHERE left >= ' . $this->left . ' && left <= ' . $this->right); - foreach ($result as $obj) { - $obj->delete(); // TODO optimize (all in one query) + public function save(NestedDatabaseObject $parent = NULL) { + if (!is_null($parent) && !isset($parent->id)) { // checks if $parent is part of the tree + throw new InvalidArgumentException('Parent node has to be part of the tree'); } - // move remaining nodes ... - $this->dbh->execute('UPDATE ' . static::table . ' SET left = left - ' . $move . ' WHERE left > ' . $this->right); - $this->dbh->execute('UPDATE ' . static::table . ' SET right = right - ' . $move . ' WHERE right > ' . $this->right); + if (isset($this->id)) { + $this->update($parent); + } + else { + $this->insert($parent); + } } /* - * checks if $child is a child of $this - * @return bool + * updates tree in database (optionally move it) */ - public function contains(NestedDatabaseObject $child) { // TODO untested - $sql = 'SELECT * FROM ' . static::table . ' WHERE left > ' . $this->left . ' && left < ' . $this->right . ' && id = ' . $child->id; - $result = $this->dbh->query($sql); + protected function update(NestedDatabaseObject $parent = NULL) { + // TODO move it if parent is given + parent::update(); + } + + /* + * inserts tree to database + */ + protected function insert(NestedDatabaseObject $parent = NULL) { + if (is_null($parent)) { + throw new InvalidArgumentException('We need a parent for a new child'); + } - return ($result->count() > 0) ? true : false; + $this->dbh->execute('UPDATE ' . static::table . ' SET lft = lft + 2 WHERE lft > ' . $parent->rgt); + $this->dbh->execute('UPDATE ' . static::table . ' SET rgt = rgt + 2 WHERE rgt >= ' . $parent->rgt); + + // update singleton instances + foreach (self::$instances[static::table] as $instance) { + if ($instance->lft > $parent->rgt) { + $instance->lft = $instance->lft + 2; + } + + if ($instance->rgt >= $parent->rgt) { + $instance->rgt = $instance->rgt + 2; + } + } + + $this->lft = $parent->rgt - 2; + $this->rgt = $parent->rgt - 1; + + parent::insert(); + } + + /*public function move(NestedDatabaseObject $parent) { // TODO finish + $move = $this->rgt - $this->lft + 1; + + // exclude the tree which we want to move by turning the sign of left and rigth columns + $sql = 'UPDATE ' . static::table . ' SET lft = lft * -1, rgt = rgt * -1 WHERE lft > ' . $this->lft . ' && rgt < ' . $this->rgt; + + // close hole + $sql = 'UPDATE ' . static::table . ' SET lft = lft - x, rgt = rgt - x WHERE lft > ' . $this->lft; + + // open new hole + + // include the tree which we want to move by turning the sign of left and rigth columns and adding an offset + + // TODO update singletons + }*/ + + /* + * query database for all descending children under this node + */ + public function getChildren() { + $sql = 'SELECT + o.*, + CAST(((o.rgt - o.lft - 1) / 2) AS UNSIGNED) AS children, + COUNT(p.id) AS level + FROM + ' . static::table . ' AS n, + ' . static::table . ' AS p, + ' . static::table . ' AS o + WHERE + o.lft > p.lft && o.rgt < p.rgt + && o.lft > n.lft && o.rgt < n.rgt + && n.id = ' . $this->id . ' + GROUP BY + o.lft + ORDER BY + o.lft'; + + $result = $this->dbh->query($sql); + + $children = array(); + foreach ($result as $row) { + $child = static::factory(array_diff_key($row, array_fill_keys(array('children', 'level'), NULL))); + $child->children = $row['children']; + $child->level = $row['level']; + + $children[$row['id']] = $child; + } + + return $children; } - public function moveTo(NestedDatabaseObject $destination) { // TODO implement - // $this->getChilds - $obj = $this->getChilds(); - foreach ($objs as $obj) { - $obj->right += $destination->left - $this->left; - $obj->left = $destination->left; + static public function getRoot() { + return current(static::getByFilter(array('lft' => 0))); + } + + /* + * delete the node including all descending children from the database + */ + public function delete() { + $move = $this->rgt - $this->lft + 1; + + // delete children + $result = $this->dbh->execute('DELETE FROM ' . static::table . ' WHERE lft >= ' . $this->lft . ' && lft <= ' . $this->rgt); + + // move remaining children ... + $this->dbh->execute('UPDATE ' . static::table . ' SET lft = lft - ' . $move . ' WHERE lft > ' . $this->rgt); + $this->dbh->execute('UPDATE ' . static::table . ' SET rgt = rgt - ' . $move . ' WHERE rgt > ' . $this->rgt); + + // update singleton instances + foreach (self::$instances[static::table] as $instance) { + if ($instance->lft >= $this->lft && $instance->rgt <= $this->rgt) { + $instance->unlink(); + } + else { + if ($instance->lft > $this->rgt) { + $instance->lft = $instance->lft - $move; + } + + if ($instance->rgt > $this->rgt) { + $instance->rgt = $instance->rgt - $move; + } + } } - - // close whole - $move = floor(($this->right - $this->left) / 2); - $move = 2 * (1 + $move); - - $this->dbh->execute('UPDATE ' . static::table . ' SET left = left - ' . $move . ' WHERE left > ' . $this->right); - $this->dbh->execute('UPDATE ' . static::table . ' SET right = right - ' . $move . ' WHERE right > ' . $this->right); - - // create hole - $this->dbh->execute('UPDATE ' . static::table . ' SET left = left + ' . $move . ' WHERE left > ' . $this->right); - $this->dbh->execute('UPDATE ' . static::table . ' SET right = right + ' . $move . ' WHERE right >= ' . $this->right); + } + + /* + * unlinks instance from database + */ + protected function unlink() { + parent::unlink(); + + unset($this->data['lft']); + unset($this->data['rgt']); + + unset($this->level); + unset($this->children); + } + + /* + * checks if $child is a descendant of $this + * @return bool + */ + public function contains(NestedDatabaseObject $child) { + return ($child->lft > $this->lft && $child->rgt < $this->rgt); } } diff --git a/backend/lib/view/jsonview.php b/backend/lib/view/jsonview.php index 2b9427a..086ad01 100644 --- a/backend/lib/view/jsonview.php +++ b/backend/lib/view/jsonview.php @@ -54,7 +54,8 @@ class JsonView extends View { $channel['id'] = (int) $obj->id; $channel['uuid'] = $obj->uuid; $channel['type'] = $obj->type; - $channel['unit'] = $obj->unit; + $channel['unit'] = $obj::unit; + $channel['name'] = $obj->name; $channel['description'] = $obj->description; $channel['resolution'] = (int) $obj->resolution; $channel['cost'] = (float) $obj->cost; @@ -76,12 +77,36 @@ class JsonView extends View { $this->jsonData['users'][] = $user; } - public function addGroup(Group $obj) { + public function addGroup(Group $obj, $recursive = false) { // TODO fix this. how do we want to handly nested set structures? $group['id'] = (int) $obj->id; $group['uuid'] = $obj->uuid; + $group['name'] = $obj->name; $group['description'] = $obj->description; + + $backtrace = array(&$group); - // TODO include sub groups? + if ($recursive) { + $children = $obj->getChildren(); + + foreach ($children as $child) { + $subGroup['id'] = (int) $child->id; + $subGroup['uuid'] = $child->uuid; + $subGroup['name'] = $child->name; + $subGroup['description'] = $child->description; + + if ($child->level > $lastLevel) { + array_push(end($backtrace), $subGroup); + // array_push($backtrace, &$subgroup); // TODO: Deprecated: Call-time pass-by-reference has been deprecated + } + elseif ($child->level < $lastLevel) { + array_pop($backtrace); + array_push(end($backtrace), $subGroup); + } + elseif ($child->level == $lastLevel) { + array_push(end($backtrace), $subGroup); + } + } + } $this->jsonData['groups'][] = $group; } diff --git a/backend/lib/view/xmlview.php b/backend/lib/view/xmlview.php index 7aefac6..9a1b4d6 100644 --- a/backend/lib/view/xmlview.php +++ b/backend/lib/view/xmlview.php @@ -53,7 +53,7 @@ class XmlView extends View { $xmlChannel->appendChild($this->xmlDoc->createElement('uuid', $obj->uuid)); $xmlChannel->appendChild($this->xmlDoc->createElement('type', $obj->type)); - $xmlChannel->appendChild($this->xmlDoc->createElement('unit', $obj->unit)); + $xmlChannel->appendChild($this->xmlDoc->createElement('unit', $obj::unit)); $xmlChannel->appendChild($this->xmlDoc->createElement('description', $obj->description)); $xmlChannel->appendChild($this->xmlDoc->createElement('resolution', (int) $obj->resolution)); $xmlChannel->appendChild($this->xmlDoc->createElement('cost', (float) $obj->cost));