From bc07ba3839db17d6ffd1a6c19c0f53a209af49a1 Mon Sep 17 00:00:00 2001 From: Ceeram Date: Mon, 13 Feb 2012 01:00:28 +0100 Subject: [PATCH] Added option['deep'] to saveAll, to save unlimited levels associated data Squashed commit of the following: commit 45caa54e3b25bc94ee10d7b3700ff334e7994257 Author: Ceeram Date: Sun Feb 12 22:29:33 2012 +0100 update docblocks for deep save commit 6f3c3b9abf12e394262b0a233188a52095f64b50 Merge: 1d32698 1dd0ff1 Author: Ceeram Date: Sun Feb 12 18:17:34 2012 +0100 Merge branch '2.1' into 2.1-saveAllTheThings commit 1d32698640fa1a3c1d606eeaf4740637ff8a1991 Author: Ceeram Date: Sun Feb 12 18:16:57 2012 +0100 Revert "adding info in docblock about associated model fieldList" This reverts commit 7cc10a2b5afc1007c388a6da449781dc351d50cd. commit 7cc10a2b5afc1007c388a6da449781dc351d50cd Author: Ceeram Date: Sun Feb 12 18:05:18 2012 +0100 adding info in docblock about associated model fieldList commit db2ad2759f6af460715a8bbee167262bcbb62d77 Author: Ceeram Date: Sun Feb 12 17:51:44 2012 +0100 add tests for deep saveAll respecting fieldList option commit 14123fccfc43fac1b4cddeea8350dca8fb9c9821 Merge: cfdf25d 2afb05b Author: Ceeram Date: Sun Feb 12 16:51:26 2012 +0100 Merge branch '2.1' into 2.1-saveAllTheThings commit cfdf25d4b587c6f1a8cd6ec2bc46dc21fa3c6704 Author: Ceeram Date: Tue Feb 7 13:54:59 2012 +0100 Make saveAllTheThings fully BC, use $options['deep'] = true, to save infinite recursive data commit 6e8c4380c37a31efc2a37a5ab6438db26d293eb3 Merge: 203c7e1 95aa7e3 Author: Ceeram Date: Wed Jan 25 14:22:26 2012 +0100 Merge branch '2.1' into 2.1-saveAllTheThings commit 203c7e1d9870e282ec7098297d47e49132904401 Author: Ceeram Date: Wed Jan 25 14:19:25 2012 +0100 validate all the things as well commit d920d0000fcab80fc48a29b45dbba147a2eed27c Merge: d648f6a 4f1be12 Author: Ceeram Date: Mon Jan 23 18:22:12 2012 +0100 Merge branch '2.1' into 2.1-saveAllTheThings commit d648f6a90419e0fa27ed1d9e9feb51c37fbf0397 Author: Ceeram Date: Fri Dec 9 15:11:52 2011 +0100 making saveAll, saveMany and saveAssociated not limited to only save directly related models --- lib/Cake/Model/Model.php | 102 ++- lib/Cake/Test/Case/Model/ModelWriteTest.php | 873 +++++++++++++++++++- lib/Cake/Test/Case/Model/models.php | 7 + 3 files changed, 947 insertions(+), 35 deletions(-) diff --git a/lib/Cake/Model/Model.php b/lib/Cake/Model/Model.php index db66c39d0..db1d460cc 100644 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -1974,6 +1974,7 @@ class Model extends Object implements CakeEventListener { * 'AssociatedModel' => array('field', 'otherfield') * ) * }}} + * - deep: see saveMany/saveAssociated * * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple * records of the same type), or an array indexed by association name. @@ -1993,11 +1994,7 @@ class Model extends Object implements CakeEventListener { return $this->saveMany($data, $options); } if ($options['validate'] === 'only') { - $validatesAssoc = $this->validateAssociated($data, $options); - if (isset($this->validationErrors[$this->alias]) && $this->validationErrors[$this->alias] === false) { - return false; - } - return $validatesAssoc; + return $this->validateAssociated($data, $options); } return $this->saveAssociated($data, $options); } @@ -2012,6 +2009,7 @@ class Model extends Object implements CakeEventListener { * - atomic: If true (default), will attempt to save all records in a single transaction. * Should be set to false if database/table does not support transactions. * - fieldList: Equivalent to the $fieldList parameter in Model::save() + * - deep: If set to true, all associated data will be saved as well. * * @param array $data Record data to save. This should be a numerically-indexed array * @param array $options Options to use when saving record data, See $options above. @@ -2025,7 +2023,7 @@ class Model extends Object implements CakeEventListener { $data = $this->data; } - $options = array_merge(array('validate' => 'first', 'atomic' => true), $options); + $options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options); $this->validationErrors = $validationErrors = array(); if (empty($data) && $options['validate'] !== false) { @@ -2046,12 +2044,21 @@ class Model extends Object implements CakeEventListener { } $return = array(); foreach ($data as $key => $record) { - $validates = ($this->create(null) !== null && $this->save($record, $options)); + $validates = $this->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false))); + } else { + $saved = $this->save($record, $options); + } + } + $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true)))); if (!$validates) { $validationErrors[$key] = $this->validationErrors; } if (!$options['atomic']) { - $return[] = $validates; + $return[$key] = $validates; } elseif (!$validates) { break; } @@ -2079,6 +2086,7 @@ class Model extends Object implements CakeEventListener { * * - atomic: If true (default), returns boolean. If false returns array. * - fieldList: Equivalent to the $fieldList parameter in Model::save() + * - deep: If set to true, all associated data will be validated as well. * * @param array $data Record data to validate. This should be a numerically-indexed array * @param array $options Options to use when validating record data (see above), See also $options of validates(). @@ -2088,14 +2096,21 @@ class Model extends Object implements CakeEventListener { * depending on whether each record validated successfully. */ public function validateMany($data, $options = array()) { - $options = array_merge(array('atomic' => true), $options); + $options = array_merge(array('atomic' => true, 'deep' => false), $options); $this->validationErrors = $validationErrors = $return = array(); foreach ($data as $key => $record) { - $validates = $this->create($record) && $this->validates($options); - if (!$validates) { - $validationErrors[$key] = $this->validationErrors; + if ($options['deep']) { + $validates = $this->validateAssociated($record, $options); + } else { + $validates = $this->create($record) && $this->validates($options); } - $return[] = $validates; + if ($validates === false || (is_array($validates) && in_array(false, $validates, true))) { + $validationErrors[$key] = $this->validationErrors; + $validates = false; + } else { + $validates = true; + } + $return[$key] = $validates; } $this->validationErrors = $validationErrors; if (!$options['atomic']) { @@ -2124,6 +2139,7 @@ class Model extends Object implements CakeEventListener { * 'AssociatedModel' => array('field', 'otherfield') * ) * }}} + * - deep: If set to true, not only directly associated data is saved, but deeper nested associated data as well. * * @param array $data Record data to save. This should be an array indexed by association name. * @param array $options Options to use when saving record data, See $options above. @@ -2137,7 +2153,7 @@ class Model extends Object implements CakeEventListener { $data = $this->data; } - $options = array_merge(array('validate' => 'first', 'atomic' => true), $options); + $options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options); $this->validationErrors = $validationErrors = array(); if (empty($data) && $options['validate'] !== false) { @@ -2160,13 +2176,26 @@ class Model extends Object implements CakeEventListener { $validates = true; foreach ($data as $association => $values) { if (isset($associations[$association]) && $associations[$association] === 'belongsTo') { - if ($this->{$association}->create(null) !== null && $this->{$association}->save($values, $options)) { - $data[$this->alias][$this->belongsTo[$association]['foreignKey']] = $this->{$association}->id; + $validates = $this->{$association}->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false))); + } else { + $saved = $this->{$association}->save($values, array_merge($options, array('atomic' => false))); + } + $validates = ($saved === true || (is_array($saved) && !in_array(false, $saved, true))); + } + if ($validates) { + if (!empty($data[$this->alias])) { + $data[$this->alias][$this->belongsTo[$association]['foreignKey']] = $this->{$association}->id; + } else { + $data[$this->belongsTo[$association]['foreignKey']] = $this->{$association}->id; + } } else { $validationErrors[$association] = $this->{$association}->validationErrors; - $validates = false; } - $return[$association][] = $validates; + $return[$association] = $validates; } } if ($validates && !($this->create(null) !== null && $this->save($data, $options))) { @@ -2184,11 +2213,20 @@ class Model extends Object implements CakeEventListener { switch ($type) { case 'hasOne': $values[$this->{$type}[$association]['foreignKey']] = $this->id; - if (!($this->{$association}->create(null) !== null && $this->{$association}->save($values, $options))) { - $validationErrors[$association] = $this->{$association}->validationErrors; - $validates = false; + $validates = $this->{$association}->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false))); + } else { + $saved = $this->{$association}->save($values, $options); + } } - $return[$association][] = $validates; + $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true)))); + if (!$validates) { + $validationErrors[$association] = $this->{$association}->validationErrors; + } + $return[$association] = $validates; break; case 'hasMany': foreach ($values as $i => $value) { @@ -2231,6 +2269,7 @@ class Model extends Object implements CakeEventListener { * * - atomic: If true (default), returns boolean. If false returns array. * - fieldList: Equivalent to the $fieldList parameter in Model::save() + * - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well. * * @param array $data Record data to validate. This should be an array indexed by association name. * @param array $options Options to use when validating record data (see above), See also $options of validates(). @@ -2239,7 +2278,7 @@ class Model extends Object implements CakeEventListener { * depending on whether each record validated successfully. */ public function validateAssociated($data, $options = array()) { - $options = array_merge(array('atomic' => true), $options); + $options = array_merge(array('atomic' => true, 'deep' => false), $options); $this->validationErrors = $validationErrors = $return = array(); if (!($this->create($data) && $this->validates($options))) { $validationErrors[$this->alias] = $this->validationErrors; @@ -2248,12 +2287,23 @@ class Model extends Object implements CakeEventListener { $return[$this->alias] = true; } $associations = $this->getAssociated(); - $validates = true; foreach ($data as $association => $values) { + $validates = true; if (isset($associations[$association])) { if (in_array($associations[$association], array('belongsTo', 'hasOne'))) { - $validates = $this->{$association}->create($values) && $this->{$association}->validates($options); - $return[$association][] = $validates; + if ($options['deep']) { + $validates = $this->{$association}->validateAssociated($values, $options); + } else { + $validates = $this->{$association}->create($values) !== null && $this->{$association}->validates($options); + } + if (is_array($validates)) { + if (in_array(false, $validates, true)) { + $validates = false; + } else { + $validates = true; + } + } + $return[$association] = $validates; } elseif ($associations[$association] === 'hasMany') { $validates = $this->{$association}->validateMany($values, $options); $return[$association] = $validates; diff --git a/lib/Cake/Test/Case/Model/ModelWriteTest.php b/lib/Cake/Test/Case/Model/ModelWriteTest.php index 0baf4d66e..d57f0a74a 100644 --- a/lib/Cake/Test/Case/Model/ModelWriteTest.php +++ b/lib/Cake/Test/Case/Model/ModelWriteTest.php @@ -2973,7 +2973,7 @@ class ModelWriteTest extends BaseModelTest { * @return void */ public function testSaveAllAtomic() { - $this->loadFixtures('Article', 'User'); + $this->loadFixtures('Article', 'User', 'Comment'); $TestModel = new Article(); $result = $TestModel->saveAll(array( @@ -3041,6 +3041,775 @@ class ModelWriteTest extends BaseModelTest { $this->assertSame($result, array('Article' => true, 'Comment' => array(true, true))); } +/** + * testSaveAllDeepAssociated method + * + * @return void + */ + public function testSaveAllDeepAssociated() { + $this->loadFixtures('Article', 'Comment', 'User', 'Attachment'); + $TestModel = new Article(); + $TestModel->hasMany['Comment']['order'] = array('Comment.created' => 'ASC'); + $TestModel->hasAndBelongsToMany = array(); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => 'newuser', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), array('deep' => true)); + $this->assertTrue($result); + + $result = $TestModel->findById(2); + $expected = array( + 'First Comment for Second Article', + 'Second Comment for Second Article', + 'First new comment', + 'Second new comment' + ); + $this->assertEquals($expected, Set::extract($result['Comment'], '{n}.comment')); + + $result = $TestModel->Comment->User->field('id', array('user' => 'newuser', 'password' => 'newuserpass')); + $this->assertEquals(5, $result); + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => 'deepsaved')) + ) + ), array('deep' => true)); + $this->assertTrue($result); + + $result = $TestModel->findById(2); + $expected = array( + 'First Comment for Second Article', + 'Second Comment for Second Article', + 'First new comment', + 'Second new comment', + 'Third new comment', + 'Fourth new comment' + ); + $this->assertEquals($expected, Set::extract($result['Comment'], '{n}.comment')); + + $result = $TestModel->Comment->Attachment->field('id', array('attachment' => 'deepsaved')); + $this->assertEquals(2, $result); + $data = array( + 'Attachment' => array( + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + 'user_id' => 5, + 'Article' => array( + 'title' => 'First Article deepsave insert', + 'body' => 'First Article Body deepsave insert', + 'User' => array( + 'user' => '', + 'password' => 'magic' + ), + ), + ) + ); + + $TestModel->Comment->Attachment->create(); + $result = $TestModel->Comment->Attachment->saveAll($data, array('deep' => true)); + $this->assertFalse($result); + + $expected = array('User' => array('user' => array('This field cannot be left blank'))); + $this->assertEquals($expected, $TestModel->validationErrors); + + $data['Comment']['Article']['User']['user'] = 'deepsave'; + $TestModel->Comment->Attachment->create(); + $result = $TestModel->Comment->Attachment->saveAll($data, array('deep' => true)); + $this->assertTrue($result); + + $result = $TestModel->Comment->Attachment->findById($TestModel->Comment->Attachment->id); + $expected = array( + 'Attachment' => array( + 'id' => '3', + 'comment_id' => '11', + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'id' => '11', + 'article_id' => '4', + 'user_id' => '5', + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + ) + ); + unset($result['Attachment']['created'], $result['Attachment']['updated']); + $this->assertEquals($expected['Attachment'], $result['Attachment']); + + unset($result['Comment']['created'], $result['Comment']['updated']); + $this->assertEquals($result['Comment'], $expected['Comment']); + + $result = $TestModel->findById($result['Comment']['article_id']); + $expected = array( + 'Article' => array( + 'id' => '4', + 'user_id' => '6', + 'title' => 'First Article deepsave insert', + 'body' => 'First Article Body deepsave insert', + 'published' => 'N', + ), + 'User' => array( + 'id' => '6', + 'user' => 'deepsave', + 'password' => 'magic', + ), + 'Comment' => array( + array( + 'id' => '11', + 'article_id' => '4', + 'user_id' => '5', + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + ) + ) + ); + unset( + $result['Article']['created'], $result['Article']['updated'], + $result['User']['created'], $result['User']['updated'], + $result['Comment'][0]['created'], $result['Comment'][0]['updated'] + ); + $this->assertEquals($result, $expected); + } + +/** + * testSaveAllDeepMany + * tests the validate methods with deeper recursive data + * + * @return void + */ + public function testSaveAllDeepMany() { + $this->loadFixtures('Article', 'Comment', 'User', 'Attachment'); + $TestModel = new Article(); + $TestModel->hasMany['Comment']['order'] = array('Comment.created' => 'ASC'); + $TestModel->hasAndBelongsToMany = array(); + + $data = array( + array( + 'id' => 1, 'body' => '', + 'Comment' => array( + array('comment' => '', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'manysaved')), + array('comment' => 'Second comment deepsaved article 1', 'published' => 'Y', 'user_id' => 2) + ) + ), + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First comment deepsaved article 2', 'published' => 'Y', 'User' => array('user' => 'savemore', 'password' => '')), + array('comment' => '', 'published' => 'Y', 'user_id' => 2) + ) + ) + ); + $TestModel->Comment->validate['comment'] = 'notEmpty'; + $result = $TestModel->saveAll($data, array('deep' => true)); + $this->assertFalse($result); + + $expected = array( + 0 => array( + 'body' => array('This field cannot be left blank') + ), + 1 => array( + 'Comment' => array( + 0 => array( + 'User' => array( + 'password' => array('This field cannot be left blank') + ) + ), + 1 => array( + 'comment' => array('This field cannot be left blank') + ) + ) + ) + ); + $result = $TestModel->validationErrors; + $this->assertSame($expected, $result); + + $data = array( + array( + 'Article' => array('id' => 1), + 'Comment' => array( + array('comment' => 'First comment deepsaved article 1', 'published' => 'Y', 'User' => array('user' => 'savemany', 'password' => 'manysaved')), + array('comment' => 'Second comment deepsaved article 1', 'published' => 'Y', 'user_id' => 2) + ) + ), + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First comment deepsaved article 2', 'published' => 'Y', 'User' => array('user' => 'savemore', 'password' => 'moresaved')), + array('comment' => 'Second comment deepsaved article 2', 'published' => 'Y', 'user_id' => 2) + ) + ) + ); + $result = $TestModel->saveAll($data, array('deep' => true)); + $this->assertTrue($result); + } +/** + * testSaveAllDeepValidateOnly + * tests the validate methods with deeper recursive data + * + * @return void + */ + public function testSaveAllDeepValidateOnly() { + $this->loadFixtures('Article', 'Comment', 'User', 'Attachment'); + $TestModel = new Article(); + $TestModel->hasMany['Comment']['order'] = array('Comment.created' => 'ASC'); + $TestModel->hasAndBelongsToMany = array(); + $TestModel->Comment->Attachment->validate['attachment'] = 'notEmpty'; + $TestModel->Comment->validate['comment'] = 'notEmpty'; + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => 'newuser', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'deep' => true) + ); + $this->assertTrue($result); + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'deep' => true) + ); + $this->assertFalse($result); + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => 'newuser', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'atomic' => false, 'deep' => true) + ); + $expected = array( + 'Article' => true, + 'Comment' => array( + true, + true + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'atomic' => false, 'deep' => true) + ); + $expected = array( + 'Article' => true, + 'Comment' => array( + false, + true + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => 'deepsaved')) + ) + ), + array('validate' => 'only', 'deep' => true) + ); + $this->assertTrue($result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => '')) + ) + ), + array('validate' => 'only', 'deep' => true) + ); + $this->assertFalse($result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => 'deepsave')) + ) + ), + array('validate' => 'only', 'atomic' => false, 'deep' => true) + ); + $expected = array( + 'Article' => true, + 'Comment' => array( + true, + true + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => '')) + ) + ), + array('validate' => 'only', 'atomic' => false, 'deep' => true) + ); + $expected = array( + 'Article' => true, + 'Comment' => array( + true, + false + ) + ); + $this->assertSame($expected, $result); + + $expected = array( + 'Comment' => array( + 1 => array( + 'Attachment' => array( + 'attachment' => array('This field cannot be left blank') + ) + ) + ) + ); + $result = $TestModel->validationErrors; + $this->assertSame($expected, $result); + + $data = array( + 'Attachment' => array( + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + 'user_id' => 5, + 'Article' => array( + 'title' => 'First Article deepsave insert', + 'body' => 'First Article Body deepsave insert', + 'User' => array( + 'user' => 'deepsave', + 'password' => 'magic' + ), + ), + ) + ); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => true)); + $this->assertTrue($result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => true)); + $expected = array( + 'Attachment' => true, + 'Comment' => true + ); + $this->assertSame($expected, $result); + + $data = array( + 'Attachment' => array( + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + 'user_id' => 5, + 'Article' => array( + 'title' => 'First Article deepsave insert', + 'body' => 'First Article Body deepsave insert', + 'User' => array( + 'user' => '', + 'password' => 'magic' + ), + ), + ) + ); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => true)); + $this->assertFalse($result); + + $result = $TestModel->Comment->Attachment->validationErrors; + $expected = array( + 'Comment' => array( + 'Article' => array( + 'User' => array( + 'user' => array('This field cannot be left blank') + ) + ) + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => true)); + $expected = array( + 'Attachment' => true, + 'Comment' => false + ); + $this->assertEquals($expected, $result); + + $data['Comment']['Article']['body'] = ''; + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => true)); + $this->assertFalse($result); + + $result = $TestModel->Comment->Attachment->validationErrors; + $expected = array( + 'Comment' => array( + 'Article' => array( + 'body' => array('This field cannot be left blank') + ) + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => true)); + $expected = array( + 'Attachment' => true, + 'Comment' => false + ); + $this->assertEquals($expected, $result); + + $data['Comment']['comment'] = ''; + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => true)); + $this->assertFalse($result); + + $result = $TestModel->Comment->Attachment->validationErrors; + $expected = array( + 'Comment' => array( + 'comment' => array('This field cannot be left blank') + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => true)); + $expected = array( + 'Attachment' => true, + 'Comment' => false + ); + $this->assertEquals($expected, $result); + + $data['Attachment']['attachment'] = ''; + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => true)); + $this->assertFalse($result); + + $result = $TestModel->Comment->Attachment->validationErrors; + $expected = array('attachment' => array('This field cannot be left blank')); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->validationErrors; + $expected = array('comment' => array('This field cannot be left blank')); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => true)); + $expected = array( + 'Attachment' => false, + 'Comment' => false + ); + $this->assertEquals($expected, $result); + } + +/** + * testSaveAllNotDeepAssociated method + * test that only directly associated data gets saved + * + * @return void + */ + public function testSaveAllNotDeepAssociated() { + $this->loadFixtures('Article', 'Comment', 'User', 'Attachment'); + $TestModel = new Article(); + $TestModel->hasMany['Comment']['order'] = array('Comment.created' => 'ASC'); + $TestModel->hasAndBelongsToMany = array(); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => 'newuser', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), array('deep' => false)); + $this->assertTrue($result); + + $result = $TestModel->Comment->User->field('id', array('user' => 'newuser', 'password' => 'newuserpass')); + $this->assertFalse($result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 4), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => 'deepsaved')) + ) + ), array('deep' => false)); + $this->assertTrue($result); + + $result = $TestModel->Comment->Attachment->field('id', array('attachment' => 'deepsaved')); + $this->assertFalse($result); + + $data = array( + 'Attachment' => array( + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + 'user_id' => 4, + 'Article' => array( + 'title' => 'First Article deepsave insert', + 'body' => 'First Article Body deepsave insert', + 'User' => array( + 'user' => 'deepsave', + 'password' => 'magic' + ), + ), + ) + ); + $expected = $TestModel->User->find('count'); + + $TestModel->Comment->Attachment->create(); + $result = $TestModel->Comment->Attachment->saveAll($data, array('deep' => false)); + $this->assertTrue($result); + + $result = $TestModel->User->find('count'); + $this->assertEquals($expected, $result); + + $result = $TestModel->Comment->Attachment->findById($TestModel->Comment->Attachment->id); + $expected = array( + 'Attachment' => array( + 'id' => '2', + 'comment_id' => '11', + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'id' => '11', + 'article_id' => '0', + 'user_id' => '4', + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + ) + ); + unset($result['Attachment']['created'], $result['Attachment']['updated']); + $this->assertEquals($expected['Attachment'], $result['Attachment']); + + unset($result['Comment']['created'], $result['Comment']['updated']); + $this->assertEquals($expected['Comment'], $result['Comment']); + } + +/** + * testSaveAllNotDeepMany + * tests the save methods to not save deeper recursive data + * + * @return void + */ + public function testSaveAllNotDeepMany() { + $this->loadFixtures('Article', 'Comment', 'User', 'Attachment'); + $TestModel = new Article(); + $TestModel->hasMany['Comment']['order'] = array('Comment.created' => 'ASC'); + $TestModel->hasAndBelongsToMany = array(); + + $data = array( + array( + 'id' => 1, 'body' => '', + 'Comment' => array( + array('comment' => '', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'manysaved')), + array('comment' => 'Second comment deepsaved article 1', 'published' => 'Y', 'user_id' => 2) + ) + ), + array( + 'Article' => array('id' => 2), + 'Comment' => array( + array('comment' => 'First comment deepsaved article 2', 'published' => 'Y', 'User' => array('user' => 'savemore', 'password' => '')), + array('comment' => '', 'published' => 'Y', 'user_id' => 2) + ) + ) + ); + $TestModel->Comment->validate['comment'] = 'notEmpty'; + $result = $TestModel->saveAll($data, array('deep' => false)); + $this->assertFalse($result); + + $expected = array( + 0 => array( + 'body' => array('This field cannot be left blank') + ) + ); + $result = $TestModel->validationErrors; + $this->assertSame($expected, $result); + + $data = array( + array( + 'Article' => array('id' => 1, 'body' => 'Ignore invalid comment'), + 'Comment' => array( + array('comment' => '', 'published' => 'Y', 'user_id' => 2) + ) + ), + array( + 'Article' => array('id' => 2, 'body' => 'Same here'), + 'Comment' => array( + array('comment' => '', 'published' => 'Y', 'user_id' => 2) + ) + ) + ); + $result = $TestModel->saveAll($data, array('deep' => false)); + $this->assertTrue($result); + } +/** + * testSaveAllNotDeepValidateOnly + * tests the validate methods to not validate deeper recursive data + * + * @return void + */ + public function testSaveAllNotDeepValidateOnly() { + $this->loadFixtures('Article', 'Comment', 'User', 'Attachment'); + $TestModel = new Article(); + $TestModel->hasMany['Comment']['order'] = array('Comment.created' => 'ASC'); + $TestModel->hasAndBelongsToMany = array(); + $TestModel->Comment->Attachment->validate['attachment'] = 'notEmpty'; + $TestModel->Comment->validate['comment'] = 'notEmpty'; + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2, 'body' => ''), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'deep' => false) + ); + $this->assertFalse($result); + + $expected = array('body' => array('This field cannot be left blank')); + $result = $TestModel->validationErrors; + $this->assertSame($expected, $result); + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2, 'body' => 'Ignore invalid user data'), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'deep' => false) + ); + $this->assertTrue($result); + + $result = $TestModel->saveAll( + array( + 'Article' => array('id' => 2, 'body' => 'Ignore invalid user data'), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'User' => array('user' => '', 'password' => 'newuserpass')), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2) + ) + ), + array('validate' => 'only', 'atomic' => false, 'deep' => false) + ); + $expected = array( + 'Article' => true, + 'Comment' => array( + true, + true + ) + ); + $this->assertSame($expected, $result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2, 'body' => 'Ignore invalid attachment data'), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => '')) + ) + ), + array('validate' => 'only', 'deep' => false) + ); + $this->assertTrue($result); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2, 'body' => 'Ignore invalid attachment data'), + 'Comment' => array( + array('comment' => 'Third new comment', 'published' => 'Y', 'user_id' => 5), + array('comment' => 'Fourth new comment', 'published' => 'Y', 'user_id' => 2, 'Attachment' => array('attachment' => '')) + ) + ), + array('validate' => 'only', 'atomic' => false, 'deep' => false) + ); + $expected = array( + 'Article' => true, + 'Comment' => array( + true, + true + ) + ); + $this->assertSame($expected, $result); + + $expected = array(); + $result = $TestModel->validationErrors; + $this->assertSame($expected, $result); + + $data = array( + 'Attachment' => array( + 'attachment' => 'deepsave insert', + ), + 'Comment' => array( + 'comment' => 'First comment deepsave insert', + 'published' => 'Y', + 'user_id' => 5, + 'Article' => array( + 'title' => 'First Article deepsave insert ignored', + 'body' => 'First Article Body deepsave insert', + 'User' => array( + 'user' => '', + 'password' => 'magic' + ), + ), + ) + ); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => false)); + $this->assertTrue($result); + + $result = $TestModel->Comment->Attachment->validationErrors; + $expected = array(); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => false)); + $expected = array( + 'Attachment' => true, + 'Comment' => true + ); + $this->assertEquals($expected, $result); + + $data['Comment']['Article']['body'] = ''; + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'deep' => false)); + $this->assertTrue($result); + + $result = $TestModel->Comment->Attachment->validationErrors; + $expected = array(); + $this->assertSame($expected, $result); + + $result = $TestModel->Comment->Attachment->saveAll($data, array('validate' => 'only', 'atomic' => false, 'deep' => false)); + $expected = array( + 'Attachment' => true, + 'Comment' => true + ); + $this->assertEquals($expected, $result); + } + /** * testSaveAllHasMany method * @@ -3212,7 +3981,7 @@ class ModelWriteTest extends BaseModelTest { $db->expects($this->once())->method('rollback'); $db->expects($this->any())->method('describe') ->will($this->returnValue(array( - 'id' => array('type' => 'integer'), + 'id' => array('type' => 'integer', 'length' => 11), 'title' => array('type' => 'string'), 'body' => array('type' => 'text'), 'published' => array('type' => 'string') @@ -4541,7 +5310,7 @@ class ModelWriteTest extends BaseModelTest { $db->expects($this->once())->method('rollback'); $db->expects($this->any())->method('describe') ->will($this->returnValue(array( - 'id' => array('type' => 'integer'), + 'id' => array('type' => 'integer', 'length' => 11), 'title' => array('type' => 'string'), 'body' => array('type' => 'text'), 'published' => array('type' => 'string') @@ -5512,11 +6281,11 @@ class ModelWriteTest extends BaseModelTest { } /** - * validateSaveAllFieldListBelongsTo + * testSaveAllFieldListValidateBelongsTo * * @return void */ - public function validateSaveAllFieldListBelongsTo() { + public function testSaveAllFieldListValidateBelongsTo() { $this->loadFixtures('Post', 'Author', 'Comment', 'Attachment'); $TestModel = new Post(); @@ -5543,8 +6312,7 @@ class ModelWriteTest extends BaseModelTest { $result = $TestModel->find('all'); $expected = array( - 'Post' => - array ( + 'Post' => array ( 'id' => '4', 'author_id' => '5', 'title' => 'Post without body', @@ -5553,8 +6321,7 @@ class ModelWriteTest extends BaseModelTest { 'created' => $ts, 'updated' => $ts, ), - 'Author' => - array ( + 'Author' => array ( 'id' => '5', 'user' => 'bob', 'password' => NULL, @@ -5683,4 +6450,92 @@ class ModelWriteTest extends BaseModelTest { $this->assertEmpty($TestModel->validationErrors); } +/** + * testSaveAllDeepFieldListValidateBelongsTo + * + * @return void + */ + public function testSaveAllDeepFieldListValidateBelongsTo() { + $this->loadFixtures('Post', 'Author', 'Comment', 'Attachment', 'Article', 'User'); + $TestModel = new Post(); + $TestModel->Author->bindModel(array('hasMany' => array('Comment' => array('foreignKey' => 'user_id'))), false); + $TestModel->recursive = 2; + + $result = $TestModel->find('all'); + $this->assertCount(3, $result); + $this->assertFalse(isset($result[3])); + $ts = date('Y-m-d H:i:s'); + + // test belongsTo + $fieldList = array( + 'Post' => array('title', 'author_id'), + 'Author' => array('user'), + 'Comment' => array('comment') + ); + $TestModel->saveAll(array( + 'Post' => array( + 'title' => 'Post without body', + 'body' => 'This will not be saved', + ), + 'Author' => array( + 'user' => 'bob', + 'test' => 'This will not be saved', + 'Comment' => array( + array('id' => 5, 'comment' => 'I am still published', 'published' => 'N')) + + )), array('fieldList' => $fieldList, 'deep' => true)); + + $result = $TestModel->Author->Comment->find('first', array( + 'conditions' => array('Comment.id' => 5), + 'fields' => array('comment', 'published') + )); + $expected = array( + 'Comment' => array( + 'comment' => 'I am still published', + 'published' => 'Y' + ) + ); + $this->assertEquals($expected, $result); + } + +/** + * testSaveAllDeepFieldListHasMany method + * + * return @void + */ + public function testSaveAllDeepFieldListHasMany() { + $this->loadFixtures('Article', 'Comment', 'User'); + $TestModel = new Article(); + $TestModel->belongsTo = $TestModel->hasAndBelongsToMany = array(); + + $this->db->truncate($TestModel); + $this->db->truncate(new Comment()); + + $fieldList = array( + 'Article' => array('id'), + 'Comment' => array('article_id', 'user_id'), + 'User' => array('user') + ); + + $result = $TestModel->saveAll(array( + 'Article' => array('id' => 2, 'title' => 'I will not save'), + 'Comment' => array( + array('comment' => 'First new comment', 'published' => 'Y', 'user_id' => 1), + array('comment' => 'Second new comment', 'published' => 'Y', 'user_id' => 2, 'User' => array('user' => 'nopassword', 'password' => 'not saved')) + ) + ), array('fieldList' => $fieldList, 'deep' => true)); + + $result = $TestModel->Comment->User->find('first', array( + 'conditions' => array('User.user' => 'nopassword'), + 'fields' => array('user', 'password') + )); + $expected = array( + 'User' => array( + 'user' => 'nopassword', + 'password' => '' + ) + ); + $this->assertEquals($expected, $result); + } + } diff --git a/lib/Cake/Test/Case/Model/models.php b/lib/Cake/Test/Case/Model/models.php index d3495c6ac..9f583066e 100644 --- a/lib/Cake/Test/Case/Model/models.php +++ b/lib/Cake/Test/Case/Model/models.php @@ -641,6 +641,13 @@ class Attachment extends CakeTestModel { * @var string 'Attachment' */ public $name = 'Attachment'; + +/** + * belongsTo property + * + * @var array + */ + public $belongsTo = array('Comment'); } /**