Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed NestedSet::moveAsFirst, appendTo, prependTo #552

Merged
merged 5 commits into from
Mar 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 203 additions & 137 deletions Library/Phalcon/Mvc/Model/Behavior/NestedSet.php

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions Library/Phalcon/Mvc/Model/Behavior/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ There are two ways this behavior can work: one tree per table and multiple trees
The mode is selected based on the value of `hasManyRoots` option that is `false` by default meaning single tree mode.
In multiple trees mode you can set `rootAttribute` option to match existing field in the table storing the tree.

### Example schema

```sql
CREATE TABLE `categories` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`description` TEXT DEFAULT NULL,
`root` INT UNSIGNED DEFAULT NULL,
`lft` INT UNSIGNED NOT NULL,
`rgt` INT UNSIGNED NOT NULL,
`level` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `category_coordinates` (`lft`,`rgt`,`root`),
KEY `category_root` (`root`),
KEY `category_lft` (`lft`),
KEY `category_lft_root` (`lft`, `root`),
KEY `category_rgt` (`rgt`),
KEY `category_rgt_root` (`rgt`, `root`),
KEY `category_level` (`level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

### Selecting from a tree

In the following we'll use an example model Category with the following in its DB:
Expand Down Expand Up @@ -358,6 +380,20 @@ for ($i = $level; $i; $i--) {
}
```

Or just:

```php
$order = 'lft'; // or 'root, lft' for multiple trees
$categories = Categories::find(['order' => $order]);

$result = [];
foreach ($categories as $category) {
$result[] = str_repeat(' ', ($category->level - 1) * 5) . $category->name;
}

echo print_r($result, true), PHP_EOL;
```

## Blameable

```php
Expand Down
10 changes: 6 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
"swiftmailer/swiftmailer": "~5.2"
},
"require-dev": {
"squizlabs/php_codesniffer": "^2.5",
"codeception/codeception": "^2.1",
"codeception/mockery-module": "^0.2",
"codeception/aerospike-module": "^0.1"
"squizlabs/php_codesniffer": "~2.5",
"codeception/codeception": "~2.1",
"codeception/mockery-module": "~0.2",
"codeception/aerospike-module": "~0.1",
"codeception/specify": "~0.4",
"codeception/verify": "~0.3"
},
"suggest": {
"ext-aerospike": "*",
Expand Down
2 changes: 1 addition & 1 deletion docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ We use the following settings of these services:
+ Username: `root`
+ Password: `''` _(empty string)_
+ DB Name: `incubator_tests`
+ Charset: `urf8`
+ Charset: `utf8`

You can change the connection settings of these services **before** running tests
by using [environment variables][4]:
Expand Down
19 changes: 19 additions & 0 deletions tests/_data/dump.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
DROP TABLE IF EXISTS `categories`;
CREATE TABLE `categories` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`description` TEXT DEFAULT NULL,
`root` INT UNSIGNED DEFAULT NULL,
`lft` INT UNSIGNED NOT NULL,
`rgt` INT UNSIGNED NOT NULL,
`level` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `category_coordinates` (`lft`,`rgt`,`root`),
KEY `category_root` (`root`),
KEY `category_lft` (`lft`),
KEY `category_lft_root` (`lft`, `root`),
KEY `category_rgt` (`rgt`),
KEY `category_rgt_root` (`rgt`, `root`),
KEY `category_level` (`level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `bug`;
CREATE TABLE `bug` (
`id` serial,
Expand Down
232 changes: 232 additions & 0 deletions tests/unit/Mvc/Model/Behavior/Helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<?php

namespace Phalcon\Test\Mvc\Model\Behavior;

use Mockery;
use Phalcon\Di;
use Phalcon\Mvc\ModelInterface;
use UnitTester;
use ReflectionProperty;
use CategoriesManyRoots;
use Phalcon\DiInterface;
use Codeception\Specify;
use Phalcon\Mvc\Model\Manager;
use Codeception\TestCase\Test;
use Phalcon\Mvc\Model\Metadata;
use Phalcon\Db\Adapter\Pdo\Mysql;
use Codeception\Specify\Config as SpecifyConfig;
use Phalcon\Mvc\Model\Behavior\NestedSet as NestedSetBehavior;

/**
* \Phalcon\Test\Mvc\Model\Behavior\Helper
* Helper class for Phalcon\Test\Mvc\Model\Behavior tests
*
* @copyright (c) 2011-2016 Phalcon Team
* @link http://www.phalconphp.com
* @author Serghei Iakovlev <[email protected]>
* @package Phalcon\Test\Mvc\Model\Behavior
*
* The contents of this file are subject to the New BSD License that is
* bundled with this package in the file docs/LICENSE.txt
*
* If you did not receive a copy of the license and are unable to obtain it
* through the world-wide-web, please send an email to [email protected]
* so that we can send you a copy immediately.
*/
class Helper extends Test
{
use Specify;

/**
* UnitTester Object
* @var UnitTester
*/
protected $tester;

/**
* @var DiInterface
*/
protected $previousDependencyInjector;

/**
* executed before each test
*/
protected function _before()
{
require_once 'Stubs/Categories.php';

$this->previousDependencyInjector = Di::getDefault();

$di = new Di();

$di->setShared('modelsMetadata', new Metadata\Memory());
$di->setShared('modelsManager', new Manager());
$di->setShared('db', function () {
return new Mysql([
'host' => TEST_DB_HOST,
'port' => TEST_DB_PORT,
'username' => TEST_DB_USER,
'password' => TEST_DB_PASSWD,
'dbname' => TEST_DB_NAME,
'charset' => TEST_DB_CHARSET,
]);
});

if ($this->previousDependencyInjector instanceof DiInterface) {
Di::setDefault($di);
}

SpecifyConfig::setDeepClone(false);

$this->truncateTable(CategoriesManyRoots::$table);
}

/**
* executed after each test
*/
protected function _after()
{
if ($this->previousDependencyInjector instanceof DiInterface) {
Di::setDefault($this->previousDependencyInjector);
} else {
Di::reset();
}
}

protected function getProperty($propertyName, NestedSetBehavior $behavior)
{
$property = new ReflectionProperty(get_class($behavior), $propertyName);
$property->setAccessible(true);

return $property->getValue($behavior);
}

/**
* @return \Phalcon\Db\AdapterInterface
*/
protected function getConnection()
{
return Di::getDefault()->getShared('db');
}

/**
* @return \Pdo
*/
protected function getDbPdo()
{
return $this->getModule('Db')->dbh;
}

protected function truncateTable($table)
{
$this->getDbPdo()->query("TRUNCATE TABLE `{$table}`")->execute();
$this->getDbPdo()->query("ALTER TABLE `{$table}` AUTO_INCREMENT = 1")->execute();

$this->tester->seeNumRecords(0, $table);
}

protected function prettifyRoots($multipleTrees = true)
{
if ($multipleTrees) {
$order = 'root, lft';
} else {
$order = 'lft';
}

$categories = CategoriesManyRoots::find(['order' => $order]);

$result = [];
foreach ($categories as $category) {
$result[] = str_repeat(' ', ($category->level - 1) * 5) . $category->name;
}

return $result;
}

/**
* Checking the integrity of keys
*
* @param int|null $rootId
*/
protected function checkIntegrity($rootId = null)
{
$connection = $this->getConnection();

$sql = "SELECT COUNT(*) cnt FROM categories WHERE lft >= rgt";
if ($rootId) {
$sql .= " AND root = {$rootId}";
}

/** @var \Phalcon\Db\Result\Pdo $check1 */
$check1 = $connection->query($sql);
$this->assertEquals(['cnt' => '0'], $check1->fetch(\PDO::FETCH_ASSOC));


$sql = "SELECT COUNT(*) cnt, MIN(lft) min, MAX(rgt) max FROM categories";
if ($rootId) {
$sql .= " WHERE root = {$rootId}";
}

/** @var \Phalcon\Db\Result\Pdo $check2 */
$check2 = $connection->query($sql);
$result = $check2->fetch(\PDO::FETCH_ASSOC);

$this->assertEquals(1, $result['min']);
$this->assertEquals($result['cnt'] * 2, $result['max']);

$sql = "SELECT COUNT(*) cnt FROM categories WHERE MOD((rgt - lft), 2) = 0";
if ($rootId) {
$sql .= " AND root = {$rootId}";
}

/** @var \Phalcon\Db\Result\Pdo $check3 */
$check3 = $connection->query($sql);
$this->assertEquals(['cnt' => '0'], $check3->fetch(\PDO::FETCH_ASSOC));

$sql = "SELECT COUNT(*) cnt FROM categories WHERE MOD((lft - level + 2), 2) = 1";
if ($rootId) {
$sql .= " AND root = {$rootId}";
}

/** @var \Phalcon\Db\Result\Pdo $check4 */
$check4 = $connection->query($sql);
$this->assertEquals(['cnt' => '0'], $check4->fetch(\PDO::FETCH_ASSOC));
}

protected function createTree()
{
$cars = new CategoriesManyRoots();
$cars->name = 'Cars';
$cars->saveNode();

$ford = new CategoriesManyRoots();
$ford->name = 'Ford';

$audi = new CategoriesManyRoots();
$audi->name = 'Audi';

$mercedes = new CategoriesManyRoots();
$mercedes->name = 'Mercedes';

$ford->appendTo($cars);
$mercedes->insertAfter($ford);
$audi->insertBefore($ford);

$phones = new CategoriesManyRoots();
$phones->name = 'Mobile Phones';
$phones->saveNode();

$samsung = new CategoriesManyRoots();
$samsung->name = 'Samsung';

$motorola = new CategoriesManyRoots();
$motorola->name = 'Motorola';

$iphone = new CategoriesManyRoots();
$iphone->name = 'iPhone';

$samsung->appendTo($phones);
$motorola->insertAfter($samsung);
$iphone->prependTo($phones);
}
}
Loading