From f7cd3b17fda392ef56fabb37cf85aaa1445a4d79 Mon Sep 17 00:00:00 2001 From: Niclas Schirrmeister Date: Wed, 8 Apr 2020 14:16:17 +0200 Subject: [PATCH] add foreign_id_key option to use specific array key as id --- .gitignore | 3 +- .phpunit.result.cache | 1 - README.md | 8 ++++ src/IdAndAttributesCollection.php | 20 ++++++++-- src/OneToManySync.php | 2 +- tests/IdAndAttributesCollectionTest.php | 43 ++++++++++++++++++++ tests/OneToManySyncTest.php | 52 +++++++++++++++++++++++++ 7 files changed, 122 insertions(+), 7 deletions(-) delete mode 100644 .phpunit.result.cache diff --git a/.gitignore b/.gitignore index 808f8c5..896e906 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build composer.lock docs vendor -coverage \ No newline at end of file +coverage +.phpunit.result.cache diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index b8a0622..0000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -C:37:"PHPUnit\Runner\DefaultTestResultCache":9466:{a:2:{s:7:"defects";a:42:{s:74:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model";i:4;s:102:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_with_a_changes_given_fields";i:4;s:77:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_two_models";i:4;s:76:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_changes_model_data";i:3;s:76:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_changes_two_models";i:3;s:74:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_detaches_a_model";i:3;s:84:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_detaches_one_of_two_models";i:3;s:87:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_set_default_data_after_detach";i:3;s:77:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_detaches_two_models";i:3;s:99:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_detaches_another_one";i:4;s:118:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_detaching_is_false";i:4;s:126:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_using_syncWithoutDetaching";i:4;s:92:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_changes_and_detach_models";i:3;s:89:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_id_from_array_input";i:4;s:122:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_empty_additional_attributes_when_input_is_from_array";i:4;s:96:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_key_from_assoc_array_as_id";i:4;s:117:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_value_from_assoc_array_as_additional_attributes";i:4;s:102:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_inherits_from_laravel_collection";i:4;s:81:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_ids";i:4;s:103:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_keys_of_assoc_array_as_id";i:4;s:119:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_converts_items_to_IdAndAttributeContainer_objects";i:4;s:95:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_inherits_from_laravel_collection";i:4;s:74:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_ids";i:4;s:96:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_keys_of_assoc_array_as_id";i:4;s:112:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_converts_items_to_IdAndAttributeContainer_objects";i:4;s:87:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_id_from_array_input";i:4;s:120:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_empty_additional_attributes_when_input_is_from_array";i:4;s:94:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_key_from_assoc_array_as_id";i:4;s:115:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_value_from_assoc_array_as_additional_attributes";i:4;s:67:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model";i:4;s:95:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_with_a_changes_given_fields";i:4;s:70:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_two_models";i:4;s:69:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_changes_model_data";i:4;s:69:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_changes_two_models";i:4;s:67:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_detaches_a_model";i:4;s:77:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_detaches_one_of_two_models";i:4;s:80:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_set_default_data_after_detach";i:4;s:70:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_detaches_two_models";i:4;s:92:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_detaches_another_one";i:4;s:111:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_detaching_is_false";i:4;s:119:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_using_syncWithoutDetaching";i:4;s:85:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_changes_and_detach_models";i:4;}s:5:"times";a:46:{s:74:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model";d:0.016;s:102:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_with_a_changes_given_fields";d:0.009;s:77:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_two_models";d:0.009;s:76:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_changes_model_data";d:0.009;s:76:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_changes_two_models";d:0.01;s:74:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_detaches_a_model";d:0.008;s:84:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_detaches_one_of_two_models";d:0.009;s:87:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_set_default_data_after_detach";d:0.009;s:77:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_detaches_two_models";d:0.012;s:99:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_detaches_another_one";d:0.009;s:118:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_detaching_is_false";d:0.01;s:126:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_using_syncWithoutDetaching";d:0.009;s:92:"Elbgoods\LaravelSyncOneToMany\Tests\OneToManySyncTest::it_attaches_changes_and_detach_models";d:0.011;s:89:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_id_from_array_input";d:0.007;s:122:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_empty_additional_attributes_when_input_is_from_array";d:0.006;s:96:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_key_from_assoc_array_as_id";d:0.006;s:117:"Elbgoods\LaravelSyncOneToMany\Tests\RelatedRowInputDataTest::it_shows_value_from_assoc_array_as_additional_attributes";d:0.006;s:94:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_id_from_array_input";d:0.006;s:127:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_empty_additional_attributes_when_input_is_from_array";d:0.006;s:101:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_key_from_assoc_array_as_id";d:0.006;s:122:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_value_from_assoc_array_as_additional_attributes";d:0.007;s:102:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_inherits_from_laravel_collection";d:0.084;s:81:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_ids";d:0.007;s:103:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_keys_of_assoc_array_as_id";d:0.007;s:119:"Elbgoods\LaravelSyncOneToMany\Tests\IdAndAttributesCollectionTest::it_converts_items_to_IdAndAttributeContainer_objects";d:0.007;s:95:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_inherits_from_laravel_collection";d:0.086;s:74:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_ids";d:0.006;s:96:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_returns_keys_of_assoc_array_as_id";d:0.006;s:112:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesCollectionTest::it_converts_items_to_IdAndAttributeContainer_objects";d:0.008;s:87:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_id_from_array_input";d:0.006;s:120:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_empty_additional_attributes_when_input_is_from_array";d:0.006;s:94:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_key_from_assoc_array_as_id";d:0.006;s:115:"Elbgoods\SyncOneToMany\Tests\IdAndAttributesContainerTest::it_shows_value_from_assoc_array_as_additional_attributes";d:0.007;s:67:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model";d:0.015;s:95:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_with_a_changes_given_fields";d:0.008;s:70:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_two_models";d:0.011;s:69:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_changes_model_data";d:0.009;s:69:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_changes_two_models";d:0.009;s:67:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_detaches_a_model";d:0.008;s:77:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_detaches_one_of_two_models";d:0.01;s:80:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_set_default_data_after_detach";d:0.009;s:70:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_detaches_two_models";d:0.012;s:92:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_detaches_another_one";d:0.011;s:111:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_detaching_is_false";d:0.009;s:119:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_a_model_and_does_not_detach_when_using_syncWithoutDetaching";d:0.009;s:85:"Elbgoods\SyncOneToMany\Tests\OneToManySyncTest::it_attaches_changes_and_detach_models";d:0.012;}}} \ No newline at end of file diff --git a/README.md b/README.md index f40ccee..8f0b942 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ $user->tasks()->sync( ); ``` +Sync using values by given key as ids +``` php +$user->tasks()->sync([ + ['task_id' => 1, status' => 'wip', 'priority' => 1], + ['task_id' => 4, status' => 'finished', 'priority' => 3], +], [foreign_id_key => 'task_id']); +``` + Result is the same as the result of the sync method of `BelongsToMany`, an array with attach, detached and updated rows. ### Changelog diff --git a/src/IdAndAttributesCollection.php b/src/IdAndAttributesCollection.php index ee3d77a..a71fdcd 100644 --- a/src/IdAndAttributesCollection.php +++ b/src/IdAndAttributesCollection.php @@ -2,13 +2,15 @@ namespace Elbgoods\SyncOneToMany; +use Illuminate\Support\Arr; use Illuminate\Support\Collection as LaravelCollection; +use InvalidArgumentException; class IdAndAttributesCollection extends LaravelCollection { - public function __construct(array $items = []) + public function __construct(array $items = [], array $options = []) { - parent::__construct($this->convertItems($items)); + parent::__construct($this->convertItems($items, $options)); } public function getIds(): array @@ -18,9 +20,19 @@ public function getIds(): array })->toArray(); } - protected function convertItems(array $items): array + protected function convertItems(array $items, array $options): array { - return collect($items)->map(static function ($value, $key) { + return collect($items)->map(static function ($value, $key) use ($options) { + if (isset($options['foreign_id_key'])) { + $foreignIdKey = $options['foreign_id_key']; + + if (! isset($value[$foreignIdKey])) { + throw new InvalidArgumentException("Any value must have a {$foreignIdKey} field (foreign id key)"); + } + + return new IdAndAttributesContainer($value[$foreignIdKey], Arr::except($value, $foreignIdKey)); + } + return new IdAndAttributesContainer($key, $value); })->values()->toArray(); } diff --git a/src/OneToManySync.php b/src/OneToManySync.php index bbbd1fa..bce8485 100644 --- a/src/OneToManySync.php +++ b/src/OneToManySync.php @@ -21,7 +21,7 @@ public static function make(HasMany $hasMany, array $ids, array $options): self public function __construct(HasMany $hasMany, array $idsAndAttributes, array $options) { $this->hasMany = $hasMany; - $this->idsAndAttributes = new IdAndAttributesCollection($idsAndAttributes); + $this->idsAndAttributes = new IdAndAttributesCollection($idsAndAttributes, $options); $this->options = $options; } diff --git a/tests/IdAndAttributesCollectionTest.php b/tests/IdAndAttributesCollectionTest.php index 2d79243..4663091 100644 --- a/tests/IdAndAttributesCollectionTest.php +++ b/tests/IdAndAttributesCollectionTest.php @@ -5,6 +5,7 @@ use Elbgoods\SyncOneToMany\IdAndAttributesCollection; use Elbgoods\SyncOneToMany\IdAndAttributesContainer; use Illuminate\Support\Collection as LaravelCollection; +use InvalidArgumentException; final class IdAndAttributesCollectionTest extends TestCase { @@ -38,6 +39,20 @@ public function it_returns_keys_of_assoc_array_as_id(): void $this->assertArrayContainsExact([2, 4], $collection->getIds()); } + /** + * @test + */ + public function it_returns_given_foreign_ids_as_ids() + { + $collection = new IdAndAttributesCollection([ + ['task_id' => 2, 'status' => 'wip'], + ['task_id' => 4, 'status' => 'finished'], + ], [ + 'foreign_id_key' => 'task_id', + ]); + $this->assertArrayContainsExact([2, 4], $collection->getIds()); + } + /** * @test */ @@ -52,4 +67,32 @@ public function it_converts_items_to_IdAndAttributeContainer_objects(): void $this->assertEquals(4, $collection->first()->getId()); $this->assertEquals(['status' => 'wip'], $collection->first()->getAdditionalAttributes()); } + + /** + * @test + */ + public function it_converts_items_to_IdAndAttributeContainer_objects_2(): void + { + $collection = new IdAndAttributesCollection([ + ['task_id' => 4, 'status' => 'wip'], + ], ['foreign_id_key' => 'task_id']); + + $this->assertCount(1, $collection); + $this->assertInstanceOf(IdAndAttributesContainer::class, $collection->first()); + $this->assertEquals(4, $collection->first()->getId()); + $this->assertEquals(['status' => 'wip'], $collection->first()->getAdditionalAttributes()); + } + + /** + * @test + */ + public function it_raises_exception_when_foreign_id_key_is_missing(): void + { + $this->expectException(InvalidArgumentException::class); + + new IdAndAttributesCollection([ + ['task_id' => 4, 'status' => 'wip'], + ['status' => 'done'], + ], ['foreign_id_key' => 'task_id']); + } } diff --git a/tests/OneToManySyncTest.php b/tests/OneToManySyncTest.php index 7965e95..5be07da 100644 --- a/tests/OneToManySyncTest.php +++ b/tests/OneToManySyncTest.php @@ -353,4 +353,56 @@ public function it_attaches_changes_and_detach_models(): void $this->assertModelEquals($user, $task5->user); $this->assertEquals('wip', $task5->status); } + + /** + * @test + */ + public function it_syncs_model_by_given_key() + { + $user = factory(User::class)->create(); + $task1 = factory(Task::class)->create([ + 'user_id' => $user->id, + 'status' => 'wip', + ]); + $task2 = factory(Task::class)->create([ + 'user_id' => $user->id, + 'status' => 'finished', + ]); + $task3 = factory(Task::class)->create([ + 'user_id' => $user->id, + 'status' => 'wip', + ]); + $task4 = factory(Task::class)->create(); + $task5 = factory(Task::class)->create(); + + $result = $user->tasks()->sync([ + $task1->id => ['task_id' => $task1->id, 'status' => 'finished'], + $task2->id => ['task_id' => $task2->id, 'status' => 'finished'], + $task4->id => ['task_id' => $task4->id, 'status' => 'wip'], + $task5->id => ['task_id' => $task5->id, 'status' => 'wip'], + ], [ + 'foreign_id_key' => 'task_id', + ]); + + $this->assertAttached([$task4->id, $task5->id], $result); + $this->assertChanged([$task1->id, $task2->id], $result); + $this->assertDetached([$task3->id], $result); + + $task1->refresh(); + $task2->refresh(); + $task3->refresh(); + $task4->refresh(); + $task5->refresh(); + + $this->assertModelEquals($user, $task1->user); + $this->assertEquals('finished', $task1->status); + $this->assertModelEquals($user, $task2->user); + $this->assertEquals('finished', $task2->status); + $this->assertNull($task3->user); + $this->assertEquals('wip', $task3->status); + $this->assertModelEquals($user, $task4->user); + $this->assertEquals('wip', $task4->status); + $this->assertModelEquals($user, $task5->user); + $this->assertEquals('wip', $task5->status); + } }