diff --git a/README.md b/README.md index bb2961b1c..d427d607d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # CakePHP -[![Bake Status](https://secure.travis-ci.org/cakephp/cakephp.png?branch=master)](http://travis-ci.org/cakephp/cakephp) [![Latest Stable Version](https://poser.pugx.org/cakephp/cakephp/v/stable.svg)](https://packagist.org/packages/cakephp/cakephp) [![License](https://poser.pugx.org/cakephp/cakephp/license.svg)](https://packagist.org/packages/cakephp/cakephp) +[![Bake Status](https://secure.travis-ci.org/cakephp/cakephp.png?branch=master)](http://travis-ci.org/cakephp/cakephp) +[![Code consistency](http://squizlabs.github.io/PHP_CodeSniffer/analysis/cakephp/cakephp/grade.svg)](http://squizlabs.github.io/PHP_CodeSniffer/analysis/cakephp/cakephp/) [![CakePHP](http://cakephp.org/img/cake-logo.png)](http://www.cakephp.org) diff --git a/lib/Cake/Controller/Controller.php b/lib/Cake/Controller/Controller.php index 6727434ae..c92467b05 100644 --- a/lib/Cake/Controller/Controller.php +++ b/lib/Cake/Controller/Controller.php @@ -611,7 +611,7 @@ class Controller extends Object implements CakeEventListener { /** * Returns a list of all events that will fire in the controller during its lifecycle. - * You can override this function to add you own listener callbacks + * You can override this function to add your own listener callbacks * * @return array */ diff --git a/lib/Cake/Core/App.php b/lib/Cake/Core/App.php index 05fa1393a..bb8369c25 100644 --- a/lib/Cake/Core/App.php +++ b/lib/Cake/Core/App.php @@ -33,7 +33,7 @@ App::uses('CakePlugin', 'Core'); * CakePHP is organized around the idea of packages, each class belongs to a package or folder where other * classes reside. You can configure each package location in your application using `App::build('APackage/SubPackage', $paths)` * to inform the framework where should each class be loaded. Almost every class in the CakePHP framework can be swapped - * by your own compatible implementation. If you wish to use you own class instead of the classes the framework provides, + * by your own compatible implementation. If you wish to use your own class instead of the classes the framework provides, * just add the class to your libs folder mocking the directory location of where CakePHP expects to find it. * * For instance if you'd like to use your own HttpSocket class, put it under diff --git a/lib/Cake/Model/Datasource/CakeSession.php b/lib/Cake/Model/Datasource/CakeSession.php index 96a66d899..6d7d53a20 100644 --- a/lib/Cake/Model/Datasource/CakeSession.php +++ b/lib/Cake/Model/Datasource/CakeSession.php @@ -378,7 +378,7 @@ class CakeSession { */ public static function read($name = null) { if (empty($name) && $name !== null) { - return false; + return null; } if (!self::_hasSession() || !self::start()) { return null; diff --git a/lib/Cake/Model/Datasource/DboSource.php b/lib/Cake/Model/Datasource/DboSource.php index 5377f969b..a60c4ee42 100644 --- a/lib/Cake/Model/Datasource/DboSource.php +++ b/lib/Cake/Model/Datasource/DboSource.php @@ -396,7 +396,7 @@ class DboSource extends DataSource { * * @param string $sql SQL statement * @param array $params Additional options for the query. - * @return bool + * @return mixed Resource or object representing the result set, or false on failure */ public function rawQuery($sql, $params = array()) { $this->took = $this->numRows = false; diff --git a/lib/Cake/Model/Model.php b/lib/Cake/Model/Model.php index bfe6f257a..cf112057e 100644 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -776,7 +776,7 @@ class Model extends Object implements CakeEventListener { /** * Returns a list of all events that will fire in the model during it's lifecycle. - * You can override this function to add you own listener callbacks + * You can override this function to add your own listener callbacks * * @return array */ @@ -3361,11 +3361,26 @@ class Model extends Object implements CakeEventListener { /** * Returns false if any fields passed match any (by default, all if $or = false) of their matching values. * + * Can be used as a validation method. When used as a validation method, the `$or` parameter + * contains an array of fields to be validated. + * * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data) - * @param bool $or If false, all fields specified must match in order for a false return value + * @param bool|array $or If false, all fields specified must match in order for a false return value * @return bool False if any records matching any fields are found */ public function isUnique($fields, $or = true) { + if (is_array($or)) { + $isRule = ( + array_key_exists('rule', $or) && + array_key_exists('required', $or) && + array_key_exists('message', $or) + ); + if (!$isRule) { + $args = func_get_args(); + $fields = $args[1]; + $or = isset($args[2]) ? $args[2] : true; + } + } if (!is_array($fields)) { $fields = func_get_args(); if (is_bool($fields[count($fields) - 1])) { diff --git a/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php b/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php index 7f70ea82c..534aaa1ed 100644 --- a/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php @@ -280,7 +280,7 @@ class CakeSessionTest extends CakeTestCase { * @return void */ public function testReadyEmpty() { - $this->assertFalse(TestCakeSession::read('')); + $this->assertNull(TestCakeSession::read('')); } /** diff --git a/lib/Cake/Test/Case/Model/ModelReadTest.php b/lib/Cake/Test/Case/Model/ModelReadTest.php index 294ce636f..b684f06d0 100644 --- a/lib/Cake/Test/Case/Model/ModelReadTest.php +++ b/lib/Cake/Test/Case/Model/ModelReadTest.php @@ -6550,12 +6550,43 @@ class ModelReadTest extends BaseModelTest { } } +/** + * Test that find() with array conditions works when there is only one element. + * + * @return void + */ + public function testFindAllArrayConditions() { + $this->loadFixtures('User'); + $TestModel = new User(); + $TestModel->cacheQueries = false; + + $result = $TestModel->find('all', array( + 'conditions' => array('User.id' => array(3)), + )); + $expected = array( + array( + 'User' => array( + 'id' => '3', + 'user' => 'larry', + 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', + 'created' => '2007-03-17 01:20:23', + 'updated' => '2007-03-17 01:22:31' + )) + ); + $this->assertEquals($expected, $result); + + $result = $TestModel->find('all', array( + 'conditions' => array('User.user' => array('larry')), + )); + $this->assertEquals($expected, $result); + } + /** * test find('list') method * * @return void */ - public function testGenerateFindList() { + public function testFindList() { $this->loadFixtures('Article', 'Apple', 'Post', 'Author', 'User', 'Comment'); $TestModel = new Article(); @@ -6825,6 +6856,32 @@ class ModelReadTest extends BaseModelTest { $this->assertEquals($expected, $result); } +/** + * Test that find(list) works with array conditions that have only one element. + * + * @return void + */ + public function testFindListArrayCondition() { + $this->loadFixtures('User'); + $TestModel = new User(); + $TestModel->cacheQueries = false; + + $result = $TestModel->find('list', array( + 'fields' => array('id', 'user'), + 'conditions' => array('User.id' => array(3)), + )); + $expected = array( + 3 => 'larry' + ); + $this->assertEquals($expected, $result); + + $result = $TestModel->find('list', array( + 'fields' => array('id', 'user'), + 'conditions' => array('User.user' => array('larry')), + )); + $this->assertEquals($expected, $result); + } + /** * testFindField method * diff --git a/lib/Cake/Test/Case/Model/ModelValidationTest.php b/lib/Cake/Test/Case/Model/ModelValidationTest.php index b04bbb46d..9ec889f80 100644 --- a/lib/Cake/Test/Case/Model/ModelValidationTest.php +++ b/lib/Cake/Test/Case/Model/ModelValidationTest.php @@ -2405,6 +2405,77 @@ class ModelValidationTest extends BaseModelTest { $this->assertEquals($expected, $result); } +/** + * Test the isUnique method when used as a validator for multiple fields. + * + * @return void + */ + public function testIsUniqueValidator() { + $this->loadFixtures('Article'); + $Article = ClassRegistry::init('Article'); + $Article->validate = array( + 'user_id' => array( + 'duplicate' => array( + 'rule' => array('isUnique', array('user_id', 'title'), false) + ) + ) + ); + $data = array( + 'user_id' => 1, + 'title' => 'First Article', + ); + $Article->create($data); + $this->assertFalse($Article->validates(), 'Contains a dupe'); + + $data = array( + 'user_id' => 1, + 'title' => 'Unique Article', + ); + $Article->create($data); + $this->assertTrue($Article->validates(), 'Should pass'); + + $Article->validate = array( + 'user_id' => array( + 'duplicate' => array( + 'rule' => array('isUnique', array('user_id', 'title')) + ) + ) + ); + $data = array( + 'user_id' => 1, + 'title' => 'Unique Article', + ); + $Article->create($data); + $this->assertFalse($Article->validates(), 'Should fail, conditions are combined with or'); + } + +/** + * Test backward compatibility of the isUnique method when used as a validator for a single field. + * + * @return void + */ + public function testBackwardCompatIsUniqueValidator() { + $this->loadFixtures('Article'); + $Article = ClassRegistry::init('Article'); + $Article->validate = array( + 'title' => array( + 'duplicate' => array( + 'rule' => 'isUnique', + 'message' => 'Title must be unique', + ), + 'minLength' => array( + 'rule' => array('minLength', 1), + 'message' => 'Title cannot be empty', + ), + ) + ); + $data = array( + 'title' => 'First Article', + ); + $data = $Article->create($data); + $this->assertFalse($Article->validates(), 'Contains a dupe'); + } + } /** diff --git a/lib/Cake/Utility/Hash.php b/lib/Cake/Utility/Hash.php index eae832d89..141dfd410 100644 --- a/lib/Cake/Utility/Hash.php +++ b/lib/Cake/Utility/Hash.php @@ -632,6 +632,9 @@ class Hash { */ public static function expand($data, $separator = '.') { $result = array(); + + $stack = array(); + foreach ($data as $flat => $value) { $keys = explode($separator, $flat); $keys = array_reverse($keys); @@ -644,7 +647,24 @@ class Hash { $k => $child ); } - $result = self::merge($result, $child); + + $stack[] = array($child, &$result); + + while (!empty($stack)) { + foreach ($stack as $curKey => &$curMerge) { + foreach ($curMerge[0] as $key => &$val) { + if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { + $stack[] = array(&$val, &$curMerge[1][$key]); + } elseif ((int)$key === $key && isset($curMerge[1][$key])) { + $curMerge[1][] = $val; + } else { + $curMerge[1][$key] = $val; + } + } + unset($stack[$curKey]); + } + unset($curMerge); + } } return $result; } @@ -664,19 +684,28 @@ class Hash { * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::merge */ public static function merge(array $data, $merge) { - $args = func_get_args(); - $return = current($args); + $args = array_slice(func_get_args(), 1); + $return = $data; - while (($arg = next($args)) !== false) { - foreach ((array)$arg as $key => $val) { - if (!empty($return[$key]) && is_array($return[$key]) && is_array($val)) { - $return[$key] = self::merge($return[$key], $val); - } elseif (is_int($key) && isset($return[$key])) { - $return[] = $val; - } else { - $return[$key] = $val; + foreach ($args as &$curArg) { + $stack[] = array((array)$curArg, &$return); + } + unset($curArg); + + while (!empty($stack)) { + foreach ($stack as $curKey => &$curMerge) { + foreach ($curMerge[0] as $key => &$val) { + if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { + $stack[] = array(&$val, &$curMerge[1][$key]); + } elseif ((int)$key === $key && isset($curMerge[1][$key])) { + $curMerge[1][] = $val; + } else { + $curMerge[1][$key] = $val; + } } + unset($stack[$curKey]); } + unset($curMerge); } return $return; }