diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results new file mode 100644 index 0000000..c36c7a4 --- /dev/null +++ b/.phpunit.cache/test-results @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":{"Tests\\OneToManyTest::test_one_to_many_term_relationships":0.192,"Tests\\ManyToManyDeleteTest::test_many_to_many_delete":0.161,"Tests\\ManyToManyDeleteTest::test_many_to_many_user_delete":0.103,"Tests\\ManyToManyDeleteTest::test_many_to_many_term_delete":0.171,"Tests\\ManyToManyDisabledDeleteTest::test_many_to_many_deletes_can_be_disabled":0.085,"Tests\\ManyToManyDisabledDeleteTest::test_many_to_many_user_deletes_can_be_disabled":0.073,"Tests\\ManyToManyDisabledDeleteTest::test_many_to_many_term_deletes_can_be_disabled":0.109,"Tests\\ManyToManyTest::test_many_to_many_relationship":0.204,"Tests\\ManyToManyTest::test_many_to_many_user_relationships":0.094,"Tests\\ManyToManyTest::test_many_to_many_term_relationships":0.106,"Tests\\ManyToOneDeleteTest::test_many_to_one_delete":0.169,"Tests\\ManyToOneDeleteTest::test_many_to_one_user_delete":0.146,"Tests\\ManyToOneDeleteTest::test_many_to_one_term_delete":0.112,"Tests\\ManyToOneDisabledDeleteTest::test_many_to_one_delete_disabled":0.118,"Tests\\ManyToOneDisabledDeleteTest::test_many_to_one_user_delete_disabled":0.103,"Tests\\ManyToOneDisabledDeleteTest::test_many_to_one_term_delete_disabled":0.07,"Tests\\ManyToOneTest::test_many_to_one_relationship":0.18,"Tests\\ManyToOneTest::test_many_to_one_user_relationship":0.098,"Tests\\ManyToOneTest::test_many_to_one_term_relationship":0.137,"Tests\\OneToManyDeleteTest::test_one_to_many_delete":0.182,"Tests\\OneToManyDeleteTest::test_one_to_many_user_delete":0.115,"Tests\\OneToManyDeleteTest::test_one_to_many_term_delete":0.132,"Tests\\OneToManyDisabledDeleteTest::test_one_to_many_delete_disabled":0.139,"Tests\\OneToManyDisabledDeleteTest::test_one_to_many_user_delete_disabled":0.076,"Tests\\OneToManyDisabledDeleteTest::test_one_to_many_term_delete_disabled":0.08,"Tests\\OneToManyTest::test_one_to_many_relationships":0.197,"Tests\\OneToManyTest::test_explicit_one_to_many":0.2,"Tests\\OneToManyTest::test_one_to_many_user_relationships":0.18,"Tests\\OneToOneDeleteTest::test_one_to_one_delete":0.07,"Tests\\OneToOneDeleteTest::test_one_to_one_user_delete":0.066,"Tests\\OneToOneDeleteTest::test_one_to_one_term_delete":0.071,"Tests\\OneToOneDisabledDeleteTest::test_one_to_one_delete_disabled":0.047,"Tests\\OneToOneDisabledDeleteTest::test_one_to_one_user_disabled_delete":0.041,"Tests\\OneToOneDisabledDeleteTest::test_one_to_one_term_delete_disabled":0.044,"Tests\\OneToOneTest::test_one_to_one_relationship":0.079,"Tests\\OneToOneTest::test_one_to_one_user_relationship":0.069,"Tests\\OneToOneTest::test_one_to_one_term_relationship":0.072,"Tests\\SetNotationRelationshipsTest::test_it_extracts_collection_names":0.01,"Tests\\SetNotationRelationshipsTest::test_it_extracts_collection_names_without_sets":0.011}} \ No newline at end of file diff --git a/src/EntryRelationship.php b/src/EntryRelationship.php index 5176da0..af4ccc1 100644 --- a/src/EntryRelationship.php +++ b/src/EntryRelationship.php @@ -14,6 +14,10 @@ class EntryRelationship const TYPE_MANY_TO_ONE = 4; + public ?EntryRelationship $origin = null; + + public ?EntryRelationship $inverted = null; + public $leftType = ''; public $rightType = ''; @@ -115,6 +119,14 @@ public function withEvents($withEvents = true) return $this; } + public function withOriginRelationship(EntryRelationship $relationship) + { + $this->origin = $relationship; + $relationship->inverted = $this; + + return $this; + } + public function isAutomaticInverse($isInverse = true, $inverseIndex = null) { $this->isAutomaticInverse = $isInverse; @@ -128,6 +140,15 @@ public function isAutomaticInverse($isInverse = true, $inverseIndex = null) return $this; } + public function getInverse() + { + if ($this->isAutomaticInverse) { + return $this->origin; + } + + return $this->inverted; + } + /** * Sets whether affected entries will be updated when deleting related entries. * diff --git a/src/Listeners/EntrySavedListener.php b/src/Listeners/EntrySavedListener.php index 914c5f0..7115f65 100644 --- a/src/Listeners/EntrySavedListener.php +++ b/src/Listeners/EntrySavedListener.php @@ -31,7 +31,7 @@ public function handle(EntrySaved $event) return; } - if (EventStack::count() > 0) { + if (EventStack::count() > 0 || $this->manager->processor()->isProcessingRelationships()) { return; } diff --git a/src/Listeners/EntrySavingListener.php b/src/Listeners/EntrySavingListener.php index a1b833f..59c4166 100644 --- a/src/Listeners/EntrySavingListener.php +++ b/src/Listeners/EntrySavingListener.php @@ -38,7 +38,7 @@ public function handle(EntrySaving $event) return; } - if (EventStack::count() > 1) { + if (EventStack::count() > 1 || $this->manager->processor()->isProcessingRelationships()) { return; } diff --git a/src/Processors/Concerns/ProcessesManyToMany.php b/src/Processors/Concerns/ProcessesManyToMany.php index d52a72b..f3931dd 100644 --- a/src/Processors/Concerns/ProcessesManyToMany.php +++ b/src/Processors/Concerns/ProcessesManyToMany.php @@ -9,6 +9,8 @@ trait ProcessesManyToMany { protected function processManyToMany(ComparisonResult $results, EntryRelationship $relationship) { + $this->processingRelationships = true; + foreach ($results->added as $addedId) { if (! $this->shouldProcessRelationship($relationship, $addedId)) { continue; @@ -24,5 +26,7 @@ protected function processManyToMany(ComparisonResult $results, EntryRelationshi $this->removeItemFromEntry($relationship, $this->getEffectedEntity($relationship, $removedId)); } + + $this->processingRelationships = false; } } diff --git a/src/Processors/Concerns/ProcessesManyToOne.php b/src/Processors/Concerns/ProcessesManyToOne.php index ca55f57..93bb09f 100644 --- a/src/Processors/Concerns/ProcessesManyToOne.php +++ b/src/Processors/Concerns/ProcessesManyToOne.php @@ -9,14 +9,14 @@ trait ProcessesManyToOne { protected function processManyToOne(ComparisonResult $results, EntryRelationship $relationship) { - if (! empty($results->added) && count($results->added) == 1 && $this->shouldProcessRelationship($relationship, $results->added[0])) { - $this->addItemToEntry($relationship, $this->getEffectedEntity($relationship, $results->added[0])); - } - foreach ($results->removed as $removedId) { if ($this->shouldProcessRelationship($relationship, $removedId)) { $this->removeItemFromEntry($relationship, $this->getEffectedEntity($relationship, $removedId)); } } + + if (! empty($results->added) && count($results->added) == 1 && $this->shouldProcessRelationship($relationship, $results->added[0])) { + $this->addItemToEntry($relationship, $this->getEffectedEntity($relationship, $results->added[0])); + } } } diff --git a/src/Processors/Concerns/ProcessesOneToMany.php b/src/Processors/Concerns/ProcessesOneToMany.php index 1b0ad37..babf56b 100644 --- a/src/Processors/Concerns/ProcessesOneToMany.php +++ b/src/Processors/Concerns/ProcessesOneToMany.php @@ -2,6 +2,7 @@ namespace Stillat\Relationships\Processors\Concerns; +use Statamic\Facades\Data; use Stillat\Relationships\Comparisons\ComparisonResult; use Stillat\Relationships\EntryRelationship; @@ -9,16 +10,33 @@ trait ProcessesOneToMany { protected function processOneToMany(ComparisonResult $results, EntryRelationship $relationship) { - foreach ($results->added as $addedId) { - if ($this->shouldProcessRelationship($relationship, $addedId)) { - $this->setFieldValue($relationship, $this->getEffectedEntity($relationship, $addedId)); - } - } - foreach ($results->removed as $removedId) { if ($this->shouldProcessRelationship($relationship, $removedId)) { + $this->dependencies[] = $removedId; + $this->dependencies[] = $this->getDependency($relationship, $removedId); $this->removeFieldValue($relationship, $this->getEffectedEntity($relationship, $removedId)); } } + + foreach ($results->added as $addedId) { + if ($this->shouldProcessRelationship($relationship, $addedId)) { + $this->dependencies[] = $addedId; + $dependent = Data::find($this->getDependency($relationship, $addedId)); + + if ($dependent !== null && $inverse = $relationship->getInverse()) { + $leftReference = $dependent->get($relationship->leftField); + + if (($key = array_search($addedId, $leftReference)) !== false) { + unset($leftReference[$key]); + + $dependent->set($relationship->leftField, array_values($leftReference)); + + $dependent->saveQuietly(); + } + } + + $this->setFieldValue($relationship, $this->getEffectedEntity($relationship, $addedId)); + } + } } } diff --git a/src/Processors/FillRelationshipsProcessor.php b/src/Processors/FillRelationshipsProcessor.php index 971ced1..978cb87 100644 --- a/src/Processors/FillRelationshipsProcessor.php +++ b/src/Processors/FillRelationshipsProcessor.php @@ -3,6 +3,7 @@ namespace Stillat\Relationships\Processors; use Statamic\Contracts\Entries\EntryRepository; +use Statamic\Facades\Taxonomy; use Stillat\Relationships\Comparisons\ComparisonResult; use Stillat\Relationships\EntryRelationship; use Stillat\Relationships\Processors\Concerns\GetsFieldValues; @@ -61,19 +62,10 @@ protected function fillRelationships($relationships) } } - protected function fillRelationship(EntryRelationship $relationship) + protected function processData($data, EntryRelationship $relationship) { - $collectionEntries = $this->entries->query() - ->whereIn('collection', [$relationship->leftCollection]) - ->where($relationship->leftField, '!=', null) - ->get(); - - if (count($collectionEntries) == 0) { - return; - } - - foreach ($collectionEntries as $entry) { - $related = $this->getFieldValue($relationship->leftField, $entry, null); + foreach ($data as $item) { + $related = $this->getFieldValue($relationship->leftField, $item, null); if ($related == null) { continue; @@ -86,8 +78,39 @@ protected function fillRelationship(EntryRelationship $relationship) $mockResults = new ComparisonResult(); $mockResults->added = $related; - $this->manager->processor()->setEntryId($entry->id()) + $this->manager->processor()->setEntryId($item->id()) ->processRelationship($relationship, $mockResults); } } + + protected function fillTaxonomyRelationship(EntryRelationship $relationship) + { + $terms = Taxonomy::find($relationship->taxonomyName)->queryTerms()->get(); + + if (count($terms) === 0) { + return; + } + + $this->processData($terms, $relationship); + } + + protected function fillRelationship(EntryRelationship $relationship) + { + if ($relationship->leftCollection === '[term]') { + $this->fillTaxonomyRelationship($relationship); + + return; + } + + $collectionEntries = $this->entries->query() + ->whereIn('collection', [$relationship->leftCollection]) + ->where($relationship->leftField, '!=', null) + ->get(); + + if (count($collectionEntries) == 0) { + return; + } + + $this->processData($collectionEntries, $relationship); + } } diff --git a/src/Processors/RelationshipProcessor.php b/src/Processors/RelationshipProcessor.php index e0d826c..ef11b4b 100644 --- a/src/Processors/RelationshipProcessor.php +++ b/src/Processors/RelationshipProcessor.php @@ -25,11 +25,11 @@ class RelationshipProcessor { - use ProcessesManyToMany, - ProcessesOneToOne, + use GetsFieldValues, + ProcessesManyToMany, ProcessesManyToOne, ProcessesOneToMany, - GetsFieldValues; + ProcessesOneToOne; /** * @var EntryRepository @@ -85,6 +85,17 @@ class RelationshipProcessor protected $isDry = false; + protected $observed = []; + + protected $processingRelationships = false; + + protected $dependencies = []; + + public function isProcessingRelationships() + { + return $this->processingRelationships; + } + public function setIsDeleting($isDeleting = true) { $this->isDelete = $isDeleting; @@ -164,7 +175,11 @@ protected function updateEntry($entry, $relationship) if (! $this->isDry) { if ($relationship->withEvents) { - $entry->save(); + if ($entry instanceof LocalizedTerm) { + $entry->term()->save(); + } else { + $entry->save(); + } } else { if ($entry instanceof LocalizedTerm) { $entry->term()->saveQuietly(); @@ -222,15 +237,24 @@ protected function getEffectedEntries($relationship, $entryIds) /** @var EntryCollection $entries */ $entries = $this->entries->query()->whereIn('id', $entryIds)->get(); - $this->effectedEntries = $entries->keyBy('id')->all(); + $this->effectedEntries = array_merge( + $this->effectedEntries, + $entries->keyBy('id')->all() + ); } elseif ($relationship->rightType == 'user') { $users = $this->getUsersByIds($entryIds); - $this->effectedUsers = $users->keyBy('id')->all(); + $this->effectedUsers = array_merge( + $this->effectedUsers, + $users->keyBy('id')->all() + ); } elseif ($relationship->rightType == 'term') { $terms = $this->getTermsByIds($relationship, $entryIds); - $this->effectedTerms = $terms->keyBy('slug')->all(); + $this->effectedTerms = array_merge( + $this->effectedTerms, + $terms->keyBy('slug')->all() + ); } } @@ -285,9 +309,11 @@ public function process($relationships) public function processRelationship($relationship, $results) { + $this->processingRelationships = true; UpdatingRelationshipsEvent::dispatch($relationship, $results); if (! $results->hasChanges()) { + $this->processingRelationships = false; UpdatedRelationshipsEvent::dispatch($relationship, $results); return; @@ -304,6 +330,7 @@ public function processRelationship($relationship, $results) } elseif ($relationship->type == EntryRelationship::TYPE_MANY_TO_ONE) { $this->processManyToOne($results, $relationship); } + $this->processingRelationships = false; UpdatedRelationshipsEvent::dispatch($relationship, $results); } @@ -332,6 +359,25 @@ protected function shouldProcessRelationship(EntryRelationship $relationship, $i return true; } + protected function getDependency(EntryRelationship $relationship, $id) + { + $data = null; + + if ($relationship->rightType == 'entry') { + $data = $this->effectedEntries[$id]; + } elseif ($relationship->rightType == 'user') { + $data = $this->effectedUsers[$id]; + } elseif ($relationship->rightType == 'term') { + $data = $this->effectedTerms[$id]; + } + + if ($data === null || ! method_exists($data, 'get')) { + return null; + } + + return $data->get($relationship->rightField); + } + protected function getEffectedEntity(EntryRelationship $relationship, $id) { if ($relationship->rightType == 'entry') { @@ -446,7 +492,13 @@ protected function removeItemFromEntry($relationship, $entry) $this->updateEntry($entry, $relationship); } else { - $entry->set($relationship->rightField, null); + $value = null; + + if (is_array($rightReference) && count($rightReference) > 0) { + $value = $rightReference; + } + + $entry->set($relationship->rightField, $value); $this->updateEntry($entry, $relationship); } } diff --git a/src/RelationshipManager.php b/src/RelationshipManager.php index 1902275..11c669e 100644 --- a/src/RelationshipManager.php +++ b/src/RelationshipManager.php @@ -111,9 +111,9 @@ private function getRelationship($left, $right) } return $this->getRelationshipBuilder($left[1], $left[0]) - ->field($left[2], $left[0]) + ->field($left[2], $left[0]) ->isRelatedTo($right[1]) - ->through($right[2], $right[0]); + ->through($right[2], $right[0]); } private function buildOneToOneRelationships($relationships) @@ -125,7 +125,10 @@ private function buildOneToOneRelationships($relationships) $right = $this->getFieldDetails($relationship[1]); $builtRelationships[] = $this->getRelationship($left, $right)->oneToOne(); - $builtRelationships[] = $this->getRelationship($right, $left)->oneToOne()->isAutomaticInverse(); + $builtRelationships[] = $this->getRelationship($right, $left) + ->oneToOne() + ->isAutomaticInverse() + ->withOriginRelationship($builtRelationships[0]); } return new RelationshipProxy($builtRelationships); @@ -150,7 +153,10 @@ private function buildOneToManyRelationships($relationships) $right = $this->getFieldDetails($relationship[1]); $builtRelationships[] = $this->getRelationship($left, $right)->manyToOne(); - $builtRelationships[] = $this->getRelationship($right, $left)->oneToMany()->isAutomaticInverse(); + $builtRelationships[] = $this->getRelationship($right, $left) + ->oneToMany() + ->isAutomaticInverse() + ->withOriginRelationship($builtRelationships[0]); } return new RelationshipProxy($builtRelationships); @@ -175,7 +181,10 @@ private function buildManyToOneRelationships($relationships) $right = $this->getFieldDetails($relationship[1]); $builtRelationships[] = $this->getRelationship($left, $right)->oneToOne(); - $builtRelationships[] = $this->getRelationship($right, $left)->manyToOne()->isAutomaticInverse(); + $builtRelationships[] = $this->getRelationship($right, $left) + ->manyToOne() + ->isAutomaticInverse() + ->withOriginRelationship($builtRelationships[0]); } return new RelationshipProxy($builtRelationships); @@ -254,7 +263,10 @@ private function buildManyToManyRelationships($relationships) $right = $this->getFieldDetails($relationship[1]); $builtRelationships[] = $this->getRelationship($left, $right)->manyToMany(); - $builtRelationships[] = $this->getRelationship($right, $left)->manyToMany()->isAutomaticInverse(); + $builtRelationships[] = $this->getRelationship($right, $left) + ->manyToMany() + ->isAutomaticInverse() + ->withOriginRelationship($builtRelationships[0]); } return new RelationshipProxy($builtRelationships); diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index a8e6014..ecf1f21 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -62,7 +62,7 @@ protected function resolveApplicationConfiguration($app) ]; foreach ($configs as $config) { - $app['config']->set("statamic.$config", require(__DIR__."/__fixtures__/config/{$config}.php")); + $app['config']->set("statamic.$config", require (__DIR__."/__fixtures__/config/{$config}.php")); } $app['config']->set('statamic.antlers.version', 'runtime'); diff --git a/tests/ManyToManyTest.php b/tests/ManyToManyTest.php index 0fb5d29..7f7fefb 100644 --- a/tests/ManyToManyTest.php +++ b/tests/ManyToManyTest.php @@ -37,6 +37,36 @@ public function test_many_to_many_relationship() $this->assertSame([], Entry::find('sponsors-2')->get('sponsoring', [])); } + public function test_many_to_many_relationship_with_events() + { + Relate::clear() + ->manyToMany('conferences.sponsors', 'sponsors.sponsoring') + ->withEvents(); + + Entry::find('sponsors-1')->set('sponsoring', [ + 'conferences-1', + 'conferences-2', + ])->save(); + + Entry::find('sponsors-2')->set('sponsoring', [ + 'conferences-2', + ])->save(); + + $this->assertSame(['sponsors-1', 'sponsors-2'], Entry::find('conferences-2')->get('sponsors')); + $this->assertSame(['sponsors-1'], Entry::find('conferences-1')->get('sponsors')); + + Entry::find('conferences-1')->set('sponsors', ['sponsors-2'])->save(); + + $this->assertSame(['conferences-2'], Entry::find('sponsors-1')->get('sponsoring')); + $this->assertSame(['conferences-2', 'conferences-1'], Entry::find('sponsors-2')->get('sponsoring')); + + Entry::find('conferences-1')->set('sponsors', [])->save(); + Entry::find('conferences-2')->set('sponsors', [])->save(); + + $this->assertSame([], Entry::find('sponsors-1')->get('sponsoring', [])); + $this->assertSame([], Entry::find('sponsors-2')->get('sponsoring', [])); + } + public function test_many_to_many_user_relationships() { Relate::clear() diff --git a/tests/OneToManyTest.php b/tests/OneToManyTest.php index 134b29a..e30afa8 100644 --- a/tests/OneToManyTest.php +++ b/tests/OneToManyTest.php @@ -37,15 +37,15 @@ public function test_explicit_one_to_many() { Relate::clear(); Relate::collection('books') - ->field('author') + ->field('author') ->isRelatedTo('authors') - ->through('books') + ->through('books') ->manyToOne(); Relate::collection('authors') - ->field('books') + ->field('books') ->isRelatedTo('books') - ->through('author') + ->through('author') ->oneToMany(); Entry::find('books-1')->set('author', 'authors-1')->save(); @@ -117,4 +117,54 @@ public function test_one_to_many_term_relationships() $this->assertSame([], $this->getTerm('topics-one')->get('posts', [])); $this->assertSame([], $this->getTerm('topics-two')->get('posts', [])); } + + public function test_one_to_many_updates_dependents() + { + Relate::clear(); + Relate::oneToMany( + 'books.author', + 'authors.books' + ); + + Entry::find('books-1')->set('author', 'authors-1')->save(); + Entry::find('books-2')->set('author', 'authors-1')->save(); + Entry::find('books-3')->set('author', 'authors-1')->save(); + + Entry::find('books-4')->set('author', 'authors-1')->save(); + Entry::find('books-5')->set('author', 'authors-2')->save(); + + Entry::find('authors-1')->set('books', [ + 'books-1', + 'books-2', + 'books-3', + ])->save(); + + Entry::find('authors-2')->set('books', [ + 'books-4', + 'books-5', + ])->save(); + + Entry::find('authors-1')->set('books', [ + 'books-1', + 'books-2', + 'books-3', + 'books-4', + ])->save(); + + $this->assertSame(['books-5'], Entry::find('authors-2')->get('books')); + + $this->assertSame([ + 'books-1', + 'books-2', + 'books-3', + 'books-4', + ], Entry::find('authors-1')->get('books')); + + $this->assertSame('authors-1', Entry::find('books-1')->get('author')); + $this->assertSame('authors-1', Entry::find('books-2')->get('author')); + $this->assertSame('authors-1', Entry::find('books-3')->get('author')); + $this->assertSame('authors-1', Entry::find('books-4')->get('author')); + + $this->assertSame('authors-2', Entry::find('books-5')->get('author')); + } } diff --git a/tests/RelationshipTestCase.php b/tests/RelationshipTestCase.php index 1f5ed14..b0381d9 100644 --- a/tests/RelationshipTestCase.php +++ b/tests/RelationshipTestCase.php @@ -157,6 +157,15 @@ protected function createCollectionEntries() [ 'title' => 'Book Two', ], + [ + 'title' => 'Book Three', + ], + [ + 'title' => 'Book Four', + ], + [ + 'title' => 'Book Five', + ], ]); $this->createEntries('conferences', [