diff --git a/src/Storage/DbalNestedSet.php b/src/Storage/DbalNestedSet.php index 78159f9b..d19c9eeb 100644 --- a/src/Storage/DbalNestedSet.php +++ b/src/Storage/DbalNestedSet.php @@ -12,6 +12,11 @@ */ class DbalNestedSet implements NestedSetInterface { + /** + * The regex for validating table names. + */ + const VALID_TABLE_REGEX = '/^[a-zA-Z]\w{1,64}$/'; + /** * The database connection. * @@ -19,21 +24,34 @@ class DbalNestedSet implements NestedSetInterface { */ protected $connection; + /** + * The table name to use for storing the nested set. + * + * @var string + */ + protected $tableName; + /** * DbalTree constructor. * * @param \Doctrine\DBAL\Connection $connection * The database connection. + * @param string $tableName + * (optional) The table name to use. */ - public function __construct(Connection $connection) { + public function __construct(Connection $connection, $tableName = 'tree') { $this->connection = $connection; + if (!preg_match(self::VALID_TABLE_REGEX, $tableName)) { + throw new \InvalidArgumentException("Table name must match the regex " . self::VALID_TABLE_REGEX); + } + $this->tableName = $tableName; } /** * {@inheritdoc} */ public function addRootNode(Node $node) { - $maxRight = $this->connection->fetchColumn('SELECT MAX(nested_right) FROM tree'); + $maxRight = $this->connection->fetchColumn('SELECT MAX(right_pos) FROM ' . $this->tableName); if ($maxRight === FALSE) { $maxRight = 0; } @@ -89,10 +107,10 @@ protected function insertNodeAtPostion($newLeftPosition, $depth, Node $node) { $this->connection->beginTransaction(); // Make space for inserting node. - $this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right + 2 WHERE nested_right >= ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET right_pos = right_pos + 2 WHERE right_pos >= ?', [$newLeftPosition] ); - $this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left + 2 WHERE nested_left >= ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET left_pos = left_pos + 2 WHERE left_pos >= ?', [$newLeftPosition] ); @@ -131,11 +149,11 @@ protected function doInsertNode($id, $revisionId, $left, $right, $depth) { $newNode = new Node($id, $revisionId, $left, $right, $depth); // Insert the new node. - $this->connection->insert('tree', [ + $this->connection->insert($this->tableName, [ 'id' => $newNode->getId(), 'revision_id' => $newNode->getRevisionId(), - 'nested_left' => $newNode->getLeft(), - 'nested_right' => $newNode->getRight(), + 'left_pos' => $newNode->getLeft(), + 'right_pos' => $newNode->getRight(), 'depth' => $newNode->getDepth(), ]); @@ -148,11 +166,11 @@ protected function doInsertNode($id, $revisionId, $left, $right, $depth) { public function findDescendants(Node $node, $depth = 0) { $descendants = []; $query = $this->connection->createQueryBuilder(); - $query->select('child.id', 'child.revision_id', 'child.nested_left', 'child.nested_right', 'child.depth') - ->from('tree', 'child') - ->from('tree', 'parent') - ->where('child.nested_left > parent.nested_left') - ->andWhere('child.nested_right < parent.nested_right') + $query->select('child.id', 'child.revision_id', 'child.left_pos', 'child.right_pos', 'child.depth') + ->from($this->tableName, 'child') + ->from($this->tableName, 'parent') + ->where('child.left_pos > parent.left_pos') + ->andWhere('child.right_pos < parent.right_pos') ->andWhere('parent.id = :id') ->andWhere('parent.revision_id = :revision_id') ->setParameter(':id', $node->getId()) @@ -163,7 +181,7 @@ public function findDescendants(Node $node, $depth = 0) { } $stmt = $query->execute(); while ($row = $stmt->fetch()) { - $descendants[] = new Node($row['id'], $row['revision_id'], $row['nested_left'], $row['nested_right'], $row['depth']); + $descendants[] = new Node($row['id'], $row['revision_id'], $row['left_pos'], $row['right_pos'], $row['depth']); } return $descendants; } @@ -180,11 +198,11 @@ public function findChildren(Node $node) { * {@inheritdoc} */ public function getNode($id, $revision_id) { - $result = $this->connection->fetchAssoc("SELECT id, revision_id, nested_left, nested_right, depth FROM tree WHERE id = ? AND revision_id = ?", + $result = $this->connection->fetchAssoc("SELECT id, revision_id, left_pos, right_pos, depth FROM " . $this->tableName . " WHERE id = ? AND revision_id = ?", [$id, $revision_id] ); if ($result) { - return new Node($id, $revision_id, $result['nested_left'], $result['nested_right'], $result['depth']); + return new Node($id, $revision_id, $result['left_pos'], $result['right_pos'], $result['depth']); } } @@ -193,11 +211,11 @@ public function getNode($id, $revision_id) { */ public function findAncestors(Node $node) { $ancestors = []; - $stmt = $this->connection->executeQuery('SELECT parent.id, parent.revision_id, parent.nested_left, parent.nested_right, parent.depth FROM tree AS child, tree AS parent WHERE child.nested_left BETWEEN parent.nested_left AND parent.nested_right AND child.id = ? AND child.revision_id = ? ORDER BY parent.nested_left', + $stmt = $this->connection->executeQuery('SELECT parent.id, parent.revision_id, parent.left_pos, parent.right_pos, parent.depth FROM ' . $this->tableName . ' AS child, ' . $this->tableName . ' AS parent WHERE child.left_pos BETWEEN parent.left_pos AND parent.right_pos AND child.id = ? AND child.revision_id = ? ORDER BY parent.left_pos', [$node->getId(), $node->getRevisionId()] ); while ($row = $stmt->fetch()) { - $ancestors[] = new Node($row['id'], $row['revision_id'], $row['nested_left'], $row['nested_right'], $row['depth']); + $ancestors[] = new Node($row['id'], $row['revision_id'], $row['left_pos'], $row['right_pos'], $row['depth']); } return $ancestors; } @@ -236,9 +254,9 @@ public function findParent(Node $node) { */ public function getTree() { $tree = []; - $stmt = $this->connection->executeQuery('SELECT id, revision_id, nested_left, nested_right, depth FROM tree ORDER BY nested_left'); + $stmt = $this->connection->executeQuery('SELECT id, revision_id, left_pos, right_pos, depth FROM ' . $this->tableName . ' ORDER BY left_pos'); while ($row = $stmt->fetch()) { - $tree[] = new Node($row['id'], $row['revision_id'], $row['nested_left'], $row['nested_right'], $row['depth']); + $tree[] = new Node($row['id'], $row['revision_id'], $row['left_pos'], $row['right_pos'], $row['depth']); } return $tree; } @@ -256,20 +274,20 @@ public function deleteNode(Node $node) { $this->connection->beginTransaction(); // Delete the node. - $this->connection->executeUpdate('DELETE FROM tree WHERE nested_left = ?', + $this->connection->executeUpdate('DELETE FROM ' . $this->tableName . ' WHERE left_pos = ?', [$left] ); // Move children up a level. - $this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right - 1, nested_left = nested_left - 1, depth = depth -1 WHERE nested_left BETWEEN ? AND ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET right_pos = right_pos - 1, left_pos = left_pos - 1, depth = depth -1 WHERE left_pos BETWEEN ? AND ?', [$left, $right] ); // Move everything back two places. - $this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right - 2 WHERE nested_right > ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET right_pos = right_pos - 2 WHERE right_pos > ?', [$right] ); - $this->connection->executeUpdate('UPDATE tree SET nested_left = tree.nested_left - 2 WHERE nested_left > ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET left_pos = left_pos - 2 WHERE left_pos > ?', [$right] ); @@ -298,15 +316,15 @@ public function deleteSubTree(Node $node) { $this->connection->beginTransaction(); // Delete the node. - $this->connection->executeUpdate('DELETE FROM tree WHERE nested_left BETWEEN ? AND ?', + $this->connection->executeUpdate('DELETE FROM ' . $this->tableName . ' WHERE left_pos BETWEEN ? AND ?', [$left, $right] ); // Move everything back two places. - $this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right - ? WHERE nested_right > ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET right_pos = right_pos - ? WHERE right_pos > ?', [$width, $right] ); - $this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left - ? WHERE nested_left > ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET left_pos = left_pos - ? WHERE left_pos > ?', [$width, $right] ); @@ -374,25 +392,25 @@ protected function moveSubTreeToPosition($newLeftPosition, Node $node) { } // Create new space for subtree. - $this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left + ? WHERE nested_left >= ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET left_pos = left_pos + ? WHERE left_pos >= ?', [$width, $newLeftPosition] ); - $this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right + ? WHERE nested_right >= ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET right_pos = right_pos + ? WHERE right_pos >= ?', [$width, $newLeftPosition] ); // Move subtree into new space. - $this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left + ?, nested_right = nested_right + ?, depth = depth + ? WHERE nested_left >= ? AND nested_right < ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET left_pos = left_pos + ?, right_pos = right_pos + ?, depth = depth + ? WHERE left_pos >= ? AND right_pos < ?', [$distance, $distance, $depthDiff, $tempPos, $tempPos + $width] ); // Remove old space vacated by subtree. - $this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left - ? WHERE nested_left > ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET left_pos = left_pos - ? WHERE left_pos > ?', [$width, $node->getRight()] ); - $this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right - ? WHERE nested_right > ?', + $this->connection->executeUpdate('UPDATE ' . $this->tableName . ' SET right_pos = right_pos - ? WHERE right_pos > ?', [$width, $node->getRight()] ); } @@ -407,11 +425,11 @@ protected function moveSubTreeToPosition($newLeftPosition, Node $node) { * {@inheritdoc} */ public function getNodeAtPosition($left) { - $result = $this->connection->fetchAssoc("SELECT id, revision_id, nested_left, nested_right, depth FROM tree WHERE nested_left = ?", + $result = $this->connection->fetchAssoc("SELECT id, revision_id, left_pos, right_pos, depth FROM " . $this->tableName . " WHERE left_pos = ?", [$left] ); if ($result) { - return new Node($result['id'], $result['revision_id'], $result['nested_left'], $result['nested_right'], $result['depth']); + return new Node($result['id'], $result['revision_id'], $result['left_pos'], $result['right_pos'], $result['depth']); } } diff --git a/tests/Fixtures/schema.php b/tests/Fixtures/schema.php deleted file mode 100644 index ffe78ed1..00000000 --- a/tests/Fixtures/schema.php +++ /dev/null @@ -1,30 +0,0 @@ - 'sqlite:///tree.sqlite', -); -$conn = DriverManager::getConnection($connectionParams, $config); - -$sm = $conn->getSchemaManager(); -$schema = $sm->createSchema(); - -$tree = $schema->createTable("tree"); -$tree->addColumn("id", "integer", ["unsigned" => TRUE]); -$tree->addColumn("revision_id", "integer", ["unsigned" => TRUE]); -$tree->addColumn("nested_left", "integer", ["unsigned" => TRUE]); -$tree->addColumn("nested_right", "integer", ["unsigned" => TRUE]); - -foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) { - $this->connection->exec($sql); -} diff --git a/tests/Fixtures/schema.sql b/tests/Fixtures/schema.sql deleted file mode 100644 index 31a2d6a1..00000000 --- a/tests/Fixtures/schema.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE tree -( - id INTEGER NOT NULL, - revision_id INTEGER NOT NULL, - nested_left INTEGER NOT NULL, - nested_right INTEGER NOT NULL -); diff --git a/tests/Fixtures/test_data.sql b/tests/Fixtures/test_data.sql deleted file mode 100644 index dc1ca485..00000000 --- a/tests/Fixtures/test_data.sql +++ /dev/null @@ -1,11 +0,0 @@ -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (1, 1, 22, 1); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (2, 1, 9, 2); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (3, 1, 21, 10); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (4, 1, 8, 3); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (5, 1, 5, 4); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (6, 1, 7, 6); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (7, 1, 16, 11); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (8, 1, 18, 17); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (9, 1, 20, 19); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (10, 1, 13, 12); -INSERT INTO tree (id, revision_id, nested_right, nested_left) VALUES (11, 1, 15, 14); diff --git a/tests/Functional/DbalNestedSetTest.php b/tests/Functional/DbalNestedSetTest.php index 68fa89ed..4ab8152a 100644 --- a/tests/Functional/DbalNestedSetTest.php +++ b/tests/Functional/DbalNestedSetTest.php @@ -30,6 +30,13 @@ class DbalNestedSetTest extends \PHPUnit_Framework_TestCase { */ protected $connection; + /** + * The table name. + * + * @var string + */ + protected $tableName = 'nested_set'; + /** * {@inheritdoc} */ @@ -40,7 +47,7 @@ protected function setUp() { ], new Configuration()); $this->createTable(); $this->loadTestData(); - $this->nestedSet = new DbalNestedSet($this->connection); + $this->nestedSet = new DbalNestedSet($this->connection, $this->tableName); } } @@ -389,6 +396,33 @@ public function testAddRootNode() { $this->assertEquals(0, $newNode->getDepth()); } + /** + * Test table name validation, max length. + * + * @expectedException \InvalidArgumentException + */ + public function testValidateTableNameTooLong() { + $this->nestedSet = new DbalNestedSet($this->connection, ""); + } + + /** + * Test table name validation, invalid chars. + * + * @expectedException \InvalidArgumentException + */ + public function testValidateTableInvalidChars() { + $this->nestedSet = new DbalNestedSet($this->connection, "Robert;)DROP TABLE students;--"); + } + + /** + * Test table name validation, first char. + * + * @expectedException \InvalidArgumentException + */ + public function testValidateTableInvalidFirstChars() { + $this->nestedSet = new DbalNestedSet($this->connection, "1abc"); + } + /** * Drops the table. */ @@ -402,17 +436,14 @@ protected function dropTable() { /** * Creates the table. - * - * @param string $tableName - * The table name. */ - protected function createTable($tableName = 'tree') { + protected function createTable() { $schema = new Schema(); - $tree = $schema->createTable($tableName); + $tree = $schema->createTable($this->tableName); $tree->addColumn("id", "integer", ["unsigned" => TRUE]); $tree->addColumn("revision_id", "integer", ["unsigned" => TRUE]); - $tree->addColumn("nested_left", "integer", ["unsigned" => TRUE]); - $tree->addColumn("nested_right", "integer", ["unsigned" => TRUE]); + $tree->addColumn("left_pos", "integer", ["unsigned" => TRUE]); + $tree->addColumn("right_pos", "integer", ["unsigned" => TRUE]); $tree->addColumn("depth", "integer", ["unsigned" => TRUE]); foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) { @@ -424,92 +455,92 @@ protected function createTable($tableName = 'tree') { * Loads the test data. */ protected function loadTestData() { - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 1, 'revision_id' => 1, - 'nested_left' => 1, - 'nested_right' => 22, + 'left_pos' => 1, + 'right_pos' => 22, 'depth' => 0, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 2, 'revision_id' => 1, - 'nested_left' => 2, - 'nested_right' => 9, + 'left_pos' => 2, + 'right_pos' => 9, 'depth' => 1, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 3, 'revision_id' => 1, - 'nested_left' => 10, - 'nested_right' => 21, + 'left_pos' => 10, + 'right_pos' => 21, 'depth' => 1, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 4, 'revision_id' => 1, - 'nested_left' => 3, - 'nested_right' => 8, + 'left_pos' => 3, + 'right_pos' => 8, 'depth' => 2, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 5, 'revision_id' => 1, - 'nested_left' => 4, - 'nested_right' => 5, + 'left_pos' => 4, + 'right_pos' => 5, 'depth' => 3, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 6, 'revision_id' => 1, - 'nested_left' => 6, - 'nested_right' => 7, + 'left_pos' => 6, + 'right_pos' => 7, 'depth' => 3, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 7, 'revision_id' => 1, - 'nested_left' => 11, - 'nested_right' => 16, + 'left_pos' => 11, + 'right_pos' => 16, 'depth' => 2, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 8, 'revision_id' => 1, - 'nested_left' => 17, - 'nested_right' => 18, + 'left_pos' => 17, + 'right_pos' => 18, 'depth' => 2, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 9, 'revision_id' => 1, - 'nested_left' => 19, - 'nested_right' => 20, + 'left_pos' => 19, + 'right_pos' => 20, 'depth' => 2, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 10, 'revision_id' => 1, - 'nested_left' => 12, - 'nested_right' => 13, + 'left_pos' => 12, + 'right_pos' => 13, 'depth' => 3, ]); - $this->connection->insert('tree', + $this->connection->insert($this->tableName, [ 'id' => 11, 'revision_id' => 1, - 'nested_left' => 14, - 'nested_right' => 15, + 'left_pos' => 14, + 'right_pos' => 15, 'depth' => 3, ] );