From 2db7b6a35a3aa3cb87d521294ebb3a70e400a3d5 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Tue, 9 Feb 2016 00:04:11 +0200 Subject: [PATCH 01/17] Fixed issue #539 --- Library/Phalcon/Session/Adapter/Database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Phalcon/Session/Adapter/Database.php b/Library/Phalcon/Session/Adapter/Database.php index 907027666..7ccf9350e 100644 --- a/Library/Phalcon/Session/Adapter/Database.php +++ b/Library/Phalcon/Session/Adapter/Database.php @@ -108,7 +108,7 @@ public function read($sessionId) $maxLifetime = (int) ini_get('session.gc_maxlifetime'); $options = $this->getOptions(); - $row = $options['db']->fetchOne( + $row = $this->connection->fetchOne( sprintf( 'SELECT %s FROM %s WHERE %s = ? AND COALESCE(%s, %s) + %d >= ?', $this->connection->escapeIdentifier($options['column_data']), @@ -214,7 +214,7 @@ public function gc($maxlifetime) { $options = $this->getOptions(); - return $options['db']->execute( + return $this->connection->execute( sprintf( 'DELETE FROM %s WHERE COALESCE(%s, %s) + %d < ?', $this->connection->escapeIdentifier($options['table']), From b59385ed82baab10e9cffce645801c0d03d32f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C5=82aszczyk?= Date: Wed, 17 Feb 2016 15:05:23 +0100 Subject: [PATCH 02/17] fix moveAsFirst and moveAsLast --- Library/Phalcon/Mvc/Model/Behavior/NestedSet.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php index 87aa49d53..c584d9f20 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php +++ b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php @@ -642,7 +642,7 @@ private function moveNode($target, $key, $levelUp) $condition = $this->leftAttribute . '>=' . $left . ' AND '; $condition .= $this->rightAttribute . '<=' . $right . ' AND '; - $condition .= $this->rootAttribute . '=' . $target->{$this->rootAttribute}; + $condition .= $this->rootAttribute . '=' . $owner->{$this->rootAttribute}; foreach ($owner::find($condition) as $i) { $arr = array( $this->leftAttribute => $i->{$this->leftAttribute} + $delta, @@ -659,8 +659,6 @@ private function moveNode($target, $key, $levelUp) } $this->ignoreEvent = false; - $this->shiftLeftRight($right + 1, $left - $right - 1); - $this->db->commit(); } else { $delta = $right - $left + 1; From 3be648db211a65189a10965bc611f9f0e83d1207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C5=82aszczyk?= Date: Wed, 17 Feb 2016 21:38:42 +0100 Subject: [PATCH 03/17] refactor shiftLeftRight() to ORM operation --- .../Phalcon/Mvc/Model/Behavior/NestedSet.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php index c584d9f20..3a3717fc1 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php +++ b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php @@ -657,11 +657,13 @@ private function moveNode($target, $key, $levelUp) return false; } } - $this->ignoreEvent = false; + $this->shiftLeftRight($right + 1, $left - $right - 1); + $this->ignoreEvent = false; $this->db->commit(); } else { $delta = $right - $left + 1; + $this->ignoreEvent = true; $this->shiftLeftRight($key, $delta); if ($left >= $key) { @@ -675,7 +677,6 @@ private function moveNode($target, $key, $levelUp) $condition .= ' AND ' . $this->rootAttribute . '=' . $owner->{$this->rootAttribute}; } - $this->ignoreEvent = true; foreach ($owner::find($condition) as $i) { if ($i->update(array($this->levelAttribute => $i->{$this->levelAttribute} + $levelDelta)) == false) { $this->db->rollback(); @@ -701,10 +702,10 @@ private function moveNode($target, $key, $levelUp) } } } - $this->ignoreEvent = false; $this->shiftLeftRight($right + 1, -$delta); + $this->ignoreEvent = false; $this->db->commit(); } @@ -726,15 +727,14 @@ private function shiftLeftRight($key, $delta) $condition .= ' AND ' . $this->rootAttribute . '=' . $owner->{$this->rootAttribute}; } - $query = sprintf( - 'UPDATE %s SET %s=%s+%d WHERE %s', - $this->owner->getSource(), - $attribute, - $attribute, - $delta, - $condition - ); - $this->owner->getWriteConnection()->execute($query); + foreach ($owner::find($condition) as $i) { + if ($i->update(array($attribute => $i->{$attribute} + $delta)) == false) { + $this->db->rollback(); + $this->ignoreEvent = false; + + return false; + } + } } } @@ -779,7 +779,9 @@ private function addNode($target, $key, $levelUp, $attributes) $owner->{$this->rootAttribute} = $target->{$this->rootAttribute}; } + $this->ignoreEvent = true; $this->shiftLeftRight($key, 2); + $this->ignoreEvent = false; $owner->{$this->leftAttribute} = $key; $owner->{$this->rightAttribute} = $key + 1; $owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp; From 0bbf7b56c36f7678f4ae4de79b748710035442b4 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Thu, 25 Feb 2016 08:57:50 +0200 Subject: [PATCH 04/17] Cleanup Phalcon\Mvc\Model\Behavior doc --- Library/Phalcon/Mvc/Model/Behavior/README.md | 330 +++++++++++-------- 1 file changed, 200 insertions(+), 130 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/Behavior/README.md b/Library/Phalcon/Mvc/Model/Behavior/README.md index a9123dfa5..3412de6fd 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/README.md +++ b/Library/Phalcon/Mvc/Model/Behavior/README.md @@ -1,33 +1,44 @@ -Phalcon\Mvc\Model\Behavior -========================== -NestedSet --------------- -## Installing and configuring +# Phalcon\Mvc\Model\Behavior + +## NestedSet + +### Installing and configuring + First you need to configure model as follows: ```php use Phalcon\Mvc\Model\Behavior\NestedSet as NestedSetBehavior; class Categories extends \Phalcon\Mvc\Model { - public function initialize() { - $this->addBehavior(new NestedSetBehavior(array( - 'leftAttribute' => 'lft', + $this->addBehavior(new NestedSetBehavior([ + 'rootAttribute' => 'root', + 'leftAttribute' => 'lft', 'rightAttribute' => 'rgt', 'levelAttribute' => 'level' - ))); + ])); } - } ``` -There is no need to validate fields specified in leftAttribute, rightAttribute, rootAttribute and levelAttribute options. -By default leftAttribute = lft, rightAttribute = rgt and levelAttribute = level so you can skip configuring these. +There is no need to validate fields specified in `leftAttribute`, `rightAttribute`, `rootAttribute` and `levelAttribute` options. -There are two ways this behavior can work: one tree per table and multiple trees per table. 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. +By default: + +* `leftAttribute` = `lft` +* `rightAttribute` = `rgt` +* `levelAttribute` = `level` +* `rootAttribute` = `root` + +so you can skip configuring these. + +There are two ways this behavior can work: one tree per table and multiple trees per table. +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. + +### Selecting from a tree -## Selecting from a tree In the following we'll use an example model Category with the following in its DB: ``` - 1. Mobile phones @@ -41,98 +52,134 @@ In the following we'll use an example model Category with the following in its D - 9. Ford - 10. Mercedes ``` -In this example we have two trees. Tree roots are ones with ID=1 and ID=7. -### Getting all roots +In this example we have two trees. Tree `roots` are ones with ID=1 and ID=7. + +#### Getting all roots + ```php -$roots=(new Categories())->roots(); +$roots = (new Categories())->roots(); ``` Result: result set containing Mobile phones and Cars nodes. You can also add the following method to the model if you use a single root: ```php -public static function getRoot() { +public static function getRoot() +{ return self::findFirst('lft = 1'); } ``` + Or the following method if you use multiple roots: ```php -public static function getRoots() { +public static function getRoots() +{ return self::find('lft = 1'); } ``` -### Getting all descendants of a node + +#### Getting all descendants of a node + ```php -$category=Categories::findFirst(1); -$descendants=$category->descendants(); +$category = Categories::findFirst(1); +$descendants = $category->descendants(); ``` + Result: result set containing iPhone, Samsung, X100, C200 and Motorola. -### Getting all children of a node + +#### Getting all children of a node + ```php -$category=Categories::findFirst(1); -$children=$category->children(); +$category = Categories::findFirst(1); +$children = $category->children(); ``` + Result: result set containing iPhone, Samsung and Motorola. -### Getting all ancestors of a node + +#### Getting all ancestors of a node + ```php -$category=Categories::findFirst(5); -$ancestors=$category->ancestors(); +$category = Categories::findFirst(5); +$ancestors = $category->ancestors(); ``` + Result: result set containing Samsung and Mobile phones. -### Getting parent of a node + +#### Getting parent of a node + ```php -$category=Categories::findFirst(9); -$parent=$category->parent(); +$category = Categories::findFirst(9); +$parent = $category->parent(); ``` + Result: Cars node. -### Getting node siblings + +#### Getting node siblings + ```php -$category=Categories::findFirst(9); -$nextSibling=$category->next(); -$prevSibling=$category->prev(); +$category = Categories::findFirst(9); +$nextSibling = $category->next(); +$prevSibling = $category->prev(); ``` + Result: Mercedes node and Audi node. -### Getting the whole tree + +#### Getting the whole tree + You can get the whole tree using standard model methods like the following. For single tree per table: ```php -Categories::find(array('order'=>'lft')); +Categories::find(['order' => 'lft']); ``` + For multiple trees per table: ```php -Categories::find(array('root=:root:', 'order'=>'lft', 'bind'=>array('root'=>$root_id))); +Categories::find(['root=:root:', 'order'=>'lft', 'bind' => ['root' => $root_id]]); ``` -## Modifying a tree +### Modifying a tree + In this section we'll build a tree like the one used in the previous section. -### Creating root nodes -You can create a root node using `saveNode()`. In a single tree per table mode you can create only one root node. If you'll attempt to create more there will be Exception thrown. + +#### Creating root nodes + +You can create a root node using `saveNode()`. In a single tree per table mode you can create only one root node. +If you'll attempt to create more there will be Exception thrown. + ```php -$root=new Categories(); -$root->title='Mobile Phones'; +$root = new Categories(); +$root->title = 'Mobile Phones'; $root->saveNode(); -$root=new Categories(); -$root->title='Cars'; + +$root = new Categories(); +$root->title = 'Cars'; $root->saveNode(); ``` + Result: ``` - 1. Mobile Phones - 2. Cars ``` -### Adding child nodes + +#### Adding child nodes + There are multiple methods allowing you adding child nodes. Let's use these to add nodes to the tree we have: ```php -$category1=new Categories(); -$category1->title='Ford'; -$category2=new Categories(); -$category2->title='Mercedes'; -$category3=new Categories(); -$category3->title='Audi'; -$root=Categories::findFirst(1); +$category1 = new Categories(); +$category1->title = 'Ford'; + +$category2 = new Categories(); +$category2->title = 'Mercedes'; + +$category3 = new Categories(); +$category3->title = 'Audi'; + +$root = Categories::findFirst(1); $category1->appendTo($root); $category2->insertAfter($category1); $category3->insertBefore($category1); ``` + Result: ``` - 1. Mobile phones @@ -141,19 +188,24 @@ Result: - 5. Mercedes - 2. Cars ``` -Logically the tree above doesn't looks correct. We'll fix it later. + +Logically the tree above does not looks correct. We'll fix it later. ```php -$category1=new Categories(); -$category1->title='Samsung'; -$category2=new Categories(); -$category2->title='Motorola'; -$category3=new Categories(); -$category3->title='iPhone'; -$root=Categories::findFirst(2); +$category1 = new Categories(); +$category1->title = 'Samsung'; + +$category2 = new Categories(); +$category2->title = 'Motorola'; + +$category3 = new Categories(); +$category3->title = 'iPhone'; + +$root = Categories::findFirst(2); $category1->appendTo($root); $category2->insertAfter($category1); $category3->prependTo($root); ``` + Result: ``` - 1. Mobile phones @@ -165,15 +217,19 @@ Result: - 7. Samsung - 8. Motorola ``` + ```php -$category1=new Categories(); -$category1->title='X100'; -$category2=new Categories(); -$category2->title='C200'; -$node=Categories::findFirst(3); +$category1 = new Categories(); +$category1->title = 'X100'; + +$category2 = new Categories(); +$category2->title = 'C200'; + +$node = Categories::findFirst(3); $category1->appendTo($node); $category2->prependTo($node); ``` + Result: ``` - 1. Mobile phones @@ -187,35 +243,46 @@ Result: - 7. Samsung - 8. Motorola ``` -## Modifying a tree + +### Modifying a tree + In this section we'll finally make our tree logical. -### Tree modification methods + +#### Tree modification methods + There are several methods allowing you to modify a tree. Let's start: ```php // move phones to the proper place -$x100=Categories::findFirst(10); -$c200=Categories::findFirst(9); -$samsung=Categories::findFirst(7); +$x100 = Categories::findFirst(10); +$c200 = Categories::findFirst(9); + +$samsung = Categories::findFirst(7); $x100->moveAsFirst($samsung); $c200->moveBefore($x100); + // now move all Samsung phones branch -$mobile_phones=Categories::findFirst(1); +$mobile_phones = Categories::findFirst(1); $samsung->moveAsFirst($mobile_phones); + // move the rest of phone models -$iphone=Categories::findFirst(6); +$iphone = Categories::findFirst(6); $iphone->moveAsFirst($mobile_phones); -$motorola=Categories::findFirst(8); + +$motorola = Categories::findFirst(8); $motorola->moveAfter($samsung); + // move car models to appropriate place -$cars=Categories::findFirst(2); -$audi=Categories::findFirst(3); -$ford=Categories::findFirst(4); -$mercedes=Categories::findFirst(5); +$cars = Categories::findFirst(2); +$audi = Categories::findFirst(3); +$ford = Categories::findFirst(4); +$mercedes = Categories::findFirst(5); -foreach(array($audi,$ford,$mercedes) as $category) +foreach([$audi,$ford,$mercedes] as $category) { $category->moveAsLast($cars); +} ``` + Result: ``` - 1. Mobile phones @@ -229,46 +296,52 @@ Result: - 4. Ford - 5. Mercedes ``` -### Moving a node making it a new root -There is a special `moveAsRoot()` method that allows moving a node and making it a new root. All descendants are moved as well in this case. + +#### Moving a node making it a new root +There is a special `moveAsRoot()` method that allows moving a node and making it a new root. +All descendants are moved as well in this case. + Example: ```php -$node=Categories::findFirst(10); +$node = Categories::findFirst(10); $node->moveAsRoot(); ``` -### Identifying node type + +#### Identifying node type There are three methods to get node type: `isRoot()`, `isLeaf()`, `isDescendantOf()`. + Example: ```php -$root=Categories::findFirst(1); -var_dump($root->isRoot()); //true; -var_dump($root->isLeaf()); //false; -$node=Categories::findFirst(9); -var_dump($node->isDescendantOf($root)); //true; -var_dump($node->isRoot()); //false; -var_dump($node->isLeaf()); //true; -$samsung=Categories::findFirst(7); -var_dump($node->isDescendantOf($samsung)); //true; -``` -## Useful code -### Non-recursive tree traversal +$root = Categories::findFirst(1); +var_dump($root->isRoot()); // true; +var_dump($root->isLeaf()); // false; + +$node = Categories::findFirst(9); +var_dump($node->isDescendantOf($root)); // true; +var_dump($node->isRoot()); // false; +var_dump($node->isLeaf()); // true; + +$samsung = Categories::findFirst(7); +var_dump($node->isDescendantOf($samsung)); // true; +``` +### Useful code + +#### Non-recursive tree traversal + ```php -$order='lft'; // or 'root, lft' for multiple trees -$categories=Categories::find(array('order'=>$order)); -$level=0; +$order = 'lft'; // or 'root, lft' for multiple trees +$categories = Categories::find(['order' => $order]); +$level = 0; -foreach($categories as $n=>$category) -{ - if($category->level==$level) +foreach ($categories as $n => $category) { + if ($category->level == $level) { echo "\n"; - else if($category->level>$level) + } elseif ($category->level>$level) { echo "
    \n"; - else - { + } else { echo "\n"; - for($i=$level-$category->level;$i;$i--) - { + for ($i = $level - $category->level; $i; $i--) { echo "
\n"; echo "\n"; } @@ -276,47 +349,44 @@ foreach($categories as $n=>$category) echo "
  • \n"; echo $category->title; - $level=$category->level; + $level = $category->level; } -for($i=$level;$i;$i--) -{ +for ($i = $level; $i; $i--) { echo "
  • \n"; echo "\n"; } ``` +## Blameable -Blameable --------------- ```php - class Products extends Phalcon\Mvc\Model { - public function initialize() { $this->keepSnapshots(true); } - } ``` -``` -CREATE TABLE audit ( - id integer primary key auto_increment, - user_name varchar(32) not null, - model_name varchar(32) not null, - ipaddress char(15) not null, - type char(1) not null, /* C=Create/U=Update */ - created_at datetime not null -); +```sql +CREATE TABLE `audit` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `user_name` VARCHAR(32) NOT NULL, + `model_name` VARCHAR(32) NOT NULL, + `ipaddress` CHAR(15) NOT NULL, + `type` CHAR(1) NOT NULL, /* C=Create/U=Update */ + `created_at` DATETIME NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE audit_detail ( - id integer primary key auto_increment, - audit_id integer not null, - field_name varchar(32) not null, - old_value varchar(32), - new_value varchar(32) not null -) +CREATE TABLE `audit_detail` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `audit_id` BIGINT NOT NULL, + `field_name` VARCHAR(32) NOT NULL, + `old_value` VARCHAR(32) DEFAULT NULL, + `new_value` VARCHAR(32) NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` From d88122c2d171ed37f5ecb4706c02ee578d047b3e Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Sat, 27 Feb 2016 03:08:39 +0200 Subject: [PATCH 05/17] Fixed NestedSet::moveAsFirst --- .../Phalcon/Mvc/Model/Behavior/NestedSet.php | 329 ++++++++++-------- Library/Phalcon/Mvc/Model/Behavior/README.md | 22 ++ 2 files changed, 214 insertions(+), 137 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php index 3a3717fc1..7f14cb466 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php +++ b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php @@ -2,19 +2,26 @@ namespace Phalcon\Mvc\Model\Behavior; -use Phalcon\Mvc\Model\Behavior; -use Phalcon\Mvc\Model\BehaviorInterface; use Phalcon\Mvc\Model; use Phalcon\Mvc\ModelInterface; -use Phalcon\Db\Adapter as DBAdapter; +use Phalcon\Mvc\Model\Behavior; +use Phalcon\Mvc\Model\Exception; +use Phalcon\Db\AdapterInterface; +use Phalcon\Mvc\Model\BehaviorInterface; +use Phalcon\Mvc\Model\ResultsetInterface; class NestedSet extends Behavior implements BehaviorInterface { /** - * @var DbAdapter|null + * @var AdapterInterface|null */ private $db; + + /** + * @var ModelInterface|null + */ private $owner; + private $hasManyRoots = false; private $rootAttribute = 'root'; private $leftAttribute = 'lft'; @@ -22,12 +29,11 @@ class NestedSet extends Behavior implements BehaviorInterface private $levelAttribute = 'level'; private $primaryKey = 'id'; private $ignoreEvent = false; - private $deleted = false; public function __construct($options = null) { - if (isset($options['db']) && $options['db'] instanceof dbAdapter) { + if (isset($options['db']) && $options['db'] instanceof AdapterInterface) { $this->db = $options['db']; } @@ -56,22 +62,39 @@ public function __construct($options = null) } } + /** + * @param string $eventType + * @param ModelInterface $model + * @throws Exception + */ public function notify($eventType, ModelInterface $model) { - $message = 'You should not use this method when NestedSetBehavior attached. Use the methods of behavior.'; switch ($eventType) { case 'beforeCreate': case 'beforeDelete': case 'beforeUpdate': if (!$this->ignoreEvent) { - throw new \Phalcon\Mvc\Model\Exception($message); + throw new Exception( + sprintf( + 'You should not use %s:%s when %s attached. Use the methods of behavior.', + get_class($model), + $eventType, + __CLASS__ + ) + ); } break; } } /** - * @throws \Exception + * Calls a method when it's missing in the model + * + * @param ModelInterface $model + * @param string $method + * @param null $arguments + * @return mixed|null|string + * @throws Exception */ public function missingMethod(ModelInterface $model, $method, $arguments = null) { @@ -79,29 +102,25 @@ public function missingMethod(ModelInterface $model, $method, $arguments = null) return null; } - if (!$this->db) { - if ($model->getDi()->has('db')) { - $this->db = $model->getDi()->get('db'); - } else { - throw new \Exception('Undefined database handler.'); - } - } - + $this->getDbHandler($model); $this->setOwner($model); - $result = call_user_func_array(array($this, $method), $arguments); - if ($result === null) { - return ''; - } - return $result; + return call_user_func_array([$this, $method], $arguments); } + /** + * @return ModelInterface + */ public function getOwner() { + if (!$this->owner instanceof ModelInterface) { + trigger_error("Owner isn't a valid ModelInterface instance.", E_USER_WARNING); + } + return $this->owner; } - public function setOwner($owner) + public function setOwner(ModelInterface $owner) { $this->owner = $owner; } @@ -178,8 +197,7 @@ public function isDescendantOf($subj) * * @param int $depth the depth. * @param boolean $addSelf If TRUE - parent node will be added to result set. - * - * @return \Phalcon\Mvc\Model\ResultsetInterface + * @return ResultsetInterface */ public function descendants($depth = null, $addSelf = false) { @@ -204,7 +222,7 @@ public function descendants($depth = null, $addSelf = false) /** * Named scope. Gets children for node (direct descendants only). * - * @return \Phalcon\Mvc\Model\ResultsetInterface + * @return ResultsetInterface */ public function children() { @@ -215,8 +233,7 @@ public function children() * Named scope. Gets ancestors for node. * * @param int $depth the depth. - * - * @return \Phalcon\Mvc\Model\ResultsetInterface + * @return ResultsetInterface */ public function ancestors($depth = null) { @@ -241,7 +258,7 @@ public function ancestors($depth = null) /** * Named scope. Gets root node(s). * - * @return \Phalcon\Mvc\Model\ResultsetInterface + * @return ResultsetInterface */ public function roots() { @@ -255,7 +272,6 @@ public function roots() * * @return \Phalcon\Mvc\ModelInterface */ - // @codingStandardsIgnoreStart public function parent() { $owner = $this->getOwner(); @@ -272,12 +288,11 @@ public function parent() return $query->execute()->getFirst(); } - // @codingStandardsIgnoreEnd /** * Named scope. Gets previous sibling of node. * - * @return \Phalcon\Mvc\ModelInterface + * @return ModelInterface */ public function prev() { @@ -295,7 +310,7 @@ public function prev() /** * Named scope. Gets next sibling of node. * - * @return \Phalcon\Mvc\ModelInterface + * @return ModelInterface */ public function next() { @@ -313,12 +328,11 @@ public function next() /** * Prepends node to target as first child. * - * @param \Phalcon\Mvc\ModelInterface $target the target - * @param array $attributes list of attributes. - * - * @return boolean whether the prepending succeeds. + * @param ModelInterface $target the target + * @param array $attributes List of attributes. + * @return boolean */ - public function prependTo($target, $attributes = null) + public function prependTo(ModelInterface $target, array $attributes = null) { return $this->addNode($target, $target->{$this->leftAttribute} + 1, 1, $attributes); } @@ -326,12 +340,11 @@ public function prependTo($target, $attributes = null) /** * Prepends target to node as first child. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * @param array $attributes list of attributes. - * - * @return boolean whether the prepending succeeds. + * @param ModelInterface $target the target. + * @param array $attributes list of attributes. + * @return boolean */ - public function prepend($target, $attributes = null) + public function prepend(ModelInterface $target, array $attributes = null) { return $target->prependTo($this->getOwner(), $attributes); } @@ -339,12 +352,11 @@ public function prepend($target, $attributes = null) /** * Appends node to target as last child. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * @param array $attributes list of attributes. - * - * @return boolean whether the appending succeeds. + * @param ModelInterface $target the target. + * @param array $attributes list of attributes. + * @return boolean */ - public function appendTo($target, $attributes = null) + public function appendTo(ModelInterface $target, array $attributes = null) { return $this->addNode($target, $target->{$this->rightAttribute}, 1, $attributes); } @@ -352,12 +364,11 @@ public function appendTo($target, $attributes = null) /** * Appends target to node as last child. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * @param array $attributes list of attributes. - * - * @return boolean whether the appending succeeds. + * @param ModelInterface $target the target. + * @param array $attributes list of attributes. + * @return boolean */ - public function append($target, $attributes = null) + public function append(ModelInterface $target, array $attributes = null) { return $target->appendTo($this->getOwner(), $attributes); } @@ -365,12 +376,11 @@ public function append($target, $attributes = null) /** * Inserts node as previous sibling of target. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * @param array $attributes list of attributes. - * - * @return boolean whether the inserting succeeds. + * @param ModelInterface $target the target. + * @param array $attributes list of attributes. + * @return boolean */ - public function insertBefore($target, $attributes = null) + public function insertBefore(ModelInterface $target, array $attributes = null) { return $this->addNode($target, $target->{$this->leftAttribute}, 0, $attributes); } @@ -378,12 +388,11 @@ public function insertBefore($target, $attributes = null) /** * Inserts node as next sibling of target. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * @param array $attributes list of attributes. - * - * @return boolean whether the inserting succeeds. + * @param ModelInterface $target the target. + * @param array $attributes list of attributes. + * @return boolean */ - public function insertAfter($target, $attributes = null) + public function insertAfter(ModelInterface $target, array $attributes = null) { return $this->addNode($target, $target->{$this->rightAttribute} + 1, 0, $attributes); } @@ -391,11 +400,10 @@ public function insertAfter($target, $attributes = null) /** * Move node as previous sibling of target. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * + * @param ModelInterface $target the target. * @return boolean */ - public function moveBefore($target) + public function moveBefore(ModelInterface $target) { return $this->moveNode($target, $target->{$this->leftAttribute}, 0); } @@ -403,11 +411,10 @@ public function moveBefore($target) /** * Move node as next sibling of target. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * + * @param ModelInterface $target the target. * @return boolean */ - public function moveAfter($target) + public function moveAfter(ModelInterface $target) { return $this->moveNode($target, $target->{$this->rightAttribute} + 1, 0); } @@ -415,11 +422,10 @@ public function moveAfter($target) /** * Move node as first child of target. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * + * @param ModelInterface $target the target. * @return boolean */ - public function moveAsFirst($target) + public function moveAsFirst(ModelInterface $target) { return $this->moveNode($target, $target->{$this->leftAttribute} + 1, 1); } @@ -427,11 +433,10 @@ public function moveAsFirst($target) /** * Move node as last child of target. * - * @param \Phalcon\Mvc\ModelInterface $target the target. - * + * @param ModelInterface $target the target. * @return boolean */ - public function moveAsLast($target) + public function moveAsLast(ModelInterface $target) { return $this->moveNode($target, $target->{$this->rightAttribute}, 1); } @@ -440,26 +445,26 @@ public function moveAsLast($target) * Move node as new root. * * @return boolean - * @throws \Phalcon\Mvc\Model\Exception + * @throws Exception */ public function moveAsRoot() { $owner = $this->getOwner(); if (!$this->hasManyRoots) { - throw new \Phalcon\Mvc\Model\Exception('Many roots mode is off.'); + throw new Exception('Many roots mode is off.'); } if ($this->getIsNewRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node should not be new record.'); + throw new Exception('The node should not be new record.'); } if ($this->getIsDeletedRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node should not be deleted.'); + throw new Exception('The node should not be deleted.'); } if ($owner->isRoot()) { - throw new \Phalcon\Mvc\Model\Exception('The node already is root node.'); + throw new Exception('The node already is root node.'); } $this->db->begin(); @@ -502,18 +507,20 @@ public function moveAsRoot() * * @param array $attributes list of attributes. * @param array $whiteList whether to perform validation. - * * @return boolean */ - public function saveNode($attributes = null, $whiteList = null) + public function saveNode(array $attributes = null, array $whiteList = null) { $owner = $this->getOwner(); + $this->ignoreEvent = true; + if (!$owner->readAttribute($this->primaryKey)) { - return $this->makeRoot($attributes, $whiteList); + $result = $this->makeRoot($attributes, $whiteList); + } else { + $result = $owner->update($attributes, $whiteList); } - $this->ignoreEvent = true; - $result = $owner->update($attributes, $whiteList); + $this->ignoreEvent = false; return $result; @@ -523,18 +530,18 @@ public function saveNode($attributes = null, $whiteList = null) * Deletes node and it's descendants. * * @return boolean - * @throws \Phalcon\Mvc\Model\Exception + * @throws Exception */ public function deleteNode() { $owner = $this->getOwner(); if ($this->getIsNewRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node cannot be deleted because it is new.'); + throw new Exception('The node cannot be deleted because it is new.'); } if ($this->getIsDeletedRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node cannot be deleted because it is already deleted.'); + throw new Exception('The node cannot be deleted because it is already deleted.'); } $this->db->begin(); @@ -578,43 +585,63 @@ public function deleteNode() } /** - * @param \Phalcon\Mvc\ModelInterface $target + * Gets DB handler. + * + * @param ModelInterface $model + * @return AdapterInterface + * @throws Exception + */ + private function getDbHandler(ModelInterface $model) + { + if (!$this->db instanceof AdapterInterface) { + if ($model->getDi()->has('db')) { + $db = $model->getDi()->getShared('db'); + if (!$db instanceof AdapterInterface) { + throw new Exception('The "db" service which was obtained from DI is invalid adapter.'); + } + $this->db = $db; + } else { + throw new Exception('Undefined database handler.'); + } + } + + return $this->db; + } + + /** + * @param ModelInterface $target * @param int $key * @param int $levelUp * * @return boolean - * @throws \Phalcon\Mvc\Model\Exception + * @throws Exception */ - private function moveNode($target, $key, $levelUp) + private function moveNode(ModelInterface $target, $key, $levelUp) { $owner = $this->getOwner(); - if (!$target) { - throw new \Phalcon\Mvc\Model\Exception('Target node is not defined.'); - } - if ($this->getIsNewRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node should not be new record.'); + throw new Exception('The node should not be new record.'); } if ($this->getIsDeletedRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node should not be deleted.'); + throw new Exception('The node should not be deleted.'); } if ($target->getIsDeletedRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The target node should not be deleted.'); + throw new Exception('The target node should not be deleted.'); } if ($owner == $target) { - throw new \Phalcon\Mvc\Model\Exception('The target node should not be self.'); + throw new Exception('The target node should not be self.'); } if ($target->isDescendantOf($owner)) { - throw new \Phalcon\Mvc\Model\Exception('The target node should not be descendant.'); + throw new Exception('The target node should not be descendant.'); } if (!$levelUp && $target->isRoot()) { - throw new \Phalcon\Mvc\Model\Exception('The target node should not be root.'); + throw new Exception('The target node should not be root.'); } $this->db->begin(); @@ -625,11 +652,15 @@ private function moveNode($target, $key, $levelUp) if ($this->hasManyRoots && $owner->{$this->rootAttribute} !== $target->{$this->rootAttribute}) { $this->ignoreEvent = true; - foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) { - $condition = $attribute . '>=' . $key - . ' AND ' . $this->rootAttribute . '=' . $target->{$this->rootAttribute}; - foreach ($owner::find($condition) as $i) { - if ($i->update(array($attribute => $i->{$attribute} + $right - $left + 1)) == false) { + + // 1. Rebuild the target tree + foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { + $condition = $attribute . '>=' . $key . ' AND ' . $this->rootAttribute . '=' . $target->{$this->rootAttribute}; + + foreach ($target::find($condition) as $i) { + $delta = $right - $left + 1; + /** @var ModelInterface $i */ + if (!$i->update([$attribute => $i->{$attribute} + $delta])) { $this->db->rollback(); $this->ignoreEvent = false; @@ -640,16 +671,19 @@ private function moveNode($target, $key, $levelUp) $delta = $key - $left; + // 2. Rebuild the owner's tree of children (owner sub-tree) $condition = $this->leftAttribute . '>=' . $left . ' AND '; $condition .= $this->rightAttribute . '<=' . $right . ' AND '; $condition .= $this->rootAttribute . '=' . $owner->{$this->rootAttribute}; + foreach ($owner::find($condition) as $i) { - $arr = array( + $arr = [ $this->leftAttribute => $i->{$this->leftAttribute} + $delta, $this->rightAttribute => $i->{$this->rightAttribute} + $delta, $this->levelAttribute => $i->{$this->levelAttribute} + $levelDelta, $this->rootAttribute => $target->{$this->rootAttribute} - ); + ]; + if ($i->update($arr) == false) { $this->db->rollback(); $this->ignoreEvent = false; @@ -658,7 +692,9 @@ private function moveNode($target, $key, $levelUp) } } - $this->shiftLeftRight($right + 1, $left - $right - 1); + // 3. Rebuild the owner tree + $this->shiftLeftRight($right + 1, $left - $right - 1, $owner); + $this->ignoreEvent = false; $this->db->commit(); } else { @@ -704,6 +740,7 @@ private function moveNode($target, $key, $levelUp) } $this->shiftLeftRight($right + 1, -$delta); + $this->ignoreEvent = false; $this->ignoreEvent = false; $this->db->commit(); @@ -715,12 +752,14 @@ private function moveNode($target, $key, $levelUp) /** * @param int $key * @param int $delta + * @param ModelInterface $model + * @return boolean */ - private function shiftLeftRight($key, $delta) + private function shiftLeftRight($key, $delta, ModelInterface $model = null) { - $owner = $this->getOwner(); + $owner = $model ?: $this->getOwner(); - foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) { + foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { $condition = $attribute . '>=' . $key; if ($this->hasManyRoots) { @@ -728,7 +767,8 @@ private function shiftLeftRight($key, $delta) } foreach ($owner::find($condition) as $i) { - if ($i->update(array($attribute => $i->{$attribute} + $delta)) == false) { + /** @var ModelInterface $i */ + if ($i->update([$attribute => $i->{$attribute} + $delta]) == false) { $this->db->rollback(); $this->ignoreEvent = false; @@ -736,61 +776,76 @@ private function shiftLeftRight($key, $delta) } } } + + return true; } /** - * @param \Phalcon\Mvc\ModelInterface $target + * @param ModelInterface $target * @param int $key * @param int $levelUp * @param array $attributes * * @return boolean - * @throws \Phalcon\Mvc\Model\Exception + * @throws \Exception */ - private function addNode($target, $key, $levelUp, $attributes) + private function addNode(ModelInterface $target, $key, $levelUp, array $attributes = null) { $owner = $this->getOwner(); - if (!$target) { - throw new \Phalcon\Mvc\Model\Exception('The node cannot be inserted because target is not defined.'); - } - if (!$this->getIsNewRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node cannot be inserted because it is not new.'); + throw new Exception('The node cannot be inserted because it is not new.'); } if ($this->getIsDeletedRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node cannot be inserted because it is deleted.'); + throw new Exception('The node cannot be inserted because it is deleted.'); } if ($target->getIsDeletedRecord()) { - throw new \Phalcon\Mvc\Model\Exception('The node cannot be inserted because target node is deleted.'); + throw new Exception('The node cannot be inserted because target node is deleted.'); } if ($owner == $target) { - throw new \Phalcon\Mvc\Model\Exception('The target node should not be self.'); + throw new Exception('The target node should not be self.'); } if (!$levelUp && $target->isRoot()) { - throw new \Phalcon\Mvc\Model\Exception('The target node should not be root.'); + throw new Exception('The target node should not be root.'); } if ($this->hasManyRoots) { $owner->{$this->rootAttribute} = $target->{$this->rootAttribute}; } - $this->ignoreEvent = true; - $this->shiftLeftRight($key, 2); - $this->ignoreEvent = false; - $owner->{$this->leftAttribute} = $key; - $owner->{$this->rightAttribute} = $key + 1; - $owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp; + $db = $this->getDbHandler($owner); + $db->begin(); - $this->ignoreEvent = true; - $result = $owner->create($attributes); - $this->ignoreEvent = false; + try { + $this->ignoreEvent = true; + $this->shiftLeftRight($key, 2); + $owner->{$this->leftAttribute} = $key; + $owner->{$this->rightAttribute} = $key + 1; + $owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp; - return $result; + $result = $owner->create($attributes); + $this->ignoreEvent = false; + + if (!$result ) { + $db->rollback(); + $this->ignoreEvent = false; + + return false; + } + + $db->commit(); + } catch(\Exception $e) { + $db->rollback(); + $this->ignoreEvent = false; + + throw $e; + } + + return true; } /** @@ -798,7 +853,7 @@ private function addNode($target, $key, $levelUp, $attributes) * @param array $whiteList * * @return boolean - * @throws \Phalcon\Mvc\Model\Exception + * @throws Exception */ private function makeRoot($attributes, $whiteList) { @@ -824,7 +879,7 @@ private function makeRoot($attributes, $whiteList) $this->db->commit(); } else { if (count($owner->roots())) { - throw new \Phalcon\Mvc\Model\Exception('Cannot create more than one root in single root mode.'); + throw new Exception('Cannot create more than one root in single root mode.'); } if ($owner->create($attributes, $whiteList) == false) { diff --git a/Library/Phalcon/Mvc/Model/Behavior/README.md b/Library/Phalcon/Mvc/Model/Behavior/README.md index 3412de6fd..290c29004 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/README.md +++ b/Library/Phalcon/Mvc/Model/Behavior/README.md @@ -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: From e1790f792c55c7282d39739a52a7cf2d7280bcfa Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Sat, 27 Feb 2016 03:45:21 +0200 Subject: [PATCH 06/17] Initial NestedSetTest skeleton --- .../Phalcon/Mvc/Model/Behavior/NestedSet.php | 13 +- composer.json | 10 +- docs/TESTING.md | 2 +- tests/_data/dump.sql | 19 ++ tests/unit/Mvc/Model/Behavior/Helper.php | 231 +++++++++++++ .../unit/Mvc/Model/Behavior/NestedSetTest.php | 316 ++++++++++++++++++ .../Mvc/Model/Behavior/Stubs/Categories.php | 72 ++++ 7 files changed, 654 insertions(+), 9 deletions(-) create mode 100644 tests/unit/Mvc/Model/Behavior/Helper.php create mode 100644 tests/unit/Mvc/Model/Behavior/NestedSetTest.php create mode 100644 tests/unit/Mvc/Model/Behavior/Stubs/Categories.php diff --git a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php index 7f14cb466..a73f9fe4e 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php +++ b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php @@ -655,8 +655,10 @@ private function moveNode(ModelInterface $target, $key, $levelUp) // 1. Rebuild the target tree foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { - $condition = $attribute . '>=' . $key . ' AND ' . $this->rootAttribute . '=' . $target->{$this->rootAttribute}; - + $condition = join(' AND ', [ + $attribute . '>=' . $key, + $this->rootAttribute . '=' . $target->{$this->rootAttribute}, + ]); foreach ($target::find($condition) as $i) { $delta = $right - $left + 1; /** @var ModelInterface $i */ @@ -823,14 +825,17 @@ private function addNode(ModelInterface $target, $key, $levelUp, array $attribut try { $this->ignoreEvent = true; $this->shiftLeftRight($key, 2); + $this->ignoreEvent = false; + $owner->{$this->leftAttribute} = $key; $owner->{$this->rightAttribute} = $key + 1; $owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp; + $this->ignoreEvent = true; $result = $owner->create($attributes); $this->ignoreEvent = false; - if (!$result ) { + if (!$result) { $db->rollback(); $this->ignoreEvent = false; @@ -838,7 +843,7 @@ private function addNode(ModelInterface $target, $key, $levelUp, array $attribut } $db->commit(); - } catch(\Exception $e) { + } catch (\Exception $e) { $db->rollback(); $this->ignoreEvent = false; diff --git a/composer.json b/composer.json index 1e16ed3ac..101be97a6 100644 --- a/composer.json +++ b/composer.json @@ -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": "*", diff --git a/docs/TESTING.md b/docs/TESTING.md index 8b131915f..274eaafdb 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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]: diff --git a/tests/_data/dump.sql b/tests/_data/dump.sql index 433fa433c..4a0899bf3 100644 --- a/tests/_data/dump.sql +++ b/tests/_data/dump.sql @@ -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, diff --git a/tests/unit/Mvc/Model/Behavior/Helper.php b/tests/unit/Mvc/Model/Behavior/Helper.php new file mode 100644 index 000000000..1ccc5611f --- /dev/null +++ b/tests/unit/Mvc/Model/Behavior/Helper.php @@ -0,0 +1,231 @@ + + * @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 license@phalconphp.com + * 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); + } +} diff --git a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php new file mode 100644 index 000000000..b3ec8cef9 --- /dev/null +++ b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php @@ -0,0 +1,316 @@ + + * @package Phalcon\Test\Mvc\Model\Behavior + * @group db + * + * 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 license@phalconphp.com + * so that we can send you a copy immediately. + */ +class NestedSetTest extends Helper +{ + /** + * Initialize NestedSet Behavior without params + * + * @author Serghei Iakovlev + * @since 2016-02-27 + */ + public function testShouldCreateNestedSetBehaviorInstanceWithNoParams() + { + $this->specify( + 'Unable to initialize NestedSet Behavior without params correctly', + function ($property, $expected) { + $behavior = new NestedSetBehavior; + expect($this->getProperty($property, $behavior))->equals($expected); + }, + ['examples' => [ + ['db', null], + ['owner', null], + ['hasManyRoots', false], + ['rootAttribute', 'root'], + ['leftAttribute', 'lft'], + ['rightAttribute', 'rgt'], + ['rootAttribute', 'root'], + ['levelAttribute', 'level'], + ['primaryKey', 'id'], + ['ignoreEvent', false], + ['deleted', false], + ]] + ); + } + + /** + * Initialize NestedSet Behavior with desired params + * + * @author Serghei Iakovlev + * @since 2016-02-27 + */ + public function testShouldCreateNestedSetBehaviorInstanceWithDesiredParams() + { + $this->specify( + 'Unable to initialize NestedSet Behavior with desired params correctly', + function ($property, $value) { + $behavior = new NestedSetBehavior([$property => $value]); + expect($this->getProperty($property, $behavior))->equals($value); + }, + ['examples' => [ + ['leftAttribute', 'left'], + ['rightAttribute', 'right'], + ['rootAttribute', 'main'], + ['levelAttribute', 'lvl'], + ['hasManyRoots', true], + ['primaryKey', 'pk'], + ['db', $this->getConnection()], + ]] + ); + } + + /** + * Creating root nodes + * + * @author Serghei Iakovlev + * @since 2016-02-27 + */ + public function testShouldCreateARootNodeUsingSaveNode() + { + $this->specify( + 'Unable to create a root node using NestedSet::saveNode', + function () { + $I = $this->tester; + + $I->seeNumRecords(0, CategoriesManyRoots::$table); + + $category1 = new CategoriesManyRoots(); + $category1->name = 'Mobile Phones'; + $category1->saveNode(); + + $I->seeInDatabase(CategoriesManyRoots::$table, ['name' => 'Mobile Phones']); + + $category1 = CategoriesManyRoots::findFirst(); + + expect($category1->root)->equals(1); + expect($category1->lft)->equals(1); + expect($category1->rgt)->equals(2); + expect($category1->level)->equals(1); + + $category2 = new CategoriesManyRoots(); + $category2->name = 'Cars'; + $category2->saveNode(); + + $I->seeInDatabase(CategoriesManyRoots::$table, ['name' => 'Cars']); + + $category2 = CategoriesManyRoots::findFirst(2); + + expect($category2->root)->equals(2); + expect($category2->lft)->equals(1); + expect($category2->rgt)->equals(2); + expect($category2->level)->equals(1); + + $category3 = new CategoriesManyRoots(); + $category3->name = 'Computers'; + $category3->saveNode(); + + $I->seeInDatabase(CategoriesManyRoots::$table, ['name' => 'Computers']); + + $category3 = CategoriesManyRoots::findFirst(3); + + expect($category3->root)->equals(3); + expect($category3->lft)->equals(1); + expect($category3->rgt)->equals(2); + expect($category3->level)->equals(1); + + $I->seeNumRecords(3, CategoriesManyRoots::$table); + + $this->checkIntegrity($category1->root); + $this->checkIntegrity($category2->root); + $this->checkIntegrity($category3->root); + } + ); + } + + /** + * Creating more than one root by using one tree per table + * + * @author Serghei Iakovlev + * @since 2016-02-28 + */ + public function testShouldCatchExceptionWhenCreateARootNodeUsingOneTreePerTable() + { + $this->specify( + 'Test managed to create more than one root by using one tree per table', + function () { + $category = new CategoriesOneRoot(); + $category->name = 'Mobile Phones'; + $category->saveNode(); + + $category = new CategoriesOneRoot(); + $category->name = 'Computers'; + $category->saveNode(); + }, [ + 'throws' => [ + 'Phalcon\Mvc\Model\Exception', + 'Cannot create more than one root in single root mode.' + ] + ] + ); + } + + /** + * Getting all roots + * + * @author Serghei Iakovlev + * @since 2016-02-28 + */ + public function testShouldDetectRoots() + { + $this->specify( + "Model can't determine roots correctly", + function () { + expect((new CategoriesManyRoots)->roots())->count(0); + + $category1 = new CategoriesManyRoots(); + $category1->name = 'Mobile Phones'; + $category1->saveNode(); + + expect($category1->roots())->count(1); + + $category2 = new CategoriesManyRoots(); + $category2->name = 'Cars'; + $category2->saveNode(); + + expect($category2->roots())->count(2); + expect($category2->roots())->isInstanceOf('Phalcon\Mvc\Model\Resultset\Simple'); + + $this->checkIntegrity($category1->root); + $this->checkIntegrity($category2->root); + } + ); + } + + /** + * Add nodes to the tree + * + * @author Serghei Iakovlev + * @since 2016-02-28 + */ + public function testShouldAddChildNodes() + { + $this->specify( + 'Unable to add nodes to the tree correctly', + function () { + $cars = new CategoriesManyRoots(); + $cars->name = 'Cars'; + $cars->saveNode(); + + $ford = new CategoriesManyRoots(); + $ford->name = 'Ford'; + + $mercedes = new CategoriesManyRoots(); + $mercedes->name = 'Mercedes'; + + $audi = new CategoriesManyRoots(); + $audi->name = 'Audi'; + + $ford->appendTo($cars); + $mercedes->insertAfter($ford); + $audi->insertBefore($ford); + + $phones = new CategoriesManyRoots(); + $phones->name = 'Mobile Phones'; + $phones->saveNode(); + + $expected = [ + 'Cars', + ' Audi', + ' Ford', + ' Mercedes', + 'Mobile Phones', + ]; + + expect($this->prettifyRoots())->equals($expected); + + $this->checkIntegrity($cars->root); + $this->checkIntegrity($phones->root); + } + ); + } + + /** + * Move node as first + * + * @author Serghei Iakovlev + * @since 2016-02-28 + */ + public function testShouldMoveNodeAsFirst() + { + $this->specify( + 'Unable to move nodes correctly by using moveAsFirst', + function () { + $this->createTree(); + + $mercedes = CategoriesManyRoots::findFirst(3); + $samsung = CategoriesManyRoots::findFirst(6); + + $x100 = new CategoriesManyRoots(); + $x100->name = 'X100'; + $x100->appendTo($mercedes); + + $c200 = new CategoriesManyRoots(); + $c200->name = 'C200'; + $c200->prependTo($mercedes); + + $expected = [ + 'Cars', + ' Audi', + ' Ford', + ' Mercedes', + ' C200', + ' X100', + 'Mobile Phones', + ' iPhone', + ' Samsung', + ' Motorola', + ]; + + expect($this->prettifyRoots())->equals($expected); + + $c200->moveAsFirst($samsung); + $x100->moveAsFirst($samsung); + + $expected = [ + 'Cars', + ' Audi', + ' Ford', + ' Mercedes', + 'Mobile Phones', + ' iPhone', + ' Samsung', + ' X100', + ' C200', + ' Motorola', + ]; + + expect($this->prettifyRoots())->equals($expected); + + $this->checkIntegrity(CategoriesManyRoots::findFirst(1)->root); // cars + $this->checkIntegrity(CategoriesManyRoots::findFirst(5)->root); // phones + } + ); + } +} diff --git a/tests/unit/Mvc/Model/Behavior/Stubs/Categories.php b/tests/unit/Mvc/Model/Behavior/Stubs/Categories.php new file mode 100644 index 000000000..6b896c5fe --- /dev/null +++ b/tests/unit/Mvc/Model/Behavior/Stubs/Categories.php @@ -0,0 +1,72 @@ +setSource(self::$table); + $this->addBehavior(new NestedSetBehavior([ + 'hasManyRoots' => false, + ])); + } +} + +/** + * Class CategoriesManyRoots + * + * @method Resultset\Simple roots() + * @method boolean saveNode(array $attributes = null, array $whiteList = null) + * @method boolean appendTo(ModelInterface $target, array $attributes = null) + * @method boolean insertAfter(ModelInterface $target, array $attributes = null) + * @method boolean insertBefore(ModelInterface $target, array $attributes = null) + * @method boolean prependTo(ModelInterface $target, array $attributes = null) + * @method boolean moveAsFirst(ModelInterface $target) + * @method boolean moveBefore(ModelInterface $target) + * + * @property int id + * @property string name + * @property string description + * @property int root + * @property int lft + * @property int rgt + * @property int level + */ +class CategoriesManyRoots extends Model +{ + public static $table = 'categories'; + + public function initialize() + { + $this->setSource(self::$table); + $this->addBehavior(new NestedSetBehavior([ + 'hasManyRoots' => true, + ])); + } +} From fccdd1c48b0b46f7cde2687762ec4948813ff33f Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Tue, 1 Mar 2016 01:17:56 +0200 Subject: [PATCH 07/17] Fixed issue #513 --- .../Phalcon/Mvc/Model/Behavior/NestedSet.php | 6 +++ tests/unit/Mvc/Model/Behavior/Helper.php | 1 + .../unit/Mvc/Model/Behavior/NestedSetTest.php | 41 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php index a73f9fe4e..59a22abe3 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php +++ b/Library/Phalcon/Mvc/Model/Behavior/NestedSet.php @@ -334,6 +334,9 @@ public function next() */ public function prependTo(ModelInterface $target, array $attributes = null) { + // Re-search $target + $target = $target::findFirst($target->{$this->primaryKey}); + return $this->addNode($target, $target->{$this->leftAttribute} + 1, 1, $attributes); } @@ -358,6 +361,9 @@ public function prepend(ModelInterface $target, array $attributes = null) */ public function appendTo(ModelInterface $target, array $attributes = null) { + // Re-search $target + $target = $target::findFirst($target->{$this->primaryKey}); + return $this->addNode($target, $target->{$this->rightAttribute}, 1, $attributes); } diff --git a/tests/unit/Mvc/Model/Behavior/Helper.php b/tests/unit/Mvc/Model/Behavior/Helper.php index 1ccc5611f..859c26e38 100644 --- a/tests/unit/Mvc/Model/Behavior/Helper.php +++ b/tests/unit/Mvc/Model/Behavior/Helper.php @@ -211,6 +211,7 @@ protected function createTree() $ford->appendTo($cars); $mercedes->insertAfter($ford); $audi->insertBefore($ford); + $phones = new CategoriesManyRoots(); $phones->name = 'Mobile Phones'; $phones->saveNode(); diff --git a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php index b3ec8cef9..50ecbca9e 100644 --- a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php +++ b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php @@ -251,6 +251,47 @@ function () { ); } + /** + * Created nodes in the desired place + * + * @author Serghei Iakovlev + * @since 2016-03-01 + * @issue 513 + */ + public function testShouldAddBelowAndAbove() + { + $this->specify( + 'Unable to created nodes in the desired place', + function () { + $root = new CategoriesManyRoots; + $root->name = 'ROOT'; + $root->saveNode(); + + $node1 = new CategoriesManyRoots; + $node1->name = 'A'; + $node1->appendTo($root); + + $node2 = new CategoriesManyRoots; + $node2->name = 'B'; + $node2->appendTo($root); + + $node3 = new CategoriesManyRoots; + $node3->name = 'C'; + $node3->prependTo($root); + + $expected = [ + 'ROOT', + ' C', + ' A', + ' B', + ]; + + expect($this->prettifyRoots())->equals($expected); + $this->checkIntegrity($root->root); + } + ); + } + /** * Move node as first * From 405c457a0f51867bb2a97b9606bec8547e9adc82 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Tue, 1 Mar 2016 03:07:37 +0200 Subject: [PATCH 08/17] Amended NestedSet doc [ci skip] --- Library/Phalcon/Mvc/Model/Behavior/README.md | 14 ++++++++++++++ tests/unit/Mvc/Model/Behavior/NestedSetTest.php | 1 + 2 files changed, 15 insertions(+) diff --git a/Library/Phalcon/Mvc/Model/Behavior/README.md b/Library/Phalcon/Mvc/Model/Behavior/README.md index 290c29004..df6baea5c 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/README.md +++ b/Library/Phalcon/Mvc/Model/Behavior/README.md @@ -380,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 diff --git a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php index 50ecbca9e..3a2427554 100644 --- a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php +++ b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php @@ -297,6 +297,7 @@ function () { * * @author Serghei Iakovlev * @since 2016-02-28 + * @issue 534 */ public function testShouldMoveNodeAsFirst() { From 98320d696137593b51469ea02e8b6345b9b2ee56 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Wed, 2 Mar 2016 02:33:46 +0200 Subject: [PATCH 09/17] Test for #535 --- .../unit/Mvc/Model/Behavior/NestedSetTest.php | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php index 3a2427554..35f68b0d4 100644 --- a/tests/unit/Mvc/Model/Behavior/NestedSetTest.php +++ b/tests/unit/Mvc/Model/Behavior/NestedSetTest.php @@ -355,4 +355,68 @@ function () { } ); } + + /** + * Move nodes between trees + * + * @author Serghei Iakovlev + * @since 2016-03-02 + * @issue 535 + */ + public function testShouldMoveNodesBetweenTrees() + { + $this->specify( + 'Unable to move nodes between trees', + function () { + $this->createTree(); + + $samsung = CategoriesManyRoots::findFirst(6); + + $x100 = new CategoriesManyRoots(); + $x100->name = 'X100'; + $x100->appendTo($samsung); + + $c200 = new CategoriesManyRoots(); + $c200->name = 'C200'; + $c200->prependTo($samsung); + + $expected = [ + 'Cars', + ' Audi', + ' Ford', + ' Mercedes', + 'Mobile Phones', + ' iPhone', + ' Samsung', + ' C200', + ' X100', + ' Motorola', + ]; + + expect($this->prettifyRoots())->equals($expected); + + $cars = CategoriesManyRoots::findFirst(1); + $motorola = CategoriesManyRoots::findFirst(7); + + $cars->moveAsFirst($motorola); + + $expected = [ + 'Mobile Phones', + ' iPhone', + ' Samsung', + ' C200', + ' X100', + ' Motorola', + ' Cars', + ' Audi', + ' Ford', + ' Mercedes', + ]; + + expect($this->prettifyRoots())->equals($expected); + + $this->checkIntegrity(CategoriesManyRoots::findFirst(5)->root); // phones + } + ); + } } From e5c3f7ccd1a6aa8e3b7a1ecee624d01c2a2d6e5e Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Fri, 4 Mar 2016 15:16:06 +0200 Subject: [PATCH 10/17] Fixed MysqlExtended::getSqlExpression decalaration --- Library/Phalcon/Db/Dialect/MysqlExtended.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Phalcon/Db/Dialect/MysqlExtended.php b/Library/Phalcon/Db/Dialect/MysqlExtended.php index 2e75a638f..7d92ce84c 100644 --- a/Library/Phalcon/Db/Dialect/MysqlExtended.php +++ b/Library/Phalcon/Db/Dialect/MysqlExtended.php @@ -36,7 +36,7 @@ class MysqlExtended extends Mysql * @return string * @throws \Exception */ - public function getSqlExpression(array $expression, $escapeChar = null) + public function getSqlExpression(array $expression, $escapeChar = null, $bindCounts = null) { if ($expression["type"] == 'functionCall') { switch ($expression["name"]) { From 04cb2da17893a6a0e36c6ab8f7da3f41f77b2c08 Mon Sep 17 00:00:00 2001 From: David Hubner Date: Wed, 30 Mar 2016 23:32:02 +0200 Subject: [PATCH 11/17] New validators --- .../Validation/Validator/ConfirmationOf.php | 79 ++++++++++++ .../Validation/Validator/PasswordStrength.php | 111 +++++++++++++++++ README.md | 2 + .../Validator/ConfirmationOfTest.php | 94 +++++++++++++++ .../Validator/PasswordStrengthTest.php | 114 ++++++++++++++++++ 5 files changed, 400 insertions(+) create mode 100644 Library/Phalcon/Validation/Validator/ConfirmationOf.php create mode 100644 Library/Phalcon/Validation/Validator/PasswordStrength.php create mode 100644 tests/unit/Validation/Validator/ConfirmationOfTest.php create mode 100644 tests/unit/Validation/Validator/PasswordStrengthTest.php diff --git a/Library/Phalcon/Validation/Validator/ConfirmationOf.php b/Library/Phalcon/Validation/Validator/ConfirmationOf.php new file mode 100644 index 000000000..9ed5aed33 --- /dev/null +++ b/Library/Phalcon/Validation/Validator/ConfirmationOf.php @@ -0,0 +1,79 @@ + | + +------------------------------------------------------------------------+ + */ + +namespace Phalcon\Validation\Validator; + +use Phalcon\Validation, + Phalcon\Validation\Validator, + Phalcon\Validation\Exception; + +/** + * Validates confirmation of other field value + * + * + * new \Phalcon\Validation\Validator\ConfirmationOf([ + * 'origField' => {string - original field attribute}, + * 'message' => {string - validation message}, + * 'allowEmpty' => {bool - allow empty value} + * ]) + * + * + * @package Phalcon\Validation\Validator + */ +class ConfirmationOf extends Validator +{ + + /** + * Value validation + * + * @param \Phalcon\Validation $validation - validation object + * @param string $attribute - validated attribute + * @return bool + * @throws \Phalcon\Validation\Exception + */ + public function validate(Validation $validation, $attribute) + { + if (!$this->hasOption('origField')) { + throw new Exception('Original field must be set'); + } + + $allowEmpty = $this->getOption('allowEmpty'); + $value = $validation->getValue($attribute); + + if ($allowEmpty && ((is_scalar($value) && (string) $value === '') || is_null($value))) { + return true; + } + + $origField = $this->getOption('origField'); + $origValue = $validation->getValue($origField); + + if (is_string($value) && $value == $origValue) { + return true; + } + + $message = ($this->hasOption('message') ? $this->getOption('message') : 'Value not confirmed'); + + $validation->appendMessage( + new Validation\Message($message, $attribute, 'ConfirmationOfValidator') + ); + + return false; + } + +} diff --git a/Library/Phalcon/Validation/Validator/PasswordStrength.php b/Library/Phalcon/Validation/Validator/PasswordStrength.php new file mode 100644 index 000000000..28af0ed56 --- /dev/null +++ b/Library/Phalcon/Validation/Validator/PasswordStrength.php @@ -0,0 +1,111 @@ + | + +------------------------------------------------------------------------+ + */ + +namespace Phalcon\Validation\Validator; + +use Phalcon\Validation; + +/** + * Validates password strength + * + * + * new \Phalcon\Validation\Validator\PasswordStrength([ + * 'minScore' => {[1-4] - minimal password score}, + * 'message' => {string - validation message}, + * 'allowEmpty' => {bool - allow empty value} + * ]) + * + * + * @package Phalcon\Validation\Validator + */ +class PasswordStrength extends Validation\Validator +{ + + const MIN_VALID_SCORE = 2; + + /** + * Value validation + * + * @param \Phalcon\Validation $validation - validation object + * @param string $attribute - validated attribute + * @return bool + */ + public function validate(Validation $validation, $attribute) + { + $allowEmpty = $this->getOption('allowEmpty'); + $value = $validation->getValue($attribute); + + if ($allowEmpty && ((is_scalar($value) && (string) $value === '') || is_null($value))) { + return true; + } + + $minScore = ($this->hasOption('minScore') ? $this->getOption('minScore') : self::MIN_VALID_SCORE); + + if (is_string($value) && $this->_countScore($value) >= $minScore) { + return true; + } + + $message = ($this->hasOption('message') ? $this->getOption('message') : 'Password too weak'); + + $validation->appendMessage( + new Validation\Message($message, $attribute, 'PasswordStrengthValidator') + ); + + return false; + } + + /** + * Calculates password strength score + * + * @param string $value - password + * @return int (1 = very weak, 2 = weak, 3 = medium, 4+ = strong) + */ + private function _countScore($value) + { + $score = 0; + $hasLower = preg_match('![a-z]!', $value); + $hasUpper = preg_match('![A-Z]!', $value); + $hasNumber = preg_match('![0-9]!', $value); + + if ($hasLower && $hasUpper) { + ++$score; + } + if (($hasNumber && $hasLower) || ($hasNumber && $hasUpper)) { + ++$score; + } + if (preg_match('![^0-9a-zA-Z]!', $value)) { + ++$score; + } + + $length = mb_strlen($value); + + if ($length >= 16) { + $score += 2; + } elseif ($length >= 8) { + ++$score; + } elseif ($length <= 4 && $score > 1) { + --$score; + } elseif ($length > 0 && $score === 0) { + ++$score; + } + + return $score; + } + +} diff --git a/README.md b/README.md index f5abc6e2c..f22543091 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,9 @@ See [CONTRIBUTING.md](docs/CONTRIBUTING.md) * [Phalcon\Avatar\Gravatar](Library/Phalcon/Avatar) - Provides an easy way to retrieve a user's profile image from Gravatar site based on a given email address (@sergeyklay) ### Validators +* [Phalcon\Validation\Validator\ConfirmationOf](Library/Phalcon/Validation/Validator) - Validates confirmation of other field value (@davihu) * [Phalcon\Validation\Validator\MongoId](Library/Phalcon/Validation/Validator) - Validate MongoId value (@Kachit) +* [Phalcon\Validation\Validator\PasswordStrength](Library/Phalcon/Validation/Validator) - Validates password strength (@davihu) ## License diff --git a/tests/unit/Validation/Validator/ConfirmationOfTest.php b/tests/unit/Validation/Validator/ConfirmationOfTest.php new file mode 100644 index 000000000..1b5351a39 --- /dev/null +++ b/tests/unit/Validation/Validator/ConfirmationOfTest.php @@ -0,0 +1,94 @@ + | + +------------------------------------------------------------------------+ + */ + +namespace Phalcon\Test\Validation\Validator; + +use Phalcon\Validation\Validator\ConfirmationOf, + Codeception\TestCase\Test, + Codeception\Util\Stub; + +class ConfirmationOfTest extends Test +{ + + protected function _before() + { + + } + + protected function _after() + { + + } + + public function testValidateExceptionWithoutOrigField() + { + $validation = Stub::make('Phalcon\Validation'); + $validator = new ConfirmationOf(); + $this->setExpectedException('Phalcon\Validation\Exception'); + $validator->validate($validation, 'confirmation'); + } + + public function testValidateSameAsOrig() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => 'value')); + $validator = new ConfirmationOf(array( + 'origField' => 'original' + )); + $this->assertTrue($validator->validate($validation, 'confirmation')); + } + + public function testValidateNotSameAsOrig() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => Stub::consecutive('val1', 'val2'), 'appendMessage' => true)); + $validator = new ConfirmationOf(array( + 'origField' => 'original' + )); + $this->assertFalse($validator->validate($validation, 'confirmation')); + } + + public function testValidateAllowEmpty() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => Stub::consecutive('', 'val2'))); + $validator = new ConfirmationOf(array( + 'origField' => 'original', + 'allowEmpty' => true + )); + $this->assertTrue($validator->validate($validation, 'confirmation')); + } + + public function testValidateNotAllowEmpty() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => Stub::consecutive('', 'val2'), 'appendMessage' => true)); + $validator = new ConfirmationOf(array( + 'origField' => 'original', + 'allowEmpty' => false + )); + $this->assertFalse($validator->validate($validation, 'confirmation')); + } + + public function testValidateInvalidValue() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => array('value', 'value'), 'appendMessage' => true)); + $validator = new ConfirmationOf(array( + 'origField' => 'original' + )); + $this->assertFalse($validator->validate($validation, 'confirmation')); + } + +} diff --git a/tests/unit/Validation/Validator/PasswordStrengthTest.php b/tests/unit/Validation/Validator/PasswordStrengthTest.php new file mode 100644 index 000000000..f7592258b --- /dev/null +++ b/tests/unit/Validation/Validator/PasswordStrengthTest.php @@ -0,0 +1,114 @@ + | + +------------------------------------------------------------------------+ + */ + +namespace Phalcon\Test\Validation\Validator; + +use Phalcon\Validation\Validator\PasswordStrength, + Codeception\TestCase\Test, + Codeception\Util\Stub; + +class PasswordStrengthTest extends Test +{ + + protected function _before() + { + + } + + protected function _after() + { + + } + + public function testValidateWeakOnDefaultScore() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => 'Weak1')); + $validator = new PasswordStrength(); + $this->assertTrue($validator->validate($validation, 'password')); + } + + public function testValidateVeryWeakOnDefaultScore() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => '12345', 'appendMessage' => true)); + $validator = new PasswordStrength(); + $this->assertFalse($validator->validate($validation, 'password')); + } + + public function testValidateMediumOnScore3() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => 'Medium99')); + $validator = new PasswordStrength(array( + 'minScore' => 3 + )); + $this->assertTrue($validator->validate($validation, 'password')); + } + + public function testValidateWeakOnScore3() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => 'Weak1', 'appendMessage' => true)); + $validator = new PasswordStrength(array( + 'minScore' => 3 + )); + $this->assertFalse($validator->validate($validation, 'password')); + } + + public function testValidateStrongOnScore4() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => 'Strong-9')); + $validator = new PasswordStrength(array( + 'minScore' => 4 + )); + $this->assertTrue($validator->validate($validation, 'password')); + } + + public function testValidateMediumOnScore4() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => 'Medium99', 'appendMessage' => true)); + $validator = new PasswordStrength(array( + 'minScore' => 4 + )); + $this->assertFalse($validator->validate($validation, 'password')); + } + + public function testValidateAllowEmpty() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => '')); + $validator = new PasswordStrength(array( + 'allowEmpty' => true + )); + $this->assertTrue($validator->validate($validation, 'password')); + } + + public function testValidateNotAllowEmpty() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => '', 'appendMessage' => true)); + $validator = new PasswordStrength(array( + 'allowEmpty' => false + )); + $this->assertFalse($validator->validate($validation, 'password')); + } + + public function testValidateInvalidValue() + { + $validation = Stub::make('Phalcon\Validation', array('getValue' => array('value', 'value'), 'appendMessage' => true)); + $validator = new PasswordStrength(); + $this->assertFalse($validator->validate($validation, 'password')); + } + +} From de83f6428cfba3192b0b977d6e851f7bc0ed06e8 Mon Sep 17 00:00:00 2001 From: David Hubner Date: Thu, 31 Mar 2016 00:22:55 +0200 Subject: [PATCH 12/17] Coding standard fixes --- .../Phalcon/Validation/Validator/ConfirmationOf.php | 10 +++++----- .../Validation/Validator/PasswordStrength.php | 4 ++-- .../unit/Validation/Validator/ConfirmationOfTest.php | 12 ++++++------ .../Validation/Validator/PasswordStrengthTest.php | 12 ++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Library/Phalcon/Validation/Validator/ConfirmationOf.php b/Library/Phalcon/Validation/Validator/ConfirmationOf.php index 9ed5aed33..00f9268e7 100644 --- a/Library/Phalcon/Validation/Validator/ConfirmationOf.php +++ b/Library/Phalcon/Validation/Validator/ConfirmationOf.php @@ -13,15 +13,15 @@ | obtain it through the world-wide-web, please send an email | | to license@phalconphp.com so we can send you a copy immediately. | +------------------------------------------------------------------------+ - | Authors: David Hubner | + | Authors: David Hubner | +------------------------------------------------------------------------+ */ namespace Phalcon\Validation\Validator; -use Phalcon\Validation, - Phalcon\Validation\Validator, - Phalcon\Validation\Exception; +use Phalcon\Validation; +use Phalcon\Validation\Exception; +use Phalcon\Validation\Validator; /** * Validates confirmation of other field value @@ -33,7 +33,7 @@ * 'allowEmpty' => {bool - allow empty value} * ]) * - * + * * @package Phalcon\Validation\Validator */ class ConfirmationOf extends Validator diff --git a/Library/Phalcon/Validation/Validator/PasswordStrength.php b/Library/Phalcon/Validation/Validator/PasswordStrength.php index 28af0ed56..0f7ba9950 100644 --- a/Library/Phalcon/Validation/Validator/PasswordStrength.php +++ b/Library/Phalcon/Validation/Validator/PasswordStrength.php @@ -13,7 +13,7 @@ | obtain it through the world-wide-web, please send an email | | to license@phalconphp.com so we can send you a copy immediately. | +------------------------------------------------------------------------+ - | Authors: David Hubner | + | Authors: David Hubner | +------------------------------------------------------------------------+ */ @@ -31,7 +31,7 @@ * 'allowEmpty' => {bool - allow empty value} * ]) * - * + * * @package Phalcon\Validation\Validator */ class PasswordStrength extends Validation\Validator diff --git a/tests/unit/Validation/Validator/ConfirmationOfTest.php b/tests/unit/Validation/Validator/ConfirmationOfTest.php index 1b5351a39..5af491e3a 100644 --- a/tests/unit/Validation/Validator/ConfirmationOfTest.php +++ b/tests/unit/Validation/Validator/ConfirmationOfTest.php @@ -13,27 +13,27 @@ | obtain it through the world-wide-web, please send an email | | to license@phalconphp.com so we can send you a copy immediately. | +------------------------------------------------------------------------+ - | Authors: David Hubner | + | Authors: David Hubner | +------------------------------------------------------------------------+ */ namespace Phalcon\Test\Validation\Validator; -use Phalcon\Validation\Validator\ConfirmationOf, - Codeception\TestCase\Test, - Codeception\Util\Stub; +use Codeception\TestCase\Test; +use Codeception\Util\Stub; +use Phalcon\Validation\Validator\ConfirmationOf; class ConfirmationOfTest extends Test { protected function _before() { - + } protected function _after() { - + } public function testValidateExceptionWithoutOrigField() diff --git a/tests/unit/Validation/Validator/PasswordStrengthTest.php b/tests/unit/Validation/Validator/PasswordStrengthTest.php index f7592258b..140f70677 100644 --- a/tests/unit/Validation/Validator/PasswordStrengthTest.php +++ b/tests/unit/Validation/Validator/PasswordStrengthTest.php @@ -13,27 +13,27 @@ | obtain it through the world-wide-web, please send an email | | to license@phalconphp.com so we can send you a copy immediately. | +------------------------------------------------------------------------+ - | Authors: David Hubner | + | Authors: David Hubner | +------------------------------------------------------------------------+ */ namespace Phalcon\Test\Validation\Validator; -use Phalcon\Validation\Validator\PasswordStrength, - Codeception\TestCase\Test, - Codeception\Util\Stub; +use Codeception\TestCase\Test; +use Codeception\Util\Stub; +use Phalcon\Validation\Validator\PasswordStrength; class PasswordStrengthTest extends Test { protected function _before() { - + } protected function _after() { - + } public function testValidateWeakOnDefaultScore() From 8122d76657ad14629742ecd92cdcf91d8c0d2d57 Mon Sep 17 00:00:00 2001 From: David Hubner Date: Thu, 31 Mar 2016 08:32:56 +0200 Subject: [PATCH 13/17] Applied php-cf --- Library/Phalcon/Validation/Validator/ConfirmationOf.php | 1 - Library/Phalcon/Validation/Validator/PasswordStrength.php | 1 - tests/unit/Validation/Validator/ConfirmationOfTest.php | 3 --- tests/unit/Validation/Validator/PasswordStrengthTest.php | 3 --- 4 files changed, 8 deletions(-) diff --git a/Library/Phalcon/Validation/Validator/ConfirmationOf.php b/Library/Phalcon/Validation/Validator/ConfirmationOf.php index 00f9268e7..461599074 100644 --- a/Library/Phalcon/Validation/Validator/ConfirmationOf.php +++ b/Library/Phalcon/Validation/Validator/ConfirmationOf.php @@ -75,5 +75,4 @@ public function validate(Validation $validation, $attribute) return false; } - } diff --git a/Library/Phalcon/Validation/Validator/PasswordStrength.php b/Library/Phalcon/Validation/Validator/PasswordStrength.php index 0f7ba9950..fc0f6db78 100644 --- a/Library/Phalcon/Validation/Validator/PasswordStrength.php +++ b/Library/Phalcon/Validation/Validator/PasswordStrength.php @@ -107,5 +107,4 @@ private function _countScore($value) return $score; } - } diff --git a/tests/unit/Validation/Validator/ConfirmationOfTest.php b/tests/unit/Validation/Validator/ConfirmationOfTest.php index 5af491e3a..c43b41e81 100644 --- a/tests/unit/Validation/Validator/ConfirmationOfTest.php +++ b/tests/unit/Validation/Validator/ConfirmationOfTest.php @@ -28,12 +28,10 @@ class ConfirmationOfTest extends Test protected function _before() { - } protected function _after() { - } public function testValidateExceptionWithoutOrigField() @@ -90,5 +88,4 @@ public function testValidateInvalidValue() )); $this->assertFalse($validator->validate($validation, 'confirmation')); } - } diff --git a/tests/unit/Validation/Validator/PasswordStrengthTest.php b/tests/unit/Validation/Validator/PasswordStrengthTest.php index 140f70677..0be5bbf34 100644 --- a/tests/unit/Validation/Validator/PasswordStrengthTest.php +++ b/tests/unit/Validation/Validator/PasswordStrengthTest.php @@ -28,12 +28,10 @@ class PasswordStrengthTest extends Test protected function _before() { - } protected function _after() { - } public function testValidateWeakOnDefaultScore() @@ -110,5 +108,4 @@ public function testValidateInvalidValue() $validator = new PasswordStrength(); $this->assertFalse($validator->validate($validation, 'password')); } - } From 87c4e2ab1416305fb20e987f2cb5332def201e29 Mon Sep 17 00:00:00 2001 From: David Hubner Date: Fri, 1 Apr 2016 10:14:25 +0200 Subject: [PATCH 14/17] Removed underscore --- Library/Phalcon/Validation/Validator/PasswordStrength.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Phalcon/Validation/Validator/PasswordStrength.php b/Library/Phalcon/Validation/Validator/PasswordStrength.php index fc0f6db78..c86ee8188 100644 --- a/Library/Phalcon/Validation/Validator/PasswordStrength.php +++ b/Library/Phalcon/Validation/Validator/PasswordStrength.php @@ -57,7 +57,7 @@ public function validate(Validation $validation, $attribute) $minScore = ($this->hasOption('minScore') ? $this->getOption('minScore') : self::MIN_VALID_SCORE); - if (is_string($value) && $this->_countScore($value) >= $minScore) { + if (is_string($value) && $this->countScore($value) >= $minScore) { return true; } @@ -76,7 +76,7 @@ public function validate(Validation $validation, $attribute) * @param string $value - password * @return int (1 = very weak, 2 = weak, 3 = medium, 4+ = strong) */ - private function _countScore($value) + private function countScore($value) { $score = 0; $hasLower = preg_match('![a-z]!', $value); From 6ca5a550af8550c128a76e782c406fb2dfb29845 Mon Sep 17 00:00:00 2001 From: Mitch Macpherson Date: Fri, 15 Apr 2016 12:08:15 +1000 Subject: [PATCH 15/17] ACL Database adapter will now work with wildcards for the role/resource (or both) with the 'allow' method #568 --- Library/Phalcon/Acl/Adapter/Database.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Library/Phalcon/Acl/Adapter/Database.php b/Library/Phalcon/Acl/Adapter/Database.php index 51a896202..aea1a31b7 100644 --- a/Library/Phalcon/Acl/Adapter/Database.php +++ b/Library/Phalcon/Acl/Adapter/Database.php @@ -438,14 +438,16 @@ public function isAllowed($role, $resource, $access) protected function insertOrUpdateAccess($roleName, $resourceName, $accessName, $action) { /** - * Check if the access is valid in the resource + * Check if the access is valid in the resource unless wildcard */ - $sql = "SELECT COUNT(*) FROM {$this->resourcesAccesses} WHERE resources_name = ? AND access_name = ?"; - $exists = $this->connection->fetchOne($sql, null, [$resourceName, $accessName]); - if (!$exists[0]) { - throw new Exception( - "Access '{$accessName}' does not exist in resource '{$resourceName}' in ACL" - ); + if ($resourceName !== '*' && $accessName !== '*') { + $sql = "SELECT COUNT(*) FROM {$this->resourcesAccesses} WHERE resources_name = ? AND access_name = ?"; + $exists = $this->connection->fetchOne($sql, null, [$resourceName, $accessName]); + if (!$exists[0]) { + throw new Exception( + "Access '{$accessName}' does not exist in resource '{$resourceName}' in ACL" + ); + } } /** From 823bf4691ad9a0e3fedfa8a12661c9bffbf62585 Mon Sep 17 00:00:00 2001 From: Mitch Macpherson Date: Mon, 18 Apr 2016 10:44:38 +1000 Subject: [PATCH 16/17] SQL based ACL adapters now return the correct 'isAllowed' result under certain circumstances --- Library/Phalcon/Acl/Adapter/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Phalcon/Acl/Adapter/Database.php b/Library/Phalcon/Acl/Adapter/Database.php index aea1a31b7..7e7e3e449 100644 --- a/Library/Phalcon/Acl/Adapter/Database.php +++ b/Library/Phalcon/Acl/Adapter/Database.php @@ -407,7 +407,7 @@ public function isAllowed($role, $resource, $access) // access_name should be given one or 'any' "AND access_name IN (?, '*')", // order be the sum of bools for 'literals' before 'any' - "ORDER BY (roles_name != '*')+(resources_name != '*')+(access_name != '*') DESC", + "ORDER BY ".$this->connection->escapeIdentifier('allowed')." DESC", // get only one... 'LIMIT 1' ]); From b54434d0a19b8d76e321e1e3c850774e999a59d2 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Wed, 4 May 2016 23:49:54 +0300 Subject: [PATCH 17/17] Added support for testing Phalcon v2.0.11 --- .travis.yml | 1 + README.md | 2 +- composer.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7116ace32..11aa5f2ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ env: - TEST_DB_NAME="incubator_tests" - TEST_DB_CHARSET="utf8" matrix: + - PHALCON_VERSION="2.0.11" - PHALCON_VERSION="2.0.10" - PHALCON_VERSION="2.0.9" - PHALCON_VERSION="2.0.8" diff --git a/README.md b/README.md index f22543091..6f3b529bd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Install Composer in a common location or in your project: curl -s http://getcomposer.org/installer | php ``` -If you are still using Phalcon 2.0.x, create the `composer.json` file as follows: +If you are using Phalcon 2.0.x, create the `composer.json` file as follows: ```json { diff --git a/composer.json b/composer.json index 101be97a6..7ff625ece 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, "require": { "php": ">=5.4", - "ext-phalcon": ">=2.0.4", + "ext-phalcon": "^2.0", "swiftmailer/swiftmailer": "~5.2" }, "require-dev": {