diff --git a/src/AbstractMapper.php b/src/AbstractMapper.php index 992e2eb..0ebda23 100644 --- a/src/AbstractMapper.php +++ b/src/AbstractMapper.php @@ -30,10 +30,12 @@ abstract class AbstractMapper /** @var array */ private array $collections = []; + public EntityFactory $entityFactory { get => $this->hydrator->entityFactory; } + public Styles\Stylable $style { get => $this->entityFactory->style; } public function __construct( - public readonly EntityFactory $entityFactory = new EntityFactory(), + public readonly Hydrator $hydrator, ) { $this->tracked = new SplObjectStorage(); $this->pending = new SplObjectStorage(); @@ -130,8 +132,6 @@ public function registerCollection(string $alias, Collection $collection): void $this->collections[$alias] = $collection; } - abstract protected function defaultHydrator(Collection $collection): Hydrator; - /** * @param array $columns * @@ -153,11 +153,6 @@ protected function filterColumns(array $columns, Collection $collection): array return array_intersect_key($columns, array_flip([...$collection->filters, $id])); } - protected function resolveHydrator(Collection $collection): Hydrator - { - return $collection->hydrator ?? $this->defaultHydrator($collection); - } - protected function registerInIdentityMap(object $entity, Collection $coll): void { if ($coll->name === null) { diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php index a344cb5..92f0bac 100644 --- a/src/Collections/Collection.php +++ b/src/Collections/Collection.php @@ -7,7 +7,6 @@ use ArrayAccess; use Respect\Data\AbstractMapper; use Respect\Data\CollectionNotBound; -use Respect\Data\Hydrator; /** @implements ArrayAccess */ class Collection implements ArrayAccess @@ -16,8 +15,6 @@ class Collection implements ArrayAccess public private(set) AbstractMapper|null $mapper = null; - public private(set) Hydrator|null $hydrator = null; - public private(set) Collection|null $parent = null; public private(set) Collection|null $connectsTo = null; @@ -101,13 +98,6 @@ public function bindMapper(AbstractMapper $mapper): static return $this; } - public function hydrateFrom(Hydrator $hydrator): static - { - $this->hydrator = $hydrator; - - return $this; - } - public function stack(Collection $collection): static { $tail = $this->last ?? $this; diff --git a/src/Hydrator.php b/src/Hydrator.php index 4fc6770..7bed807 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -10,10 +10,17 @@ /** Transforms raw backend data into entity instances mapped to their collections */ interface Hydrator { - /** @return SplObjectStorage|false */ + public EntityFactory $entityFactory { get; } + + /** Returns just the root entity */ public function hydrate( mixed $raw, Collection $collection, - EntityFactory $entityFactory, + ): object|false; + + /** @return SplObjectStorage|false */ + public function hydrateAll( + mixed $raw, + Collection $collection, ): SplObjectStorage|false; } diff --git a/src/Hydrators/Base.php b/src/Hydrators/Base.php index 2acaf79..dbf09aa 100644 --- a/src/Hydrators/Base.php +++ b/src/Hydrators/Base.php @@ -4,6 +4,7 @@ namespace Respect\Data\Hydrators; +use DomainException; use Respect\Data\Collections\Collection; use Respect\Data\Collections\Typed; use Respect\Data\EntityFactory; @@ -13,10 +14,35 @@ /** Base hydrator providing collection-tree entity wiring */ abstract class Base implements Hydrator { + public function __construct( + public readonly EntityFactory $entityFactory = new EntityFactory(), + ) { + } + + public function hydrate( + mixed $raw, + Collection $collection, + ): object|false { + $entities = $this->hydrateAll($raw, $collection); + if ($entities === false) { + return false; + } + + foreach ($entities as $entity) { + if ($entities[$entity] === $collection) { + return $entity; + } + } + + throw new DomainException( + 'Hydration produced no entity for collection "' . $collection->name . '"', + ); + } + /** @param SplObjectStorage $entities */ - protected function wireRelationships(SplObjectStorage $entities, EntityFactory $entityFactory): void + protected function wireRelationships(SplObjectStorage $entities): void { - $style = $entityFactory->style; + $style = $this->entityFactory->style; $others = clone $entities; foreach ($entities as $entity) { @@ -40,12 +66,12 @@ protected function wireRelationships(SplObjectStorage $entities, EntityFactory $ continue; } - $id = $entityFactory->get($other, $style->identifier($otherColl->name)); + $id = $this->entityFactory->get($other, $style->identifier($otherColl->name)); if ($id === null) { continue; } - $entityFactory->set($entity, $relationName, $other); + $this->entityFactory->set($entity, $relationName, $other); } } } @@ -57,13 +83,12 @@ protected function wireRelationships(SplObjectStorage $entities, EntityFactory $ */ protected function resolveEntityClass( Collection $collection, - EntityFactory $entityFactory, object|array $row, ): string { if ($collection instanceof Typed) { - return $collection->resolveEntityClass($entityFactory, $row); + return $collection->resolveEntityClass($this->entityFactory, $row); } - return $entityFactory->resolveClass((string) $collection->name); + return $this->entityFactory->resolveClass((string) $collection->name); } } diff --git a/src/Hydrators/Nested.php b/src/Hydrators/Nested.php index 8a45158..ac89031 100644 --- a/src/Hydrators/Nested.php +++ b/src/Hydrators/Nested.php @@ -5,7 +5,6 @@ namespace Respect\Data\Hydrators; use Respect\Data\Collections\Collection; -use Respect\Data\EntityFactory; use SplObjectStorage; use function is_array; @@ -14,10 +13,9 @@ final class Nested extends Base { /** @return SplObjectStorage|false */ - public function hydrate( + public function hydrateAll( mixed $raw, Collection $collection, - EntityFactory $entityFactory, ): SplObjectStorage|false { if (!is_array($raw)) { return false; @@ -26,10 +24,10 @@ public function hydrate( /** @var SplObjectStorage $entities */ $entities = new SplObjectStorage(); - $this->hydrateNode($raw, $collection, $entityFactory, $entities); + $this->hydrateNode($raw, $collection, $entities); if ($entities->count() > 1) { - $this->wireRelationships($entities, $entityFactory); + $this->wireRelationships($entities); } return $entities; @@ -42,11 +40,10 @@ public function hydrate( private function hydrateNode( array $data, Collection $collection, - EntityFactory $entityFactory, SplObjectStorage $entities, ): void { - $entity = $entityFactory->create( - $this->resolveEntityClass($collection, $entityFactory, $data), + $entity = $this->entityFactory->create( + $this->resolveEntityClass($collection, $data), ); foreach ($data as $key => $value) { @@ -54,17 +51,17 @@ private function hydrateNode( continue; } - $entityFactory->set($entity, $key, $value); + $this->entityFactory->set($entity, $key, $value); } $entities[$entity] = $collection; if ($collection->connectsTo !== null) { - $this->hydrateChild($data, $collection->connectsTo, $entityFactory, $entities); + $this->hydrateChild($data, $collection->connectsTo, $entities); } foreach ($collection->children as $child) { - $this->hydrateChild($data, $child, $entityFactory, $entities); + $this->hydrateChild($data, $child, $entities); } } @@ -75,7 +72,6 @@ private function hydrateNode( private function hydrateChild( array $parentData, Collection $child, - EntityFactory $entityFactory, SplObjectStorage $entities, ): void { $key = $child->name; @@ -85,6 +81,6 @@ private function hydrateChild( /** @var array $childData */ $childData = $parentData[$key]; - $this->hydrateNode($childData, $child, $entityFactory, $entities); + $this->hydrateNode($childData, $child, $entities); } } diff --git a/src/Hydrators/PrestyledAssoc.php b/src/Hydrators/PrestyledAssoc.php index 9d9c8e9..83137bd 100644 --- a/src/Hydrators/PrestyledAssoc.php +++ b/src/Hydrators/PrestyledAssoc.php @@ -9,7 +9,6 @@ use Respect\Data\Collections\Collection; use Respect\Data\Collections\Composite; use Respect\Data\Collections\Filtered; -use Respect\Data\EntityFactory; use SplObjectStorage; use function array_keys; @@ -31,10 +30,9 @@ final class PrestyledAssoc extends Base private Collection|null $cachedCollection = null; /** @return SplObjectStorage|false */ - public function hydrate( + public function hydrateAll( mixed $raw, Collection $collection, - EntityFactory $entityFactory, ): SplObjectStorage|false { if (!$raw || !is_array($raw)) { return false; @@ -59,19 +57,19 @@ public function hydrate( if (!isset($instances[$basePrefix])) { $coll = $collMap[$basePrefix]; - $class = $this->resolveEntityClass($coll, $entityFactory, $props); - $instances[$basePrefix] = $entityFactory->create($class); + $class = $this->resolveEntityClass($coll, $props); + $instances[$basePrefix] = $this->entityFactory->create($class); $entities[$instances[$basePrefix]] = $coll; } $entity = $instances[$basePrefix]; foreach ($props as $prop => $value) { - $entityFactory->set($entity, $prop, $value, styled: true); + $this->entityFactory->set($entity, $prop, $value, styled: true); } } if ($entities->count() > 1) { - $this->wireRelationships($entities, $entityFactory); + $this->wireRelationships($entities); } return $entities; diff --git a/tests/AbstractMapperTest.php b/tests/AbstractMapperTest.php index 306c002..f0fe9cc 100644 --- a/tests/AbstractMapperTest.php +++ b/tests/AbstractMapperTest.php @@ -22,8 +22,8 @@ class AbstractMapperTest extends TestCase protected function setUp(): void { - $factory = new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\'); - $this->mapper = new class ($factory) extends AbstractMapper { + $hydrator = new Nested(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $this->mapper = new class ($hydrator) extends AbstractMapper { public function flush(): void { } @@ -38,11 +38,6 @@ public function fetchAll(Collection $collection, mixed $extra = null): array { return []; } - - protected function defaultHydrator(Collection $collection): Hydrator - { - return new Nested(); - } }; } @@ -104,7 +99,7 @@ public function getStyleShouldReturnSameInstanceOnSubsequentCalls(): void public function styleShouldComeFromEntityFactory(): void { $style = new CakePHP(); - $mapper = new class (new EntityFactory(style: $style)) extends AbstractMapper { + $mapper = new class (new Nested(new EntityFactory(style: $style))) extends AbstractMapper { public function flush(): void { } @@ -119,11 +114,6 @@ public function fetchAll(Collection $collection, mixed $extra = null): array { return []; } - - protected function defaultHydrator(Collection $collection): Hydrator - { - return new Nested(); - } }; $this->assertSame($style, $mapper->style); } @@ -223,7 +213,9 @@ public function magicGetShouldReturnNewCollectionWhenNotRegistered(): void #[Test] public function hydrationWiresRelatedEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Hello', 'post_id' => 5], ]); @@ -244,7 +236,9 @@ public function hydrationWiresRelatedEntity(): void #[Test] public function persistAfterHydrationPreservesRelation(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Hello', 'post_id' => 5], ]); @@ -269,7 +263,9 @@ public function persistAfterHydrationPreservesRelation(): void #[Test] public function hydrationWithNoMatchLeavesRelationNull(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Hello', 'post_id' => 999], ]); @@ -286,7 +282,9 @@ public function hydrationWithNoMatchLeavesRelationNull(): void #[Test] public function hydrationWiresRelationWithStringPk(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Hello', 'post_id' => 5], ]); @@ -304,7 +302,9 @@ public function hydrationWiresRelationWithStringPk(): void #[Test] public function callingRegisteredCollectionClonesAndAppliesCondition(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Hello'], ['id' => 2, 'title' => 'World'], @@ -325,7 +325,9 @@ public function callingRegisteredCollectionClonesAndAppliesCondition(): void #[Test] public function callingRegisteredCollectionWithoutConditionReturnsClone(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $coll = Filtered::posts('title'); $mapper->postTitles = $coll; @@ -340,7 +342,9 @@ public function callingRegisteredCollectionWithoutConditionReturnsClone(): void #[Test] public function callingRegisteredChainedCollectionDoesNotMutateTemplate(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', []); $mapper->seed('comment', []); @@ -360,7 +364,9 @@ public function callingRegisteredChainedCollectionDoesNotMutateTemplate(): void #[Test] public function filteredPersistDelegatesToParentCollection(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', []); $mapper->seed('author', []); $mapper->authorsWithPosts = Filtered::post()->author(); @@ -377,7 +383,9 @@ public function filteredPersistDelegatesToParentCollection(): void #[Test] public function filteredWithoutConnectsToFallsBackToNormalPersist(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', []); $post = new Stubs\Post(); @@ -392,7 +400,9 @@ public function filteredWithoutConnectsToFallsBackToNormalPersist(): void #[Test] public function filteredUpdatePersistsOnlyFilteredColumns(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body'], ]); @@ -414,7 +424,9 @@ public function filteredUpdatePersistsOnlyFilteredColumns(): void #[Test] public function filteredInsertPersistsOnlyFilteredColumns(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', []); $postTitles = Filtered::post('title'); @@ -434,7 +446,9 @@ public function filteredInsertPersistsOnlyFilteredColumns(): void #[Test] public function filterColumnsPassesThroughForPlainCollection(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body'], ]); @@ -455,7 +469,9 @@ public function filterColumnsPassesThroughForPlainCollection(): void #[Test] public function filterColumnsPassesThroughForEmptyFilters(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body'], ]); @@ -478,7 +494,9 @@ public function filterColumnsPassesThroughForEmptyFilters(): void #[Test] public function filterColumnsPassesThroughForIdentifierOnly(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body'], ]); @@ -501,7 +519,9 @@ public function filterColumnsPassesThroughForIdentifierOnly(): void #[Test] public function fetchPopulatesIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ['id' => 2, 'title' => 'Second'], @@ -519,7 +539,9 @@ public function fetchPopulatesIdentityMap(): void #[Test] public function fetchReturnsCachedEntityFromIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ]); @@ -533,7 +555,9 @@ public function fetchReturnsCachedEntityFromIdentityMap(): void #[Test] public function fetchAllPopulatesIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ['id' => 2, 'title' => 'Second'], @@ -546,7 +570,9 @@ public function fetchAllPopulatesIdentityMap(): void #[Test] public function flushInsertRegistersInIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', []); $entity = new Stubs\Post(); @@ -560,7 +586,9 @@ public function flushInsertRegistersInIdentityMap(): void #[Test] public function flushDeleteEvictsFromIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'To Delete'], ]); @@ -577,7 +605,9 @@ public function flushDeleteEvictsFromIdentityMap(): void #[Test] public function clearIdentityMapEmptiesMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ]); @@ -592,7 +622,9 @@ public function clearIdentityMapEmptiesMap(): void #[Test] public function resetDoesNotClearIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ]); @@ -607,7 +639,9 @@ public function resetDoesNotClearIdentityMap(): void #[Test] public function pendingOperationTypes(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Existing'], ]); @@ -641,7 +675,9 @@ public function pendingOperationTypes(): void #[Test] public function trackedCountReflectsTrackedEntities(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ]); @@ -655,7 +691,9 @@ public function trackedCountReflectsTrackedEntities(): void #[Test] public function registerSkipsEntityWithNullCollectionName(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $entity = new Stubs\Foo(); $entity->id = 1; @@ -670,7 +708,9 @@ public function registerSkipsEntityWithNullCollectionName(): void #[Test] public function registerSkipsEntityWithNoPkValue(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', []); // Entity with no 'id' set @@ -686,7 +726,9 @@ public function registerSkipsEntityWithNoPkValue(): void #[Test] public function deleteEvictsUsingTrackedCollection(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Test'], ]); @@ -704,7 +746,9 @@ public function deleteEvictsUsingTrackedCollection(): void #[Test] public function findInIdentityMapSkipsNonScalarCondition(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'First'], ]); @@ -721,7 +765,9 @@ public function findInIdentityMapSkipsNonScalarCondition(): void #[Test] public function findInIdentityMapSkipsCollectionWithChildren(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Hello', 'post_id' => 5], ]); @@ -737,7 +783,9 @@ public function findInIdentityMapSkipsCollectionWithChildren(): void #[Test] public function persistUntrackedEntityWithMatchingPkUpdates(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original'], ]); @@ -767,7 +815,9 @@ public function persistUntrackedEntityWithMatchingPkUpdates(): void #[Test] public function persistReadOnlyEntityInsertWorks(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('read_only_author', []); $entity = $mapper->entityFactory->create(Stubs\ReadOnlyAuthor::class, name: 'Alice'); @@ -781,7 +831,9 @@ public function persistReadOnlyEntityInsertWorks(): void #[Test] public function persistReadOnlyViaCollectionPkUpdates(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('read_only_author', [ ['id' => 1, 'name' => 'Original', 'bio' => null], ]); @@ -814,7 +866,9 @@ public function persistReadOnlyViaCollectionPkUpdates(): void #[Test] public function persistReadOnlyViaCollectionPkFlushUpdatesStorage(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('read_only_author', [ ['id' => 1, 'name' => 'Original', 'bio' => null], ]); @@ -835,7 +889,9 @@ public function persistReadOnlyViaCollectionPkFlushUpdatesStorage(): void #[Test] public function identityMapReplaceEvictsOldEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('read_only_author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -853,7 +909,9 @@ public function identityMapReplaceEvictsOldEntity(): void #[Test] public function identityMapReplaceFallsBackToInsertWhenNoPkMatch(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('read_only_author', []); // No identity map entries — should insert @@ -870,7 +928,9 @@ public function identityMapReplaceFallsBackToInsertWhenNoPkMatch(): void #[Test] public function identityMapReplaceDetachesPreviouslyPendingEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original'], ]); @@ -897,7 +957,9 @@ public function identityMapReplaceDetachesPreviouslyPendingEntity(): void #[Test] public function identityMapReplaceSkipsSameEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Test'], ]); @@ -917,7 +979,9 @@ public function identityMapReplaceSkipsSameEntity(): void #[Test] public function readOnlyNestedHydrationWiresRelation(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Great post', 'post_id' => 5], ]); @@ -939,7 +1003,9 @@ public function readOnlyNestedHydrationWiresRelation(): void #[Test] public function readOnlyNestedHydrationThreeLevels(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('comment', [ ['id' => 1, 'text' => 'Nice', 'post_id' => 5], ]); @@ -966,7 +1032,9 @@ public function readOnlyNestedHydrationThreeLevels(): void #[Test] public function readOnlyInsertWithRelationExtractsFk(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('post', []); $mapper->seed('author', []); @@ -1000,7 +1068,9 @@ public function readOnlyInsertWithRelationExtractsFk(): void #[Test] public function readOnlyReplaceViaCollectionPkPreservesRelation(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body', 'author_id' => 10], ]); @@ -1035,7 +1105,9 @@ public function readOnlyReplaceViaCollectionPkPreservesRelation(): void #[Test] public function readOnlyReplaceWithNewRelation(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body', 'author_id' => 10], ]); @@ -1070,7 +1142,9 @@ public function readOnlyReplaceWithNewRelation(): void #[Test] public function partialEntityPersistAutoUpdatesViaIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body', 'author_id' => 10], ]); @@ -1097,7 +1171,9 @@ public function partialEntityPersistAutoUpdatesViaIdentityMap(): void #[Test] public function readOnlyMultipleEntitiesFetchAllTracksAll(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ['id' => 2, 'name' => 'Bob', 'bio' => null], @@ -1125,7 +1201,9 @@ public function readOnlyMultipleEntitiesFetchAllTracksAll(): void #[Test] public function identityMapReplaceSkipsSetWhenPkAlreadyInitialized(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1145,7 +1223,9 @@ public function identityMapReplaceSkipsSetWhenPkAlreadyInitialized(): void #[Test] public function persistReturnsEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); // Insert path $entity = new Stubs\Post(); @@ -1162,7 +1242,9 @@ public function persistReturnsEntity(): void #[Test] public function readOnlyDeleteEvictsFromIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1184,7 +1266,9 @@ public function readOnlyDeleteEvictsFromIdentityMap(): void #[Test] public function persistPartialEntityOnPendingInsertMergesViaIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', []); $author = $mapper->entityFactory->create(Stubs\Immutable\Author::class, id: 1, name: 'Alice'); @@ -1203,7 +1287,9 @@ public function persistPartialEntityOnPendingInsertMergesViaIdentityMap(): void #[Test] public function persistPartialEntityOnTrackedUpdateMerges(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1223,7 +1309,9 @@ public function persistPartialEntityOnTrackedUpdateMerges(): void #[Test] public function mutableMergeAppliesOverlayPropertiesToExisting(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1250,7 +1338,9 @@ public function mutableMergeAppliesOverlayPropertiesToExisting(): void #[Test] public function readOnlyMergeNoDiffReturnsSameEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1269,7 +1359,9 @@ public function readOnlyMergeNoDiffReturnsSameEntity(): void #[Test] public function identityMapLookupNormalizesNumericStringCondition(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1284,7 +1376,9 @@ public function identityMapLookupNormalizesNumericStringCondition(): void #[Test] public function identityMapLookupReturnsNullForNonScalarCondition(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1299,7 +1393,9 @@ public function identityMapLookupReturnsNullForNonScalarCondition(): void #[Test] public function mutableMergeTracksExistingWhenNotYetTracked(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -1328,7 +1424,9 @@ public function mutableMergeTracksExistingWhenNotYetTracked(): void #[Test] public function mergeWithIdentityMapNormalizesConditionFallback(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); diff --git a/tests/Collections/CollectionTest.php b/tests/Collections/CollectionTest.php index e1c417e..6dd39a6 100644 --- a/tests/Collections/CollectionTest.php +++ b/tests/Collections/CollectionTest.php @@ -328,18 +328,12 @@ public function magicGetShouldUseRegisteredCollectionFromMapper(): void $this->assertEquals('bar', $result->connectsTo?->name); } - #[Test] - public function hydrateFromSetsHydrator(): void - { - $hydrator = new Nested(); - $coll = Collection::foo()->hydrateFrom($hydrator); - $this->assertSame($hydrator, $coll->hydrator); - } - #[Test] public function persistReturnsSameEntity(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', []); $entity = $mapper->entityFactory->create(Stubs\Immutable\Author::class, name: 'Alice'); @@ -350,7 +344,9 @@ public function persistReturnsSameEntity(): void #[Test] public function persistPartialEntityMergesViaIdentityMap(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -369,7 +365,9 @@ public function persistPartialEntityMergesViaIdentityMap(): void #[Test] public function persistPartialEntityFlushesUpdate(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => null], ]); @@ -389,7 +387,9 @@ public function persistPartialEntityFlushesUpdate(): void #[Test] public function persistPartialEntityOnGraphUpdatesRelation(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('post', [ ['id' => 1, 'title' => 'Original', 'text' => 'Body', 'author_id' => 10], ]); @@ -416,7 +416,9 @@ public function persistPartialEntityOnGraphUpdatesRelation(): void #[Test] public function persistPartialEntityNullValueApplied(): void { - $mapper = new InMemoryMapper(new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\')); + $mapper = new InMemoryMapper(new Nested(new EntityFactory( + entityNamespace: 'Respect\\Data\\Stubs\\Immutable\\', + ))); $mapper->seed('author', [ ['id' => 1, 'name' => 'Alice', 'bio' => 'has bio'], ]); diff --git a/tests/Hydrators/NestedTest.php b/tests/Hydrators/NestedTest.php index 7293f2c..ebf05aa 100644 --- a/tests/Hydrators/NestedTest.php +++ b/tests/Hydrators/NestedTest.php @@ -12,6 +12,7 @@ use Respect\Data\EntityFactory; #[CoversClass(Nested::class)] +#[CoversClass(Base::class)] class NestedTest extends TestCase { private Nested $hydrator; @@ -20,8 +21,8 @@ class NestedTest extends TestCase protected function setUp(): void { - $this->hydrator = new Nested(); $this->factory = new EntityFactory(entityNamespace: 'Respect\\Data\\Stubs\\'); + $this->hydrator = new Nested($this->factory); } #[Test] @@ -29,9 +30,9 @@ public function hydrateReturnsFalseForNonArray(): void { $collection = Collection::author(); - $this->assertFalse($this->hydrator->hydrate(null, $collection, $this->factory)); - $this->assertFalse($this->hydrator->hydrate(false, $collection, $this->factory)); - $this->assertFalse($this->hydrator->hydrate('string', $collection, $this->factory)); + $this->assertFalse($this->hydrator->hydrateAll(null, $collection)); + $this->assertFalse($this->hydrator->hydrateAll(false, $collection)); + $this->assertFalse($this->hydrator->hydrateAll('string', $collection)); } #[Test] @@ -40,7 +41,7 @@ public function hydrateSingleEntity(): void $raw = ['id' => 1, 'name' => 'Author Name']; $collection = Collection::author(); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(1, $result); @@ -62,7 +63,7 @@ public function hydrateWithNestedChild(): void $collection = Collection::post(); $collection->stack(Collection::author()); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(2, $result); @@ -75,7 +76,7 @@ public function hydrateWithMissingNestedKeyReturnsPartial(): void $collection = Collection::post(); $collection->stack(Collection::author()); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(1, $result); @@ -98,7 +99,7 @@ public function hydrateDeeplyNested(): void $post->stack(Collection::author()); $collection->stack($post); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(3, $result); @@ -117,7 +118,7 @@ public function hydrateWithChildren(): void $categoryColl = Collection::category(); $collection = Collection::post($authorColl, $categoryColl); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(3, $result); @@ -129,7 +130,7 @@ public function hydrateWithTypedCollection(): void $raw = ['id' => 1, 'title' => 'Issue', 'type' => 'Bug']; $collection = Typed::issue('type'); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(1, $result); @@ -143,7 +144,7 @@ public function hydrateChildWithNullNameIsSkipped(): void $collection = Collection::post(); $collection->addChild($child); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(1, $result); @@ -156,9 +157,46 @@ public function hydrateScalarNestedValueIsIgnored(): void $collection = Collection::post(); $collection->stack(Collection::author()); - $result = $this->hydrator->hydrate($raw, $collection, $this->factory); + $result = $this->hydrator->hydrateAll($raw, $collection); $this->assertNotFalse($result); $this->assertCount(1, $result); } + + #[Test] + public function hydrateReturnsFalseForInvalidInput(): void + { + $this->assertFalse($this->hydrator->hydrate(null, Collection::author())); + } + + #[Test] + public function hydrateReturnsRootEntity(): void + { + $raw = ['id' => 1, 'name' => 'Alice']; + $result = $this->hydrator->hydrate($raw, Collection::author()); + + $this->assertNotFalse($result); + $this->assertEquals(1, $this->factory->get($result, 'id')); + $this->assertEquals('Alice', $this->factory->get($result, 'name')); + } + + #[Test] + public function hydrateReturnsRootWithWiredRelation(): void + { + $raw = [ + 'id' => 1, + 'title' => 'Post', + 'author' => ['id' => 5, 'name' => 'Author'], + ]; + $collection = Collection::post(); + $collection->stack(Collection::author()); + + $result = $this->hydrator->hydrate($raw, $collection); + + $this->assertNotFalse($result); + $this->assertEquals(1, $this->factory->get($result, 'id')); + $author = $this->factory->get($result, 'author'); + $this->assertIsObject($author); + $this->assertEquals(5, $this->factory->get($author, 'id')); + } } diff --git a/tests/Hydrators/PrestyledAssocTest.php b/tests/Hydrators/PrestyledAssocTest.php index cda0014..0119e60 100644 --- a/tests/Hydrators/PrestyledAssocTest.php +++ b/tests/Hydrators/PrestyledAssocTest.php @@ -17,6 +17,7 @@ use Respect\Data\Stubs\Bug; #[CoversClass(PrestyledAssoc::class)] +#[CoversClass(Base::class)] class PrestyledAssocTest extends TestCase { private EntityFactory $factory; @@ -29,24 +30,23 @@ protected function setUp(): void #[Test] public function hydrateReturnsFalseForEmpty(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $coll = Collection::author(); - $this->assertFalse($hydrator->hydrate(null, $coll, $this->factory)); - $this->assertFalse($hydrator->hydrate([], $coll, $this->factory)); - $this->assertFalse($hydrator->hydrate(false, $coll, $this->factory)); + $this->assertFalse($hydrator->hydrateAll(null, $coll)); + $this->assertFalse($hydrator->hydrateAll([], $coll)); + $this->assertFalse($hydrator->hydrateAll(false, $coll)); } #[Test] public function hydrateSingleEntity(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $collection = Collection::author(); - $result = $hydrator->hydrate( + $result = $hydrator->hydrateAll( ['author__id' => 1, 'author__name' => 'Alice'], $collection, - $this->factory, ); $this->assertNotFalse($result); @@ -60,10 +60,10 @@ public function hydrateSingleEntity(): void #[Test] public function hydrateMultipleEntitiesFromJoinedRow(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $collection = Collection::author()->post; - $result = $hydrator->hydrate( + $result = $hydrator->hydrateAll( [ 'author__id' => 1, 'author__name' => 'Alice', @@ -72,7 +72,6 @@ public function hydrateMultipleEntitiesFromJoinedRow(): void 'post__author' => 1, ], $collection, - $this->factory, ); $this->assertNotFalse($result); @@ -92,10 +91,10 @@ public function hydrateMultipleEntitiesFromJoinedRow(): void #[Test] public function hydrateWiresRelationships(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $collection = Collection::post()->author; - $result = $hydrator->hydrate( + $result = $hydrator->hydrateAll( [ 'post__id' => 10, 'post__title' => 'Hello', @@ -104,7 +103,6 @@ public function hydrateWiresRelationships(): void 'author__name' => 'Alice', ], $collection, - $this->factory, ); $this->assertNotFalse($result); @@ -115,16 +113,38 @@ public function hydrateWiresRelationships(): void $this->assertEquals(1, $this->factory->get($author, 'id')); } + #[Test] + public function hydrateReturnsRootRegardlessOfColumnOrder(): void + { + $hydrator = new PrestyledAssoc($this->factory); + $collection = Collection::post()->author; + + // Author columns appear before post columns + $result = $hydrator->hydrate( + [ + 'author__id' => 1, + 'author__name' => 'Alice', + 'post__id' => 10, + 'post__title' => 'Hello', + 'post__author' => 1, + ], + $collection, + ); + + $this->assertNotFalse($result); + $this->assertEquals(10, $this->factory->get($result, 'id')); + $this->assertEquals('Hello', $this->factory->get($result, 'title')); + } + #[Test] public function hydrateResolvesTypedEntities(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $collection = Typed::issue('type'); - $result = $hydrator->hydrate( + $result = $hydrator->hydrateAll( ['issue__id' => 1, 'issue__type' => 'Bug', 'issue__title' => 'Bug Report'], $collection, - $this->factory, ); $this->assertNotFalse($result); @@ -135,15 +155,14 @@ public function hydrateResolvesTypedEntities(): void #[Test] public function hydrateSkipsUnfilteredFilteredCollections(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $filtered = Filtered::post(); $collection = Collection::author(); $collection->stack($filtered); - $result = $hydrator->hydrate( + $result = $hydrator->hydrateAll( ['author__id' => 1, 'author__name' => 'Alice'], $collection, - $this->factory, ); $this->assertNotFalse($result); @@ -153,10 +172,10 @@ public function hydrateSkipsUnfilteredFilteredCollections(): void #[Test] public function hydrateCompositeEntity(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $composite = Composite::author(['profile' => ['bio']])->post; - $result = $hydrator->hydrate( + $result = $hydrator->hydrateAll( [ 'author__id' => 1, 'author__name' => 'Alice', @@ -165,7 +184,6 @@ public function hydrateCompositeEntity(): void 'post__title' => 'Hello', ], $composite, - $this->factory, ); $this->assertNotFalse($result); @@ -179,18 +197,16 @@ public function hydrateCompositeEntity(): void #[Test] public function hydrateCachesCollMapAcrossRows(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $collection = Collection::author(); - $first = $hydrator->hydrate( + $first = $hydrator->hydrateAll( ['author__id' => 1, 'author__name' => 'Alice'], $collection, - $this->factory, ); - $second = $hydrator->hydrate( + $second = $hydrator->hydrateAll( ['author__id' => 2, 'author__name' => 'Bob'], $collection, - $this->factory, ); $this->assertNotFalse($first); @@ -200,15 +216,14 @@ public function hydrateCachesCollMapAcrossRows(): void #[Test] public function hydrateThrowsOnUnknownPrefix(): void { - $hydrator = new PrestyledAssoc(); + $hydrator = new PrestyledAssoc($this->factory); $collection = Collection::author(); $this->expectException(DomainException::class); $this->expectExceptionMessage('Unknown column prefix'); - $hydrator->hydrate( + $hydrator->hydrateAll( ['author__id' => 1, 'unknown__foo' => 'bar'], $collection, - $this->factory, ); } } diff --git a/tests/InMemoryMapper.php b/tests/InMemoryMapper.php index 9620599..b73dce7 100644 --- a/tests/InMemoryMapper.php +++ b/tests/InMemoryMapper.php @@ -5,7 +5,6 @@ namespace Respect\Data; use Respect\Data\Collections\Collection; -use Respect\Data\Hydrators\Nested; use function array_filter; use function array_merge; @@ -83,11 +82,6 @@ public function flush(): void $this->reset(); } - protected function defaultHydrator(Collection $collection): Hydrator - { - return new Nested(); - } - private function insertEntity(object $entity, Collection $collection, string $tableName, string $id): void { $row = $this->filterColumns( @@ -143,7 +137,7 @@ private function hydrateRow(array $row, Collection $collection): object|false { $this->attachRelated($row, $collection); - $entities = $this->resolveHydrator($collection)->hydrate($row, $collection, $this->entityFactory); + $entities = $this->hydrator->hydrateAll($row, $collection); if ($entities === false) { return false; } diff --git a/tests/Styles/CakePHP/CakePHPIntegrationTest.php b/tests/Styles/CakePHP/CakePHPIntegrationTest.php index e99b40a..bb238ed 100644 --- a/tests/Styles/CakePHP/CakePHPIntegrationTest.php +++ b/tests/Styles/CakePHP/CakePHPIntegrationTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Respect\Data\EntityFactory; +use Respect\Data\Hydrators\Nested; use Respect\Data\InMemoryMapper; use Respect\Data\Styles\CakePHP; @@ -20,10 +21,10 @@ class CakePHPIntegrationTest extends TestCase protected function setUp(): void { $this->style = new CakePHP(); - $this->mapper = new InMemoryMapper(new EntityFactory( + $this->mapper = new InMemoryMapper(new Nested(new EntityFactory( style: $this->style, entityNamespace: __NAMESPACE__ . '\\', - )); + ))); $this->mapper->seed('posts', [ ['id' => 5, 'title' => 'Post Title', 'text' => 'Post Text', 'author_id' => 1], diff --git a/tests/Styles/NorthWind/NorthWindIntegrationTest.php b/tests/Styles/NorthWind/NorthWindIntegrationTest.php index bf440e7..bb3b4c3 100644 --- a/tests/Styles/NorthWind/NorthWindIntegrationTest.php +++ b/tests/Styles/NorthWind/NorthWindIntegrationTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Respect\Data\EntityFactory; +use Respect\Data\Hydrators\Nested; use Respect\Data\InMemoryMapper; use Respect\Data\Styles\NorthWind; @@ -20,10 +21,10 @@ class NorthWindIntegrationTest extends TestCase protected function setUp(): void { $this->style = new NorthWind(); - $this->mapper = new InMemoryMapper(new EntityFactory( + $this->mapper = new InMemoryMapper(new Nested(new EntityFactory( style: $this->style, entityNamespace: __NAMESPACE__ . '\\', - )); + ))); $this->mapper->seed('Posts', [ ['PostID' => 5, 'Title' => 'Post Title', 'Text' => 'Post Text', 'AuthorID' => 1], diff --git a/tests/Styles/Plural/PluralIntegrationTest.php b/tests/Styles/Plural/PluralIntegrationTest.php index f60fab4..ce88fa7 100644 --- a/tests/Styles/Plural/PluralIntegrationTest.php +++ b/tests/Styles/Plural/PluralIntegrationTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Respect\Data\EntityFactory; +use Respect\Data\Hydrators\Nested; use Respect\Data\InMemoryMapper; use Respect\Data\Styles\Plural; @@ -20,10 +21,10 @@ class PluralIntegrationTest extends TestCase protected function setUp(): void { $this->style = new Plural(); - $this->mapper = new InMemoryMapper(new EntityFactory( + $this->mapper = new InMemoryMapper(new Nested(new EntityFactory( style: $this->style, entityNamespace: __NAMESPACE__ . '\\', - )); + ))); $this->mapper->seed('posts', [ ['id' => 5, 'title' => 'Post Title', 'text' => 'Post Text', 'author_id' => 1], diff --git a/tests/Styles/Sakila/SakilaIntegrationTest.php b/tests/Styles/Sakila/SakilaIntegrationTest.php index 5129a4d..d06c550 100644 --- a/tests/Styles/Sakila/SakilaIntegrationTest.php +++ b/tests/Styles/Sakila/SakilaIntegrationTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Respect\Data\EntityFactory; +use Respect\Data\Hydrators\Nested; use Respect\Data\InMemoryMapper; use Respect\Data\Styles\Sakila; @@ -20,10 +21,10 @@ class SakilaIntegrationTest extends TestCase protected function setUp(): void { $this->style = new Sakila(); - $this->mapper = new InMemoryMapper(new EntityFactory( + $this->mapper = new InMemoryMapper(new Nested(new EntityFactory( style: $this->style, entityNamespace: __NAMESPACE__ . '\\', - )); + ))); $this->mapper->seed('post', [ ['post_id' => 5, 'title' => 'Post Title', 'text' => 'Post Text', 'author_id' => 1],