diff --git a/src/AbstractMapper.php b/src/AbstractMapper.php index 26ab3ee..992e2eb 100644 --- a/src/AbstractMapper.php +++ b/src/AbstractMapper.php @@ -80,9 +80,9 @@ public function markTracked(object $entity, Collection $collection): bool public function persist(object $object, Collection $onCollection): object { - $next = $onCollection->next; - if ($onCollection instanceof Filtered && $next !== null) { - $this->persist($object, $next); + $connectsTo = $onCollection->connectsTo; + if ($onCollection instanceof Filtered && $connectsTo !== null) { + $this->persist($object, $connectsTo); return $object; } @@ -103,6 +103,7 @@ public function persist(object $object, Collection $onCollection): object $this->pending[$object] = 'insert'; $this->markTracked($object, $onCollection); + $this->registerInIdentityMap($object, $onCollection); return $object; } @@ -123,21 +124,9 @@ public function isTracked(object $entity): bool return $this->tracked->offsetExists($entity); } - public function replaceTracked(object $old, object $new, Collection $onCollection): void - { - $op = $this->pending[$old] ?? 'update'; - $this->tracked->offsetUnset($old); - $this->pending->offsetUnset($old); - $this->evictFromIdentityMap($old, $onCollection); - - $this->markTracked($new, $onCollection); - $this->registerInIdentityMap($new, $onCollection); - $this->pending[$new] = $op; - } - public function registerCollection(string $alias, Collection $collection): void { - $collection->mapper = $this; + $collection->bindMapper($this); $this->collections[$alias] = $collection; } @@ -199,7 +188,7 @@ protected function evictFromIdentityMap(object $entity, Collection $coll): void protected function findInIdentityMap(Collection $collection): object|null { - if ($collection->name === null || !is_scalar($collection->condition) || $collection->more) { + if ($collection->name === null || !is_scalar($collection->condition) || $collection->hasMore) { return null; } @@ -234,6 +223,8 @@ private function tryMergeWithIdentityMap(object $entity, Collection $coll): obje $this->entityFactory->set($entity, $idName, $idValue); } + $op = $this->pending[$existing] ?? 'update'; + if ($this->entityFactory->isReadOnly($existing)) { $merged = $this->entityFactory->mergeEntities($existing, $entity); @@ -245,7 +236,7 @@ private function tryMergeWithIdentityMap(object $entity, Collection $coll): obje $this->registerInIdentityMap($merged, $coll); } - $this->pending[$merged] = 'update'; + $this->pending[$merged] = $op; return $merged; } @@ -258,7 +249,7 @@ private function tryMergeWithIdentityMap(object $entity, Collection $coll): obje $this->markTracked($existing, $coll); } - $this->pending[$existing] = 'update'; + $this->pending[$existing] = $op; return $existing; } @@ -290,9 +281,8 @@ public function __get(string $name): Collection } $coll = new Collection($name); - $coll->mapper = $this; - return $coll; + return $coll->bindMapper($this); } public function __isset(string $alias): bool @@ -310,14 +300,10 @@ public function __call(string $name, array $arguments): Collection { if (isset($this->collections[$name])) { $collection = clone $this->collections[$name]; - $collection->mapper = $this; - return $collection->with(...$arguments); + return $collection->bindMapper($this)->with(...$arguments); } - $collection = Collection::__callstatic($name, $arguments); - $collection->mapper = $this; - - return $collection; + return Collection::__callstatic($name, $arguments)->bindMapper($this); } } diff --git a/src/CollectionIterator.php b/src/CollectionIterator.php index 05e36f7..178cefd 100644 --- a/src/CollectionIterator.php +++ b/src/CollectionIterator.php @@ -51,15 +51,15 @@ public function key(): string public function hasChildren(): bool { - return $this->current()->more; + return $this->current()->hasMore; } public function getChildren(): RecursiveArrayIterator { $c = $this->current(); $pool = $c->hasChildren ? $c->children : []; - if ($c->next !== null) { - $pool[] = $c->next; + if ($c->connectsTo !== null) { + $pool[] = $c->connectsTo; } return new static( diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php index 89dbf57..a344cb5 100644 --- a/src/Collections/Collection.php +++ b/src/Collections/Collection.php @@ -14,13 +14,13 @@ class Collection implements ArrayAccess { public private(set) bool $required = true; - public AbstractMapper|null $mapper = null; + public private(set) AbstractMapper|null $mapper = null; - public Hydrator|null $hydrator = null; + public private(set) Hydrator|null $hydrator = null; public private(set) Collection|null $parent = null; - public private(set) Collection|null $next = null; + public private(set) Collection|null $connectsTo = null; private Collection|null $last = null; @@ -29,7 +29,7 @@ class Collection implements ArrayAccess public bool $hasChildren { get => !empty($this->children); } - public bool $more { get => $this->hasChildren || $this->next !== null; } + public bool $hasMore { get => $this->hasChildren || $this->connectsTo !== null; } /** @var array|scalar|null */ public private(set) array|int|float|string|bool|null $condition = []; @@ -50,22 +50,9 @@ public function addChild(Collection $child): void $this->children[] = $clone; } - public function persist(object $object, mixed ...$changes): object + public function persist(object $object): object { - $mapper = $this->resolveMapper(); - - if ($changes) { - $original = $object; - $object = $mapper->entityFactory->withChanges($original, ...$changes); - - if ($mapper->isTracked($original)) { - $mapper->replaceTracked($original, $object, $this); - - return $object; - } - } - - return $mapper->persist($object, $this); + return $this->resolveMapper()->persist($object, $this); } public function remove(object $object): bool @@ -106,6 +93,14 @@ public function offsetUnset(mixed $offset): void // no-op } + /** @internal Used by AbstractMapper to bind this collection */ + public function bindMapper(AbstractMapper $mapper): static + { + $this->mapper = $mapper; + + return $this; + } + public function hydrateFrom(Hydrator $hydrator): static { $this->hydrator = $hydrator; @@ -116,7 +111,7 @@ public function hydrateFrom(Hydrator $hydrator): static public function stack(Collection $collection): static { $tail = $this->last ?? $this; - $tail->setNext($collection); + $tail->setConnectsTo($collection); $this->last = $collection->last ?? $collection; return $this; @@ -151,10 +146,10 @@ private function resolveMapper(): AbstractMapper return $this->findMapper() ?? throw new CollectionNotBound($this->name); } - private function setNext(Collection $collection): void + private function setConnectsTo(Collection $collection): void { $collection->parent = $this; - $this->next = $collection; + $this->connectsTo = $collection; } /** @param array $arguments */ @@ -187,9 +182,9 @@ public function __call(string $name, array $children): static public function __clone(): void { - if ($this->next !== null) { - $this->next = clone $this->next; - $this->next->parent = $this; + if ($this->connectsTo !== null) { + $this->connectsTo = clone $this->connectsTo; + $this->connectsTo->parent = $this; } $clonedChildren = []; @@ -209,8 +204,8 @@ public function __clone(): void $node = $this; - while ($node->next !== null) { - $node = $node->next; + while ($node->connectsTo !== null) { + $node = $node->connectsTo; } $this->last = $node !== $this ? $node : null; diff --git a/src/EntityFactory.php b/src/EntityFactory.php index d9027e4..7683a63 100644 --- a/src/EntityFactory.php +++ b/src/EntityFactory.php @@ -11,9 +11,7 @@ use ReflectionUnionType; use function array_key_exists; -use function array_keys; use function class_exists; -use function implode; use function is_array; use function is_bool; use function is_float; @@ -124,41 +122,6 @@ public function create(string $class, mixed ...$properties): object return $entity; } - public function withChanges(object $entity, mixed ...$changes): object - { - $clone = $this->reflectClass($entity::class)->newInstanceWithoutConstructor(); - $styledChanges = []; - foreach ($changes as $prop => $value) { - $styledChanges[$this->style->styledProperty((string) $prop)] = $value; - } - - foreach ($this->reflectProperties($entity::class) as $name => $prop) { - if (array_key_exists($name, $styledChanges)) { - $value = $styledChanges[$name]; - $coerced = $this->coerce($prop, $value); - - if ($coerced === null && !($prop->getType()?->allowsNull() ?? false)) { - throw new DomainException( - 'Invalid value for ' . $entity::class . '::$' . $name, - ); - } - - $prop->setValue($clone, $coerced); - unset($styledChanges[$name]); - } elseif ($prop->isInitialized($entity)) { - $prop->setValue($clone, $prop->getValue($entity)); - } - } - - if ($styledChanges) { - throw new DomainException( - 'Unknown properties for ' . $entity::class . ': ' . implode(', ', array_keys($styledChanges)), - ); - } - - return $clone; - } - public function mergeEntities(object $base, object $overlay): object { if ($base::class !== $overlay::class) { diff --git a/src/Hydrators/Nested.php b/src/Hydrators/Nested.php index 2c76d5f..8a45158 100644 --- a/src/Hydrators/Nested.php +++ b/src/Hydrators/Nested.php @@ -59,8 +59,8 @@ private function hydrateNode( $entities[$entity] = $collection; - if ($collection->next !== null) { - $this->hydrateChild($data, $collection->next, $entityFactory, $entities); + if ($collection->connectsTo !== null) { + $this->hydrateChild($data, $collection->connectsTo, $entityFactory, $entities); } foreach ($collection->children as $child) { diff --git a/tests/AbstractMapperTest.php b/tests/AbstractMapperTest.php index 30b7e03..306c002 100644 --- a/tests/AbstractMapperTest.php +++ b/tests/AbstractMapperTest.php @@ -72,8 +72,8 @@ public function magicCallShouldBypassToCollection(): void { $collection = $this->mapper->author()->post()->comment(); $this->assertEquals('author', $collection->name); - $this->assertEquals('post', $collection->next?->name); - $this->assertEquals('comment', $collection->next?->next?->name); + $this->assertEquals('post', $collection->connectsTo?->name); + $this->assertEquals('comment', $collection->connectsTo?->connectsTo?->name); } #[Test] @@ -81,8 +81,8 @@ public function magicGetterShouldBypassToCollection(): void { $collection = $this->mapper->author->post->comment; $this->assertEquals('author', $collection->name); - $this->assertEquals('post', $collection->next?->name); - $this->assertEquals('comment', $collection->next?->next?->name); + $this->assertEquals('post', $collection->connectsTo?->name); + $this->assertEquals('comment', $collection->connectsTo?->connectsTo?->name); } #[Test] @@ -352,7 +352,7 @@ public function callingRegisteredChainedCollectionDoesNotMutateTemplate(): void $original = $mapper->commentedPosts; $this->assertNull( - $original->next?->next, + $original->connectsTo?->connectsTo, 'Stacking on a clone should not mutate the registered collection', ); } @@ -375,7 +375,7 @@ public function filteredPersistDelegatesToParentCollection(): void } #[Test] - public function filteredWithoutNextFallsBackToNormalPersist(): void + public function filteredWithoutConnectsToFallsBackToNormalPersist(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); $mapper->seed('post', []); @@ -1068,7 +1068,7 @@ public function readOnlyReplaceWithNewRelation(): void } #[Test] - public function withChangesAndPersistAutoUpdatesViaIdentityMap(): void + public function partialEntityPersistAutoUpdatesViaIdentityMap(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('post', [ @@ -1079,11 +1079,11 @@ public function withChangesAndPersistAutoUpdatesViaIdentityMap(): void ['id' => 20, 'name' => 'Bob', 'bio' => null], ]); - $post = $mapper->post->author->fetch(); + $mapper->post->author->fetch(); $bob = $mapper->author[20]->fetch(); - // withChanges preserves PK → persist auto-detects update via identity map - $updated = $mapper->entityFactory->withChanges($post, title: 'Changed', author: $bob); + // Partial entity with same PK → persist auto-detects update via identity map + $updated = $mapper->entityFactory->create(Stubs\Immutable\Post::class, id: 1, title: 'Changed', author: $bob); $mapper->post->persist($updated); $mapper->flush(); @@ -1182,37 +1182,37 @@ public function readOnlyDeleteEvictsFromIdentityMap(): void } #[Test] - public function persistWithChangesOnPendingInsertReplacesOriginal(): void + public function persistPartialEntityOnPendingInsertMergesViaIdentityMap(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('author', []); - $author = $mapper->entityFactory->create(Stubs\Immutable\Author::class, name: 'Alice'); + $author = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Alice'); $mapper->author->persist($author); - // Persist with changes on a pending-insert entity must replace, not duplicate - $updated = $mapper->author->persist($author, name: 'Bob'); + // Partial entity with same PK merges via identity map, does not duplicate + $updated = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Bob'); + $mapper->author->persist($updated); $mapper->flush(); $all = $mapper->author->fetchAll(); $this->assertCount(1, $all); $this->assertSame('Bob', $all[0]->name); - $this->assertFalse($mapper->isTracked($author)); - $this->assertTrue($mapper->isTracked($updated)); } #[Test] - public function persistWithChangesOnTrackedUpdateReplacesOriginal(): void + public function persistPartialEntityOnTrackedUpdateMerges(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); - $fetched = $mapper->author[1]->fetch(); + $mapper->author[1]->fetch(); - // Persist with changes on a tracked (fetched) entity - $mapper->author->persist($fetched, name: 'Bob'); + // Partial entity with same PK auto-detects update via identity map + $partial = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Bob'); + $mapper->author->persist($partial); $mapper->flush(); $mapper->clearIdentityMap(); diff --git a/tests/Collections/CollectionTest.php b/tests/Collections/CollectionTest.php index c1d5f10..e1c417e 100644 --- a/tests/Collections/CollectionTest.php +++ b/tests/Collections/CollectionTest.php @@ -85,8 +85,8 @@ public function dynamicGetterShouldStackCollection(): void $coll->someTest; $this->assertEquals( 'someTest', - $coll->next?->name, - 'First time should change next item', + $coll->connectsTo?->name, + 'First time should change connectsTo item', ); } @@ -97,14 +97,14 @@ public function dynamicGetterShouldChainCollection(): void $coll->someTest; $this->assertEquals( 'someTest', - $coll->next?->name, - 'First time should change next item', + $coll->connectsTo?->name, + 'First time should change connectsTo item', ); $coll->anotherTest; $this->assertEquals( 'someTest', - $coll->next?->name, - 'The next item on a chain should never be changed after first time', + $coll->connectsTo?->name, + 'The connectsTo item on a chain should never be changed after first time', ); } @@ -112,8 +112,8 @@ public function dynamicGetterShouldChainCollection(): void public function settingConditionViaDynamicOffsetShouldUseLastNode(): void { $foo = Collection::foo()->bar->baz[42]; - $bar = $foo->next; - $baz = $bar->next; + $bar = $foo->connectsTo; + $baz = $bar->connectsTo; $this->assertEmpty($foo->condition); $this->assertEmpty($bar->condition); $this->assertEquals(42, $baz->condition); @@ -128,9 +128,9 @@ public function dynamicMethodCallShouldAcceptChildren(): void Collection::children(), Collection::here(), ); - $next = $coll->next; - $this->assertNotNull($next); - $this->assertEquals(3, count($next->children)); + $connected = $coll->connectsTo; + $this->assertNotNull($connected); + $this->assertEquals(3, count($connected->children)); } #[Test] @@ -149,14 +149,14 @@ public function addChildShouldSetChildrenObjectProperties(): void public function childrenShouldMakeHasMoreTrue(): void { $coll = Collection::foo(Collection::thisIsAChildren()); - $this->assertTrue($coll->more); + $this->assertTrue($coll->hasMore); } #[Test] public function chainingShouldMakeHasMoreTrue(): void { $coll = Collection::foo()->barChain; - $this->assertTrue($coll->more); + $this->assertTrue($coll->hasMore); } #[Test] @@ -188,7 +188,7 @@ public function persistShouldPersistOnAttachedMapper(): void ->method('persist') ->with($persisted, $collection) ->willReturn($persisted); - $collection->mapper = $mapperMock; + $collection->bindMapper($mapperMock); $result = $collection->persist($persisted); $this->assertSame($persisted, $result); } @@ -203,7 +203,7 @@ public function removeShouldPersistOnAttachedMapper(): void ->method('remove') ->with($removed, $collection) ->willReturn(true); - $collection->mapper = $mapperMock; + $collection->bindMapper($mapperMock); $collection->remove($removed); } @@ -217,7 +217,7 @@ public function fetchShouldPersistOnAttachedMapper(): void ->method('fetch') ->with($collection) ->willReturn($result); - $collection->mapper = $mapperMock; + $collection->bindMapper($mapperMock); $collection->fetch(); } @@ -232,7 +232,7 @@ public function fetchShouldPersistOnAttachedMapperWithExtraParam(): void ->method('fetch') ->with($collection, $extra) ->willReturn($result); - $collection->mapper = $mapperMock; + $collection->bindMapper($mapperMock); $collection->fetch($extra); } @@ -245,7 +245,7 @@ public function fetchAllShouldPersistOnAttachedMapper(): void ->method('fetchAll') ->with($collection) ->willReturn([]); - $collection->mapper = $mapperMock; + $collection->bindMapper($mapperMock); $collection->fetchAll(); } @@ -259,7 +259,7 @@ public function fetchAllShouldPersistOnAttachedMapperWithExtraParam(): void ->method('fetchAll') ->with($collection, $extra) ->willReturn([]); - $collection->mapper = $mapperMock; + $collection->bindMapper($mapperMock); $collection->fetchAll($extra); } @@ -308,10 +308,10 @@ public function getParentShouldReturnNullWhenNoParent(): void } #[Test] - public function getNextShouldReturnNullWhenNoNext(): void + public function getConnectsToShouldReturnNullWhenNoConnectsTo(): void { $coll = new Collection('foo'); - $this->assertNull($coll->next); + $this->assertNull($coll->connectsTo); } #[Test] @@ -323,9 +323,9 @@ public function magicGetShouldUseRegisteredCollectionFromMapper(): void $mapperMock->method('__get')->with('bar')->willReturn($registered); $coll = new Collection('foo'); - $coll->mapper = $mapperMock; + $coll->bindMapper($mapperMock); $result = $coll->bar; - $this->assertEquals('bar', $result->next?->name); + $this->assertEquals('bar', $result->connectsTo?->name); } #[Test] @@ -337,7 +337,7 @@ public function hydrateFromSetsHydrator(): void } #[Test] - public function persistWithoutChangesReturnsSameEntity(): void + public function persistReturnsSameEntity(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('author', []); @@ -348,7 +348,7 @@ public function persistWithoutChangesReturnsSameEntity(): void } #[Test] - public function persistWithChangesReturnsModifiedCopy(): void + public function persistPartialEntityMergesViaIdentityMap(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('author', [ @@ -356,25 +356,28 @@ public function persistWithChangesReturnsModifiedCopy(): void ]); $fetched = $mapper->author[1]->fetch(); + $this->assertSame('Alice', $fetched->name); - $result = $mapper->author[1]->persist($fetched, name: 'Bob'); + $partial = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Bob'); + $result = $mapper->author->persist($partial); $this->assertNotSame($fetched, $result); $this->assertSame('Bob', $result->name); $this->assertSame(1, $result->id); - $this->assertSame('Alice', $fetched->name); } #[Test] - public function persistWithChangesFlushesUpdate(): void + public function persistPartialEntityFlushesUpdate(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); - $fetched = $mapper->author[1]->fetch(); - $mapper->author[1]->persist($fetched, name: 'Bob', bio: 'Writer'); + $mapper->author[1]->fetch(); + + $partial = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Bob', bio: 'Writer'); + $mapper->author->persist($partial); $mapper->flush(); $mapper->clearIdentityMap(); @@ -384,7 +387,7 @@ public function persistWithChangesFlushesUpdate(): void } #[Test] - public function persistWithChangesOnGraphUpdatesRelation(): void + public function persistPartialEntityOnGraphUpdatesRelation(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('post', [ @@ -395,13 +398,14 @@ public function persistWithChangesOnGraphUpdatesRelation(): void ['id' => 20, 'name' => 'Bob', 'bio' => null], ]); - $post = $mapper->post->author->fetch(); + $mapper->post->author->fetch(); $bob = $mapper->author[20]->fetch(); - $updated = $mapper->post->persist($post, title: 'Changed', author: $bob); + $updated = $mapper->entityFactory->create(Stubs\Immutable\Post::class, id: 1, title: 'Changed', author: $bob); + $result = $mapper->post->persist($updated); $mapper->flush(); - $this->assertSame(1, $updated->id); + $this->assertSame(1, $result->id); $mapper->clearIdentityMap(); $refetched = $mapper->post->author->fetch(); @@ -410,7 +414,7 @@ public function persistWithChangesOnGraphUpdatesRelation(): void } #[Test] - public function persistWithChangesNullValueApplied(): void + public function persistPartialEntityNullValueApplied(): void { $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); $mapper->seed('author', [ @@ -420,7 +424,8 @@ public function persistWithChangesNullValueApplied(): void $fetched = $mapper->author[1]->fetch(); $this->assertSame('has bio', $fetched->bio); - $mapper->author[1]->persist($fetched, bio: null); + $partial = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Alice', bio: null); + $mapper->author->persist($partial); $mapper->flush(); $mapper->clearIdentityMap(); diff --git a/tests/Collections/CompositeTest.php b/tests/Collections/CompositeTest.php index 9580b05..f47ed6f 100644 --- a/tests/Collections/CompositeTest.php +++ b/tests/Collections/CompositeTest.php @@ -20,7 +20,7 @@ public function collectionCanBeCreatedStaticallyWithChildren(): void $children2 = Composite::baz(['bat' => ['bar']])->bat(); $coll = Collection::foo($children1, $children2)->bar(); $this->assertInstanceOf(Collection::class, $coll); - $this->assertInstanceOf(Collection::class, $coll->next); + $this->assertInstanceOf(Collection::class, $coll->connectsTo); $this->assertInstanceOf(Composite::class, $children1); $this->assertInstanceOf(Composite::class, $children2); $this->assertTrue($coll->hasChildren); diff --git a/tests/Collections/FilteredTest.php b/tests/Collections/FilteredTest.php index d0312bc..b117d29 100644 --- a/tests/Collections/FilteredTest.php +++ b/tests/Collections/FilteredTest.php @@ -20,7 +20,7 @@ public function collectionCanBeCreatedStaticallyWithChildren(): void $children2 = Filtered::baz('bat')->bat(); $coll = Collection::foo($children1, $children2)->bar(); $this->assertInstanceOf(Collection::class, $coll); - $this->assertInstanceOf(Collection::class, $coll->next); + $this->assertInstanceOf(Collection::class, $coll->connectsTo); $this->assertInstanceOf(Filtered::class, $children1); $this->assertInstanceOf(Filtered::class, $children2); $this->assertTrue($coll->hasChildren); diff --git a/tests/Collections/TypedTest.php b/tests/Collections/TypedTest.php index 7b6abeb..246cf59 100644 --- a/tests/Collections/TypedTest.php +++ b/tests/Collections/TypedTest.php @@ -21,7 +21,7 @@ public function collectionCanBeCreatedStaticallyWithChildren(): void $children2 = Typed::baz('b')->bat(); $coll = Collection::foo($children1, $children2)->bar(); $this->assertInstanceOf(Collection::class, $coll); - $this->assertInstanceOf(Collection::class, $coll->next); + $this->assertInstanceOf(Collection::class, $coll->connectsTo); $this->assertInstanceOf(Typed::class, $children1); $this->assertInstanceOf(Typed::class, $children2); $this->assertTrue($coll->hasChildren); diff --git a/tests/EntityFactoryTest.php b/tests/EntityFactoryTest.php index aa1b628..c605ee3 100644 --- a/tests/EntityFactoryTest.php +++ b/tests/EntityFactoryTest.php @@ -384,115 +384,6 @@ public function extractColumnsResolvesReadOnlyRelationFk(): void $this->assertEquals('Test', $cols['title']); } - #[Test] - public function withChangesCreatesModifiedCopy(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $entity = new Stubs\ReadOnlyAuthor(id: 1, name: 'Alice', bio: 'bio'); - - $copy = $factory->withChanges($entity, name: 'Bob'); - assert($copy instanceof Stubs\ReadOnlyAuthor); - - $this->assertSame(1, $copy->id); - $this->assertSame('Bob', $copy->name); - $this->assertSame('bio', $copy->bio); - $this->assertSame('Alice', $entity->name); - } - - #[Test] - public function withChangesPreservesPkForIdentityMapLookup(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\Immutable\\'); - - $author = new Stubs\Immutable\Author(id: 5, name: 'Alice'); - - $post = new Stubs\Immutable\Post(id: 10, title: 'Hello', text: 'World', author: $author); - - $bob = new Stubs\Immutable\Author(id: 6, name: 'Bob'); - - $copy = $factory->withChanges($post, title: 'Changed', author: $bob); - assert($copy instanceof Stubs\Immutable\Post); - - $this->assertSame(10, $copy->id); - $this->assertSame('Changed', $copy->title); - $this->assertSame('World', $copy->text); - $this->assertInstanceOf(Stubs\Immutable\Author::class, $copy->author); - $this->assertSame('Bob', $copy->author->name); - $this->assertSame(6, $copy->author->id); - } - - #[Test] - public function withChangesWorksOnMutableEntities(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $author = new Stubs\Author(); - $author->id = 1; - $author->name = 'Alice'; - $author->bio = 'bio'; - - $copy = $factory->withChanges($author, name: 'Bob'); - assert($copy instanceof Stubs\Author); - $this->assertSame(1, $copy->id); - $this->assertSame('Bob', $copy->name); - $this->assertSame('bio', $copy->bio); - } - - #[Test] - public function withChangesThrowsOnUnknownProperty(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $entity = new Stubs\ReadOnlyAuthor(id: 1, name: 'Alice'); - - $this->expectException(DomainException::class); - $this->expectExceptionMessage('Unknown properties'); - $factory->withChanges($entity, nname: 'Bob'); - } - - #[Test] - public function withChangesAppliesNullValue(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $entity = new Stubs\ReadOnlyAuthor(id: 1, name: 'Alice', bio: 'has bio'); - - $copy = $factory->withChanges($entity, bio: null); - assert($copy instanceof Stubs\ReadOnlyAuthor); - $this->assertNull($copy->bio); - $this->assertSame('Alice', $copy->name); - $this->assertSame(1, $copy->id); - } - - #[Test] - public function withChangesPreservesUninitializedProperties(): void - { - $factory = new EntityFactory( - entityNamespace: __NAMESPACE__ . '\\Stubs\\', - ); - - $entity = $factory->create($factory->resolveClass('read_only_author')); - $factory->set($entity, 'name', 'Alice'); - // $id and $bio are uninitialized - - $copy = $factory->withChanges($entity, name: 'Bob'); - assert($copy instanceof Stubs\ReadOnlyAuthor); - $this->assertSame('Bob', $copy->name); - $this->assertFalse((new ReflectionProperty($copy, 'id'))->isInitialized($copy)); - $this->assertFalse((new ReflectionProperty($copy, 'bio'))->isInitialized($copy)); - } - - #[Test] - public function withChangesWithEmptyChangesReturnsCopy(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $entity = new Stubs\ReadOnlyAuthor(id: 1, name: 'Alice', bio: 'bio'); - - $copy = $factory->withChanges($entity); - assert($copy instanceof Stubs\ReadOnlyAuthor); - $this->assertNotSame($entity, $copy); - $this->assertSame(1, $copy->id); - $this->assertSame('Alice', $copy->name); - $this->assertSame('bio', $copy->bio); - } - #[Test] public function createAndCopyWorksOnReadOnlyEntity(): void { @@ -515,28 +406,6 @@ public function createAndCopyWorksOnReadOnlyEntity(): void $this->assertSame('Source', $entity->name); } - #[Test] - public function withChangesCoercesTypes(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $entity = new Stubs\ReadOnlyAuthor(id: 1, name: 'Alice'); - - $copy = $factory->withChanges($entity, name: 42); - assert($copy instanceof Stubs\ReadOnlyAuthor); - $this->assertSame('42', $copy->name); - } - - #[Test] - public function withChangesThrowsOnInvalidValue(): void - { - $factory = new EntityFactory(entityNamespace: __NAMESPACE__ . '\\Stubs\\'); - $entity = new Stubs\ReadOnlyAuthor(id: 1, name: 'Alice'); - - $this->expectException(DomainException::class); - $this->expectExceptionMessage('Invalid value'); - $factory->withChanges($entity, name: null); - } - #[Test] public function mergeEntitiesReturnsMergedCloneWhenPropertiesDiffer(): void { diff --git a/tests/InMemoryMapper.php b/tests/InMemoryMapper.php index 8a26b87..9620599 100644 --- a/tests/InMemoryMapper.php +++ b/tests/InMemoryMapper.php @@ -161,8 +161,8 @@ private function hydrateRow(array $row, Collection $collection): object|false /** @param array $parentRow */ private function attachRelated(array &$parentRow, Collection $collection): void { - if ($collection->next !== null) { - $this->attachChild($parentRow, $collection->next); + if ($collection->connectsTo !== null) { + $this->attachChild($parentRow, $collection->connectsTo); } foreach ($collection->children as $child) { @@ -187,7 +187,7 @@ private function attachChild(array &$parentRow, Collection $child): void return; } - if ($child->more) { + if ($child->hasMore) { $this->attachRelated($childRow, $child); }