diff --git a/src/ORM/Association/HasMany.php b/src/ORM/Association/HasMany.php index 61a5efe2d5f..d14f281bbf2 100644 --- a/src/ORM/Association/HasMany.php +++ b/src/ORM/Association/HasMany.php @@ -214,7 +214,7 @@ public function link(EntityInterface $sourceEntity, array $targetEntities, array $currentEntities = (new Collection((array)$sourceEntity->get($property)))->append($targetEntities); - $sourceEntity->set($property, $currentEntities->toList()); + $sourceEntity->set($property, array_unique($currentEntities->toList())); $savedEntity = $this->saveAssociated($sourceEntity); @@ -291,6 +291,65 @@ function ($assoc) use ($targetEntities) { } } + /** + * Replaces existing association links between the source entity and the target + * with the ones passed. This method does a smart cleanup, links that are already + * persisted and present in `$targetEntities` will not be deleted, new links will + * be created for the passed target entities that are not already in the database + * and the rest will be removed. + * + * For example, if an author has many articles, such as 'article1','article 2' and 'article 3' and you pass + * to this method an array containing the entities for articles 'article 1' and 'article 4', + * only the link for 'article 1' will be kept in database, the links for 'article 2' and 'article 3' will be + * deleted and the link for 'article 4' will be created. + * + * Existing links are not deleted and created again, they are either left untouched + * or updated. + * + * This method does not check link uniqueness. + * + * On success, the passed `$sourceEntity` will contain `$targetEntities` as value + * in the corresponding property for this association. + * + * Additional options for new links to be saved can be passed in the third argument, + * check `Table::save()` for information on the accepted options. + * + * ### Example: + * + * ``` + * $author->articles = [$article1, $article2, $article3, $article4]; + * $authors->save($author); + * $articles = [$article1, $article3]; + * $authors->association('articles')->replaceLinks($author, $articles); + * ``` + * + * `$author->get('articles')` will contain only `[$article1, $article3]` at the end + * + * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for + * this association + * @param array $targetEntities list of entities from the target table to be linked + * @param array $options list of options to be passed to `save` persisting or + * updating new links + * @throws \InvalidArgumentException if non persisted entities are passed or if + * any of them is lacking a primary key value + * @return bool success + */ + public function replaceLinks(EntityInterface $sourceEntity, array $targetEntities, array $options = []) + { + $property = $this->property(); + $sourceEntity->set($property, $targetEntities); + $saveStrategy = $this->saveStrategy(); + $this->saveStrategy(self::SAVE_REPLACE); + $result = $this->saveAssociated($sourceEntity, $options); + $ok = ($result instanceof EntityInterface); + + if ($ok) { + $sourceEntity = $result; + } + $this->saveStrategy($saveStrategy); + return $ok; + } + /** * Deletes/sets null the related objects according to the dependency between source and targets and foreign key nullability * Skips deleting records present in $remainingEntities diff --git a/tests/TestCase/ORM/TableTest.php b/tests/TestCase/ORM/TableTest.php index afbff8656df..85269e9fd8f 100644 --- a/tests/TestCase/ORM/TableTest.php +++ b/tests/TestCase/ORM/TableTest.php @@ -3968,7 +3968,61 @@ public function testLinkHasManyReplaceSaveStrategy() $sizeArticles++; $this->assertEquals($authors->Articles->findAllByAuthorId($author->id)->count(), $sizeArticles); - $this->assertEquals(count($author->articles), $sizeArticles); + $this->assertEquals($sizeArticles, count($author->articles)); + } + + /** + * Integration test for linking entities with HasMany. The input contains already linked entities and they should not appeat duplicated + * + * @return void + */ + public function testLinkHasManyExisting() + { + $authors = TableRegistry::get('Authors'); + $articles = TableRegistry::get('Articles'); + + $authors->hasMany('Articles', [ + 'foreignKey' => 'author_id', + 'saveStrategy' => 'replace' + ]); + + $author = $authors->newEntity(['name' => 'mylux']); + $author = $authors->save($author); + + $newArticles = $articles->newEntities( + [ + [ + 'title' => 'New bakery next corner', + 'body' => 'They sell tastefull cakes' + ], + [ + 'title' => 'Spicy cake recipe', + 'body' => 'chocolate and peppers' + ] + ] + ); + + $this->assertTrue($authors->Articles->link($author, $newArticles)); + + $sizeArticles = count($newArticles); + + $newArticles = array_merge( + $author->articles, + $articles->newEntities( + [ + [ + 'title' => 'Nothing but the cake', + 'body' => 'It is all that we need' + ] + ] + ) + ); + $this->assertTrue($authors->Articles->link($author, $newArticles)); + + $sizeArticles++; + + $this->assertEquals($sizeArticles, $authors->Articles->findAllByAuthorId($author->id)->count()); + $this->assertEquals($sizeArticles, count($author->articles)); } /** @@ -4217,6 +4271,60 @@ public function testReplacelinksBelongsToManyWithJoint() $this->assertEquals(3, $article->tags[1]->id); } + public function testReplaceLinksHasMany() + { + $authors = TableRegistry::get('Authors'); + $articles = TableRegistry::get('Articles'); + + $authors->hasMany('Articles', [ + 'foreignKey' => 'author_id' + ]); + + $author = $authors->newEntity(['name' => 'mylux']); + $author = $authors->save($author); + + $newArticles = $articles->newEntities( + [ + [ + 'title' => 'New bakery next corner', + 'body' => 'They sell tastefull cakes' + ], + [ + 'title' => 'Spicy cake recipe', + 'body' => 'chocolate and peppers' + ] + ] + ); + + $sizeArticles = count($newArticles); + + $this->assertTrue($authors->Articles->link($author, $newArticles)); + + $this->assertEquals($authors->Articles->findAllByAuthorId($author->id)->count(), $sizeArticles); + $this->assertEquals(count($author->articles), $sizeArticles); + + $newArticles = array_merge( + $author->articles, + $articles->newEntities( + [ + [ + 'title' => 'Cheese cake recipe', + 'body' => 'The secrets of mixing salt and sugar' + ], + [ + 'title' => 'Not another piece of cake', + 'body' => 'This is the best' + ] + ] + ) + ); + unset($newArticles[0]); + + $this->assertTrue($authors->Articles->replaceLinks($author, $newArticles)); + $this->assertEquals(count($newArticles), count($author->articles)); + $this->assertEquals((new Collection($newArticles))->extract('title'), (new Collection($author->articles))->extract('title')); + } + /** * Tests that it is possible to call find with no arguments *