diff --git a/src/ORM/Behavior/TranslateBehavior.php b/src/ORM/Behavior/TranslateBehavior.php index 5d32ee882fa..98e2b5815d3 100644 --- a/src/ORM/Behavior/TranslateBehavior.php +++ b/src/ORM/Behavior/TranslateBehavior.php @@ -74,7 +74,7 @@ class TranslateBehavior extends Behavior */ protected $_defaultConfig = [ 'implementedFinders' => ['translations' => 'findTranslations'], - 'implementedMethods' => ['locale' => 'locale'], + 'implementedMethods' => ['locale' => 'locale', 'mergeTranslations' => 'mergeTranslations'], 'fields' => [], 'translationTable' => 'I18n', 'defaultLocale' => '', @@ -82,7 +82,8 @@ class TranslateBehavior extends Behavior 'allowEmptyTranslations' => true, 'onlyTranslated' => false, 'strategy' => 'subquery', - 'tableLocator' => null + 'tableLocator' => null, + 'validator' => false ]; /** @@ -326,6 +327,48 @@ public function afterSave(Event $event, EntityInterface $entity) $entity->unsetProperty('_i18n'); } + /** + * Merges `$data` into `$original` entity recursively using Marshaller merge method, if + * original entity is null, new one will be created. + * The translated entity may only contain the fields defined in the + * behavior configuration (`fields`), you can use `fieldList` option as a + * whitelist of fields to be assigned. + * + * The result will be and array [entities, errors]: + * - entities indexed by locale name + * - errors indexed by locale name + * or null if there are no fields to merge. + * + * ## Note: Translated entity data not will be validated during merge. + * + * @param \Cake\Datasource\EntityInterface $original The original entity + * @param array $data key value list of languages with fields to be merged into the translate entity + * @param \Cake\ORM\Marshaller $marshaller Marshaller + * @param array $options list of options for Marshaller + * @return array|null + */ + public function mergeTranslations($original, array $data, \Cake\ORM\Marshaller $marshaller, array $options = []) + { + $options['fieldList'] = (isset($options['fieldList'])) ? array_intersect($this->_config['fields'], $options['fieldList']) : $this->_config['fields']; + if (empty($options['fieldList'])) { + return null; + } + + $options['validate'] = $this->_config['validator']; + $errors = []; + foreach ($data as $language => $fields) { + if (!isset($original[$language])) { + $original[$language] = $this->_table->newEntity(); + } + $marshaller->merge($original[$language], $fields, $options); + if ((bool)$original[$language]->errors()) { + $errors[$language] = $original[$language]->errors(); + } + } + + return [$original, $errors]; + } + /** * Sets all future finds for the bound table to also fetch translated fields for * the passed locale. If no value is passed, it returns the currently configured diff --git a/src/ORM/Marshaller.php b/src/ORM/Marshaller.php index 6c1320665d9..7105bca1f13 100644 --- a/src/ORM/Marshaller.php +++ b/src/ORM/Marshaller.php @@ -147,6 +147,8 @@ public function one(array $data, array $options = []) $marshallOptions['forceNew'] = $options['forceNew']; } + $hasTranslations = $this->_hasTranslations($options); + $errors = $this->_validate($data, $options, true); $properties = []; foreach ($data as $key => $value) { @@ -166,6 +168,8 @@ public function one(array $data, array $options = []) } elseif ($columnType) { $converter = Type::build($columnType); $value = $converter->marshal($value); + } elseif ($key === '_translations' && $hasTranslations) { + $value = $this->_mergeTranslations(null, $value, $errors, $options); } $properties[$key] = $value; } @@ -226,7 +230,7 @@ protected function _validate($data, $options, $isNew) */ protected function _prepareDataAndOptions($data, $options) { - $options += ['validate' => true]; + $options += ['validate' => true, 'translations' => true]; $tableName = $this->_table->alias(); if (isset($data[$tableName])) { @@ -449,6 +453,51 @@ protected function _loadBelongsToMany($assoc, $ids) return $this->_loadAssociatedByIds($assoc, $ids); } + /** + * Call translation merge. Validations errors during merge will be added to `$errors` param + * + * @param \Cake\Datasource\EntityInterface $original The original entity + * @param array $data key value list of languages with fields to be merged into the translate entity + * @param array $errors array with entity errors + * @param array $options list of options + * @return array|null + */ + protected function _mergeTranslations($original, array $data, array &$errors, array $options = []) + { + $result = $this->_table->mergeTranslations($original, $data, $this, $options); + + if (is_array($result)) { + if ((bool)$result[1]) { + $errors['_translations'] = $result[1]; + } + $result = $result[0]; + } + + return $result; + } + + /** + * Return if table contains translate behavior or we specificate to use via `translations` options. + * + * In case that $options has `fieldList` option and `_translations` field is not present inside it, it will include + * + * ### Options: + * + * - translations: Set to false to disable translations + * + * @param array $options List of options + * @return bool + */ + protected function _hasTranslations(array &$options = []) + { + $hasTranslations = ($this->_table->behaviors()->hasMethod('mergeTranslations') && (bool)$options['translations']); + if ($hasTranslations && !empty($options['fieldList']) && !in_array('_translations', $options['fieldList'])) { + array_push($options['fieldList'], '_translations'); + } + + return $hasTranslations; + } + /** * Merges `$data` into `$entity` and recursively does the same for each one of * the association names passed in `$options`. When merging associations, if an @@ -503,6 +552,8 @@ public function merge(EntityInterface $entity, array $data, array $options = []) } } + $hasTranslations = $this->_hasTranslations($options); + $errors = $this->_validate($data + $keys, $options, $isNew); $schema = $this->_table->schema(); $properties = $marshalledAssocs = []; @@ -530,6 +581,8 @@ public function merge(EntityInterface $entity, array $data, array $options = []) ) { continue; } + } elseif ($key === '_translations' && $hasTranslations) { + $value = $this->_mergeTranslations($original, $value, $errors, $options); } $properties[$key] = $value; diff --git a/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php b/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php index 6aab8e47364..23f56d72436 100644 --- a/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php +++ b/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php @@ -1068,4 +1068,315 @@ public function testSaveWithCleanFields() $this->assertEquals('New Body', $result->body); $this->assertSame($article->title, $result->title); } + + public function testMergeTranslations() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title']]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'en' => [ + 'title' => 'Title EN', + 'body' => 'Body EN' + ], + 'es' => [ + 'title' => 'Title ES', + 'body' => 'Body ES' + ], + ]; + + $marshallOptionsExpected = [ + 'validate' => false, + 'translates' => true, + 'fieldList' => ['title'] + ]; + + $marshallMock = $this->getMockBuilder('\Cake\ORM\Marshaller') + ->setMethods(['merge']) + ->setConstructorArgs([$table]) + ->getMock(); + $marshallMock->expects($this->exactly(2)) + ->method('merge') + ->withConsecutive( + [$this->isInstanceOf(__NAMESPACE__ . '\Article'), $this->equalTo($data['en']), $this->equalTo($marshallOptionsExpected)], + [$this->isInstanceOf(__NAMESPACE__ . '\Article'), $this->equalTo($data['es']), $this->equalTo($marshallOptionsExpected)] + ); + + $result = $table->mergeTranslations(null, $data, $marshallMock, $options = ['translates' => true]); + + $this->assertCount(2, $result); + $this->assertInstanceOf(__NAMESPACE__ . '\Article', $result[0]['en']); + $this->assertInstanceOf(__NAMESPACE__ . '\Article', $result[0]['es']); + $this->assertEmpty($result[1]); // Error + } + + public function testMergeWithValidator() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title'], 'validator' => 'custom']); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'en' => [ + 'title' => 'Title EN', + 'body' => 'Body EN' + ], + 'es' => [ + 'title' => 'Title ES', + 'body' => 'Body ES' + ], + ]; + $marshallOptionsExpected = [ + 'validate' => 'custom', + 'translates' => true, + 'fieldList' => ['title'] + ]; + + $marshallMock = $this->getMockBuilder('\Cake\ORM\Marshaller') + ->setMethods(['merge']) + ->setConstructorArgs([$table]) + ->getMock(); + $marshallMock->expects($this->exactly(2)) + ->method('merge') + ->withConsecutive( + [$this->isInstanceOf(__NAMESPACE__ . '\Article'), $this->equalTo($data['en']), $this->equalTo($marshallOptionsExpected)], + [$this->isInstanceOf(__NAMESPACE__ . '\Article'), $this->equalTo($data['es']), $this->equalTo($marshallOptionsExpected)] + ); + + $table->mergeTranslations(null, $data, $marshallMock, ['translates' => true]); + } + + public function testMergeWithFieldListOption() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title', 'body']]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'en' => [ + 'title' => 'Title EN', + 'body' => 'Body EN' + ] + ]; + $options = ['translates' => true, 'fieldList' => ['body', 'author_id']]; + + $marshallOptionsExpected = [ + 'validate' => false, + 'translates' => true, + 'fieldList' => [1 => 'body'] + ]; + + $marshallMock = $this->getMockBuilder('\Cake\ORM\Marshaller') + ->setMethods(['merge']) + ->setConstructorArgs([$table]) + ->getMock(); + $marshallMock->expects($this->exactly(1)) + ->method('merge') + ->with( + $this->isInstanceOf(__NAMESPACE__ . '\Article'), + $this->equalTo($data['en']), + $this->equalTo($marshallOptionsExpected) + ); + + $table->mergeTranslations(null, $data, $marshallMock, $options); + } + + public function testMergeWithEmptyFieldListOption() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title', 'body']]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'en' => [ + 'title' => 'Title EN', + 'body' => 'Body EN' + ] + ]; + $options = ['validate' => true, 'translates' => true, 'fieldList' => []]; + + $marshallMock = $this->getMockBuilder('\Cake\ORM\Marshaller') + ->setMethods(['merge']) + ->setConstructorArgs([$table]) + ->getMock(); + $marshallMock->expects($this->never()) + ->method('merge'); + + $result = $table->mergeTranslations(null, $data, $marshallMock, $options); + + $this->assertNull($result); + } + + public function testMergeRealData() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title', 'body']]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'en' => [ + 'title' => 'Title EN', + 'body' => 'Body EN' + ], + 'es' => [ + 'title' => 'Title ES', + 'body' => 'Body ES' + ], + ]; + + $marshaller = new \Cake\ORM\Marshaller($table); + + // Merge All Fields + $result = $table->mergeTranslations(null, $data, $marshaller, []); + $this->assertEquals($data['en'], $result[0]['en']->toArray()); + $this->assertEquals($data['es'], $result[0]['es']->toArray()); + + // Only Merge Title + $result = $table->mergeTranslations(null, $data, $marshaller, ['fieldList' => ['title']]); + $this->assertArrayNotHasKey('body', $result[0]['en']->toArray()); + + // FieldList not has translated fields (or fieldList is empty) + $result = $table->mergeTranslations(null, $data, $marshaller, ['fieldList' => ['author_id']]); + $this->assertNull($result); + } + + public function testMergeDataWithValidator() + { + $table = TableRegistry::get('Articles'); + + $validator = new \Cake\Validation\Validator; + $validator->requirePresence('title', 'create') + ->notEmpty('title'); + $validator->requirePresence('body') + ->notEmpty('body') + ->add('body', [ + 'length' => [ + 'rule' => ['minLength', 10], + 'message' => 'min_length_10', + ] + ]); + + $table->addBehavior('Translate', ['fields' => ['title', 'body'], 'validator' => $validator]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'en' => [ + 'title' => '', + 'body' => 'Very Looong Body EN' + ], + 'es' => [ + 'title' => 'Title ES', + 'body' => 'Body ES' + ], + ]; + + $marshaller = new \Cake\ORM\Marshaller($table); + + // Fails Title (en) and Body (es) + $expectedErrors = [ + 'en' => [ + 'title' => ['_empty' => 'This field cannot be left empty'] + ], + 'es' => [ + 'body' => ['length' => 'min_length_10'] + ] + ]; + $result = $table->mergeTranslations(null, $data, $marshaller, []); + $this->assertEquals($expectedErrors, $result[1]); + $this->assertArrayHasKey('title', $result[0]['en']->invalid()); + + // Add title to en language + $data['en']['title'] = 'Title EN'; + $expectedErrors = [ + 'es' => [ + 'body' => ['length' => 'min_length_10'] + ] + ]; + $result = $table->mergeTranslations(null, $data, $marshaller, []); + $this->assertEquals($expectedErrors, $result[1]); + + // All Fields are ok + $data['es']['body'] = 'Very Looong Body ES'; + $result = $table->mergeTranslations(null, $data, $marshaller, []); + $this->assertEmpty($result[1]); + } + + public function testSaveNewRecordWithTranslatesField() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title']]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'author_id' => 1, + 'published' => 'N', + '_translations' => [ + 'en' => [ + 'title' => 'Title EN', + 'body' => 'Body EN' + ], + 'es' => [ + 'title' => 'Title ES' + ] + ] + ]; + + $article = $table->patchEntity($table->newEntity(), $data); + $result = $table->save($article); + + $this->assertNotFalse($result); + + $expected = [ + [ + 'en' => [ + 'title' => 'Title EN', + 'locale' => 'en' + ], + 'es' => [ + 'title' => 'Title ES', + 'locale' => 'es' + ] + ] + ]; + $result = $table->find('translations')->where(['id' => $result->id]); + $this->assertEquals($expected, $this->_extractTranslations($result)->toArray()); + } + + public function testSaveExistingRecordWithTranslatesField() + { + $table = TableRegistry::get('Articles'); + $table->addBehavior('Translate', ['fields' => ['title', 'body']]); + $table->entityClass(__NAMESPACE__ . '\Article'); + + $data = [ + 'author_id' => 1, + 'published' => 'Y', + '_translations' => [ + 'eng' => [ + 'title' => 'First Article1', + 'body' => 'First Article content has been updated' + ], + 'spa' => [ + 'title' => 'Mi nuevo titulo', + 'body' => 'Contenido Actualizado' + ] + ] + ]; + + $article = $table->find()->first(); + $article = $table->patchEntity($article, $data); + + $this->assertNotFalse($table->save($article)); + + $results = $this->_extractTranslations( + $table->find('translations')->where(['id' => 1]) + )->first(); + + $this->assertEquals('Mi nuevo titulo', $results['spa']['title']); + $this->assertEquals('Contenido Actualizado', $results['spa']['body']); + + $this->assertEquals('First Article1', $results['eng']['title']); + $this->assertEquals('Description #1', $results['eng']['description']); + } } diff --git a/tests/TestCase/ORM/MarshallerTest.php b/tests/TestCase/ORM/MarshallerTest.php index ccb0a250007..edc565ea4f8 100644 --- a/tests/TestCase/ORM/MarshallerTest.php +++ b/tests/TestCase/ORM/MarshallerTest.php @@ -3059,4 +3059,388 @@ public function testEnsurePrimaryKeyBeingReadFromTableWhenLoadingBelongsToManyRe ]; $this->assertEquals($expected, $result->toArray()); } + + /** + * Test all scenarios + * + * @return void + */ + public function testHasTranslations() + { + // Mocks + $mockBehavior = $this->_getMockBehaviors(); + $mockBehavior->expects($this->at(1)) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(true)); + $mockBehavior->expects($this->at(2)) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(true)); + $mockBehavior->expects($this->at(3)) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(true)); + $mockBehavior->expects($this->at(4)) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(true)); + $mockBehavior->expects($this->at(5)) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(false)); + + $mockTable = $this->_getMockTable(['mergeTranslations'], ['behaviors' => $mockBehavior]); + + $marshall = new Marshaller($mockTable); + $method = $this->_getProtectedMethod($marshall, '_hasTranslations'); + + $options = ['translations' => true]; + $args = [&$options]; + + // Must return true + $this->assertTrue($method->invokeArgs($marshall, $args)); + + // Add _translations to fieldList + $options['translations'] = true; + $options['fieldList'] = ['title']; + $this->assertTrue($method->invokeArgs($marshall, $args)); + $this->assertContains('_translations', $options['fieldList']); + + // Call again, now _translations is already inside fieldList + $this->assertTrue($method->invokeArgs($marshall, $args)); + $this->assertEquals(['title', '_translations'], $options['fieldList']); + + // Return false + $options['translations'] = false; + $this->assertFalse($method->invokeArgs($marshall, $args)); + + // Table not has behavior + $options['translations'] = true; + $this->assertFalse($method->invokeArgs($marshall, $args)); + } + + /** + * Test Merge translation without errors + * + * @return void + */ + public function testMergeTranslations() + { + // Mocks + $mockBehavior = $this->_getMockBehaviors(); + $mockTable = $this->_getMockTable(['mergeTranslations'], ['behaviors' => $mockBehavior]); + + $marshall = new Marshaller($mockTable); + $method = $this->_getProtectedMethod($marshall, '_mergeTranslations'); + + $errors = []; + $data = ['en' => ['title' => 'Hi'], 'es' => ['title' => 'hola']]; + $args = [ + null, + $data, + &$errors + ]; + + $returnValue = [ + [ + 'en' => null, + 'es' => null + ], + [] + ]; + $mockTable->expects($this->once()) + ->method('mergeTranslations') + ->with( + $this->isNull(), + $this->equalTo($data), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo([]) + ) + ->will($this->returnValue($returnValue)); + + $result = $method->invokeArgs($marshall, $args); + + $this->assertEquals($returnValue[0], $result); + $this->assertEmpty($errors); + } + + /** + * Test Merge translation with errors, add to `$errors` parameter + * + * @return void + */ + public function testMergeTranslationsWithErrors() + { + // Mocks + $mockBehavior = $this->_getMockBehaviors(); + $mockTable = $this->_getMockTable(['mergeTranslations'], ['behaviors' => $mockBehavior]); + + $marshall = new Marshaller($mockTable); + $method = $this->_getProtectedMethod($marshall, '_mergeTranslations'); + + $errors = []; + $data = ['en' => ['title' => 'Hi'], 'es' => ['title' => 'hola']]; + $args = [ + null, + $data, + &$errors + ]; + + $returnValue = [ + [ + 'en' => null, + 'es' => null + ], + [ + 'en' => [] + ] + ]; + $mockTable->expects($this->once()) + ->method('mergeTranslations') + ->with( + $this->isNull(), + $this->equalTo($data), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo([]) + ) + ->will($this->returnValue($returnValue)); + + $result = $method->invokeArgs($marshall, $args); + + $this->assertEquals($returnValue[0], $result); + $this->assertArrayHasKey('_translations', $errors); + } + + /** + * Test Merge translation when return null value + * + * @return void + */ + public function testMergeTranslationsReturnNull() + { + // Mocks + $mockBehavior = $this->_getMockBehaviors(); + $mockTable = $this->_getMockTable(['mergeTranslations'], ['behaviors' => $mockBehavior]); + + $marshall = new Marshaller($mockTable); + $method = $this->_getProtectedMethod($marshall, '_mergeTranslations'); + + $errors = []; + $data = ['en' => ['title' => 'Hi'], 'es' => ['title' => 'hola']]; + $args = [ + null, + $data, + &$errors + ]; + + $mockTable->expects($this->once()) + ->method('mergeTranslations') + ->with( + $this->isNull(), + $this->equalTo($data), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo([]) + ) + ->will($this->returnValue(null)); + + $this->assertNull($method->invokeArgs($marshall, $args)); + $this->assertEmpty($errors); + } + + /** + * Test calling One method from Marshaller + * + * @return void + */ + public function testMergeTranslationsViaOneMethod() + { + $data = [ + 'author_id' => 1, + '_translations' => [ + 'en' => [ + 'title' => 'Title EN' + ], + 'es' => [ + 'title' => 'Title ES' + ] + ] + ]; + + $expectedMarshallerOptions = [ + 'validate' => true, + 'translations' => true + ]; + + // Mocks + $mockBehavior = $this->_getMockBehaviors(); + $mockBehavior->expects($this->any()) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(true)); + + $mockTable = $this->_getMockTable(['mergeTranslations'], ['behaviors' => $mockBehavior]); + $mockTable->expects($this->at(0)) + ->method('mergeTranslations') + ->with( + $this->isNull(), + $this->equalTo($data['_translations']), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo($expectedMarshallerOptions) + ) + ->will($this->returnValue(true)); + + // Second Call + $returnValue = [ + [ + 'en' => null, + 'es' => null + ], + [ + 'en' => [] + ] + ]; + $mockTable->expects($this->at(1)) + ->method('mergeTranslations') + ->with( + $this->isNull(), + $this->equalTo($data['_translations']), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo($expectedMarshallerOptions) + ) + ->will($this->returnValue($returnValue)); + + $marshall = new Marshaller($mockTable); + $result = $marshall->one($data); + + $this->assertTrue($result->get('_translations')); + $this->assertEmpty($result->errors()); + + // With errors + $result = $marshall->one($data); + $this->assertEquals($returnValue[0], $result->get('_translations')); + $this->assertNotEmpty($result->errors()); + $this->assertArrayHasKey('_translations', $result->errors()); + } + + /** + * Test calling merge method from Marshaller + * + * @return void + */ + public function testMergeTranslationsViaMergeMethod() + { + $data = [ + 'author_id' => 1, + '_translations' => [ + 'en' => [ + 'title' => 'Title EN' + ], + 'es' => [ + 'title' => 'Title ES' + ] + ] + ]; + + $expectedMarshallerOptions = [ + 'validate' => true, + 'translations' => true + ]; + + // Mocks + $mockBehavior = $this->_getMockBehaviors(); + $mockBehavior->expects($this->any()) + ->method('hasMethod') + ->with('mergeTranslations') + ->will($this->returnValue(true)); + + $mockTable = $this->_getMockTable(['mergeTranslations'], ['behaviors' => $mockBehavior]); + $mockTable->expects($this->at(0)) + ->method('mergeTranslations') + ->with( + $this->isNull(), + $this->equalTo($data['_translations']), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo($expectedMarshallerOptions) + ) + ->will($this->returnValue(true)); + + // Second Call + $returnValue = [ + [ + 'en' => null, + 'es' => null + ], + [ + 'en' => [] + ] + ]; + $mockTable->expects($this->at(1)) + ->method('mergeTranslations') + ->with( + $this->isTrue(), // _translation value inside entity is true (see last expect call) + $this->equalTo($data['_translations']), + $this->isInstanceOf('Cake\ORM\Marshaller'), + $this->equalTo($expectedMarshallerOptions) + ) + ->will($this->returnValue($returnValue)); + + $entity = $this->articles->newEntity(); + + $marshall = new Marshaller($mockTable); + $marshall->merge($entity, $data); + + $this->assertTrue($entity->get('_translations')); + $this->assertEmpty($entity->errors()); + + // Now with errors + $marshall->merge($entity, $data); + $this->assertEquals($returnValue[0], $entity->get('_translations')); + $this->assertNotEmpty($entity->errors()); + $this->assertArrayHasKey('_translations', $entity->errors()); + } + + /** + * Helper method for making mocks. + * + * @param array $methods + * @return \Cake\ORM\Table (Mock) + */ + protected function _getMockTable(array $methods = [], array $config = []) + { + $config += [ + 'alias' => 'Articles', + 'schema' => [ + 'id' => ['type' => 'integer'] + ] + ]; + + return $this->getMockBuilder('Cake\ORM\Table') + ->setMethods($methods) + ->setConstructorArgs([$config]) + ->getMock(); + } + + /** + * Helper method for making mocks. + * + * @param array $methods + * @return Cake\ORM\BehaviorRegistry (Mock) + */ + protected function _getMockBehaviors(array $methods = []) + { + return $this->getMockBuilder('Cake\ORM\BehaviorRegistry') + ->disableOriginalConstructor() + ->setMethods($methods) + ->getMock(); + } + + protected function _getProtectedMethod($obj, $name) + { + $class = new \ReflectionClass($obj); + $method = $class->getMethod($name); + $method->setAccessible(true); + + return $method; + } }