diff --git a/lib/Cake/Model/Model.php b/lib/Cake/Model/Model.php index cec7af7ab..1e4b0a953 100644 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -2812,44 +2812,10 @@ class Model extends Object implements CakeEventListener { if ($state === 'before') { return $query; } elseif ($state === 'after') { - $return = $idMap = array(); - $ids = Set::extract($results, '{n}.' . $this->alias . '.' . $this->primaryKey); - - if (isset($results[0][$this->alias]) && !array_key_exists('parent_id', $results[0][$this->alias])) { - trigger_error( - __d('cake_dev', 'You cannot use find("threaded") on models without a "parent_id" field.'), - E_USER_WARNING - ); - return $return; - } - - foreach ($results as $result) { - $result['children'] = array(); - $id = $result[$this->alias][$this->primaryKey]; - $parentId = $result[$this->alias]['parent_id']; - if (isset($idMap[$id]['children'])) { - $idMap[$id] = array_merge($result, (array)$idMap[$id]); - } else { - $idMap[$id] = array_merge($result, array('children' => array())); - } - if (!$parentId || !in_array($parentId, $ids)) { - $return[] =& $idMap[$id]; - } else { - $idMap[$parentId]['children'][] =& $idMap[$id]; - } - } - if (count($return) > 1) { - $ids = array_unique(Set::extract('/' . $this->alias . '/parent_id', $return)); - if (count($ids) > 1) { - $root = $return[0][$this->alias]['parent_id']; - foreach ($return as $key => $value) { - if ($value[$this->alias]['parent_id'] != $root) { - unset($return[$key]); - } - } - } - } - return $return; + return Set::nest($results, array( + 'idPath' => '/' . $this->alias . '/' . $this->primaryKey, + 'parentPath' => '/' . $this->alias . '/parent_id' + )); } } diff --git a/lib/Cake/Test/Case/Model/ModelReadTest.php b/lib/Cake/Test/Case/Model/ModelReadTest.php index 71072a6a9..8660714da 100644 --- a/lib/Cake/Test/Case/Model/ModelReadTest.php +++ b/lib/Cake/Test/Case/Model/ModelReadTest.php @@ -2990,18 +2990,6 @@ class ModelReadTest extends BaseModelTest { $this->assertEquals($afterFindData, $noAfterFindData); } -/** - * find(threaded) should trigger errors whne there is no parent_id field. - * - * @expectedException PHPUnit_Framework_Error_Warning - * @return void - */ - public function testFindThreadedError() { - $this->loadFixtures('Apple', 'Sample'); - $Apple = new Apple(); - $Apple->find('threaded'); - } - /** * testFindAllThreaded method * diff --git a/lib/Cake/Test/Case/Utility/SetTest.php b/lib/Cake/Test/Case/Utility/SetTest.php index b460da55b..276f6925b 100644 --- a/lib/Cake/Test/Case/Utility/SetTest.php +++ b/lib/Cake/Test/Case/Utility/SetTest.php @@ -3137,4 +3137,420 @@ class SetTest extends CakeTestCase { $expected = array('one' => array('a', 'b', 'c' => 'cee'), 'two' => 2, 'three' => null); $this->assertEquals($expected, $result); } + +/** + * test Set nest with a normal model result set. For kicks rely on Set nest detecting the key names + * automatically + * + * @return void + */ + public function testNestModel() { + $input = array( + array( + 'ModelName' => array( + 'id' => 1, + 'parent_id' => null + ), + ), + array( + 'ModelName' => array( + 'id' => 2, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 3, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 4, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 5, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 6, + 'parent_id' => null + ), + ), + array( + 'ModelName' => array( + 'id' => 7, + 'parent_id' => 6 + ), + ), + array( + 'ModelName' => array( + 'id' => 8, + 'parent_id' => 6 + ), + ), + array( + 'ModelName' => array( + 'id' => 9, + 'parent_id' => 6 + ), + ), + array( + 'ModelName' => array( + 'id' => 10, + 'parent_id' => 6 + ) + ) + ); + $expected = array( + array( + 'ModelName' => array( + 'id' => 1, + 'parent_id' => null + ), + 'children' => array( + array( + 'ModelName' => array( + 'id' => 2, + 'parent_id' => 1 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 3, + 'parent_id' => 1 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 4, + 'parent_id' => 1 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 5, + 'parent_id' => 1 + ), + 'children' => array() + ), + + ) + ), + array( + 'ModelName' => array( + 'id' => 6, + 'parent_id' => null + ), + 'children' => array( + array( + 'ModelName' => array( + 'id' => 7, + 'parent_id' => 6 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 8, + 'parent_id' => 6 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 9, + 'parent_id' => 6 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 10, + 'parent_id' => 6 + ), + 'children' => array() + ) + ) + ) + ); + $result = Set::nest($input); + $this->assertEquals($expected, $result); + } + +/** + * test Set nest with a normal model result set, and a nominated root id + * + * @return void + */ + public function testNestModelExplicitRoot() { + $input = array( + array( + 'ModelName' => array( + 'id' => 1, + 'parent_id' => null + ), + ), + array( + 'ModelName' => array( + 'id' => 2, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 3, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 4, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 5, + 'parent_id' => 1 + ), + ), + array( + 'ModelName' => array( + 'id' => 6, + 'parent_id' => null + ), + ), + array( + 'ModelName' => array( + 'id' => 7, + 'parent_id' => 6 + ), + ), + array( + 'ModelName' => array( + 'id' => 8, + 'parent_id' => 6 + ), + ), + array( + 'ModelName' => array( + 'id' => 9, + 'parent_id' => 6 + ), + ), + array( + 'ModelName' => array( + 'id' => 10, + 'parent_id' => 6 + ) + ) + ); + $expected = array( + array( + 'ModelName' => array( + 'id' => 6, + 'parent_id' => null + ), + 'children' => array( + array( + 'ModelName' => array( + 'id' => 7, + 'parent_id' => 6 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 8, + 'parent_id' => 6 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 9, + 'parent_id' => 6 + ), + 'children' => array() + ), + array( + 'ModelName' => array( + 'id' => 10, + 'parent_id' => 6 + ), + 'children' => array() + ) + ) + ) + ); + $result = Set::nest($input, array('root' => 6)); + $this->assertEquals($expected, $result); + } + +/** + * test Set nest with a 1d array - this method should be able to handle any type of array input + * + * @return void + */ + public function testNest1Dimensional() { + $input = array( + array( + 'id' => 1, + 'parent_id' => null + ), + array( + 'id' => 2, + 'parent_id' => 1 + ), + array( + 'id' => 3, + 'parent_id' => 1 + ), + array( + 'id' => 4, + 'parent_id' => 1 + ), + array( + 'id' => 5, + 'parent_id' => 1 + ), + array( + 'id' => 6, + 'parent_id' => null + ), + array( + 'id' => 7, + 'parent_id' => 6 + ), + array( + 'id' => 8, + 'parent_id' => 6 + ), + array( + 'id' => 9, + 'parent_id' => 6 + ), + array( + 'id' => 10, + 'parent_id' => 6 + ) + ); + $expected = array( + array( + 'id' => 1, + 'parent_id' => null, + 'children' => array( + array( + 'id' => 2, + 'parent_id' => 1, + 'children' => array() + ), + array( + 'id' => 3, + 'parent_id' => 1, + 'children' => array() + ), + array( + 'id' => 4, + 'parent_id' => 1, + 'children' => array() + ), + array( + 'id' => 5, + 'parent_id' => 1, + 'children' => array() + ), + + ) + ), + array( + 'id' => 6, + 'parent_id' => null, + 'children' => array( + array( + 'id' => 7, + 'parent_id' => 6, + 'children' => array() + ), + array( + 'id' => 8, + 'parent_id' => 6, + 'children' => array() + ), + array( + 'id' => 9, + 'parent_id' => 6, + 'children' => array() + ), + array( + 'id' => 10, + 'parent_id' => 6, + 'children' => array() + ) + ) + ) + ); + $result = Set::nest($input, array('idPath' => '/id', 'parentPath' => '/parent_id')); + $this->assertEquals($expected, $result); + } + +/** + * test Set nest with no specified parent data. + * + * The result should be the same as the input. + * For an easier comparison, unset all the empty children arrays from the result + * + * @return void + */ + public function testMissingParent() { + $input = array( + array( + 'id' => 1, + ), + array( + 'id' => 2, + ), + array( + 'id' => 3, + ), + array( + 'id' => 4, + ), + array( + 'id' => 5, + ), + array( + 'id' => 6, + ), + array( + 'id' => 7, + ), + array( + 'id' => 8, + ), + array( + 'id' => 9, + ), + array( + 'id' => 10, + ) + ); + + $result = Set::nest($input, array('idPath' => '/id', 'parentPath' => '/parent_id')); + foreach($result as &$row) { + if (empty($row['children'])) { + unset($row['children']); + } + } + $this->assertEquals($input, $result); + } } diff --git a/lib/Cake/Utility/Set.php b/lib/Cake/Utility/Set.php index a62726708..68e1eb077 100644 --- a/lib/Cake/Utility/Set.php +++ b/lib/Cake/Utility/Set.php @@ -1114,4 +1114,100 @@ class Set { } return null; } + +/** + * Takes in a flat array and returns a nested array + * + * @param mixed $data + * @param array $options Options are: + * children - the key name to use in the resultset for children + * idPath - the path to a key that identifies each entry + * parentPath - the path to a key that identifies the parent of each entry + * root - the id of the desired top-most result + * @return array of results, nested + * @link + */ + public static function nest($data, $options = array()) { + if (!$data) { + return $data; + } + + $alias = key(current($data)); + $options += array( + 'idPath' => "/$alias/id", + 'parentPath' => "/$alias/parent_id", + 'children' => 'children', + 'root' => null + ); + + $return = $idMap = array(); + $ids = Set::extract($data, $options['idPath']); + $idKeys = explode('/', trim($options['idPath'], '/')); + $parentKeys = explode('/', trim($options['parentPath'], '/')); + + foreach ($data as $result) { + $result[$options['children']] = array(); + + $id = Set::get($result, $idKeys); + $parentId = Set::get($result, $parentKeys); + + if (isset($idMap[$id][$options['children']])) { + $idMap[$id] = array_merge($result, (array)$idMap[$id]); + } else { + $idMap[$id] = array_merge($result, array($options['children'] => array())); + } + if (!$parentId || !in_array($parentId, $ids)) { + $return[] =& $idMap[$id]; + } else { + $idMap[$parentId][$options['children']][] =& $idMap[$id]; + } + } + + if ($options['root']) { + $root = $options['root']; + } else { + $root = Set::get($return[0], $parentKeys); + } + + foreach ($return as $i => $result) { + $id = Set::get($result, $idKeys); + $parentId = Set::get($result, $parentKeys); + if ($id !== $root && $parentId != $root) { + unset($return[$i]); + } + } + + return array_values($return); + } + +/** + * Return the value at the specified position + * + * @param mixed $input an array + * @param mixed $path string or array of array keys + * @return the value at the specified position or null if it doesn't exist + */ + public static function get($input, $path = null) { + if (is_string($path)) { + if (strpos($path, '/') !== false) { + $keys = explode('/', trim($path, '/')); + } else { + $keys = explode('.', trim($path, '.')); + } + } else { + $keys = $path; + } + if (!$keys) { + return $input; + } + + $return = $input; + foreach($keys as $key) { + if (!isset($return[$key])) { + return null; + } + $return = $return[$key]; + } + return $return; + } }