diff --git a/src/Factory/EntityFactory.php b/src/Factory/EntityFactory.php index 3fd35739..97d76fd5 100644 --- a/src/Factory/EntityFactory.php +++ b/src/Factory/EntityFactory.php @@ -183,6 +183,34 @@ class: $entity[\Spameri\Elastic\Model\Insert\PrepareEntityArray::ENTITY_CLASS], ); } + } elseif ( + $attribute->getName() === \Spameri\Elastic\Mapping\ValueCollection::class + ) { + /** @var array{class: class-string} $arguments */ + $arguments = $attribute->getArguments(); + + // The mirror of PrepareEntityArray's ValueCollectionInterface + // branch, which writes value() for each member and so leaves a + // flat list of scalars in the document. Rebuilding one member + // per scalar is the whole of it; there are no nested properties + // to resolve, and looking for them under `field.*` is what this + // used to do by falling through to the tail below - producing an + // empty collection, silently, on every single read. + $propertyValue = new $propertyTypeName(); + + if (\is_array($value)) { + foreach ($value as $item) { + if ($item === null || $item === '') { + continue; + } + + $collectionValue = new $arguments['class']($item); + $propertyValue->add($collectionValue); + + $this->changeSet->markExisting($collectionValue); + } + } + } elseif ( $attribute->getName() === \Spameri\Elastic\Mapping\STIEntity::class ) { diff --git a/src/Mapping/ValueCollection.php b/src/Mapping/ValueCollection.php new file mode 100644 index 00000000..e3aed959 --- /dev/null +++ b/src/Mapping/ValueCollection.php @@ -0,0 +1,24 @@ + $genres + */ + public function __construct( + #[\Spameri\Elastic\Mapping\Entity(class: \Spameri\Elastic\Entity\Property\ElasticId::class)] + public \Spameri\Elastic\Entity\Property\ElasticIdInterface $id, + #[\Spameri\Elastic\Mapping\ValueCollection(class: \SpameriTests\Elastic\Data\Entity\Video\Details\Genre::class)] + public \SpameriTests\Elastic\Data\Entity\Video\Details\GenreCollection $genres, + ) + { + parent::__construct($id); + } + +} diff --git a/tests/SpameriTests/Elastic/Factory/EntityFactory/ValueCollectionTest.phpt b/tests/SpameriTests/Elastic/Factory/EntityFactory/ValueCollectionTest.phpt new file mode 100644 index 00000000..06d5f6c7 --- /dev/null +++ b/tests/SpameriTests/Elastic/Factory/EntityFactory/ValueCollectionTest.phpt @@ -0,0 +1,146 @@ +container->getByType(\Spameri\Elastic\EntityManager::class); + /** @var \Spameri\Elastic\Factory\EntityFactory $entityFactory */ + $entityFactory = $this->container->getByType(\Spameri\Elastic\Factory\EntityFactory::class); + /** @var \Spameri\Elastic\Model\Insert\PrepareEntityArray $prepareEntityArray */ + $prepareEntityArray = $this->container->getByType(\Spameri\Elastic\Model\Insert\PrepareEntityArray::class); + + $entity = new \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection( + new \Spameri\Elastic\Entity\Property\ElasticId('vc-1'), + new \SpameriTests\Elastic\Data\Entity\Video\Details\GenreCollection( + new \SpameriTests\Elastic\Data\Entity\Video\Details\Genre('Action'), + new \SpameriTests\Elastic\Data\Entity\Video\Details\Genre('Science Fiction'), + ), + ); + + $source = $prepareEntityArray->prepare($entity); + + // What the write side puts in the document: a flat list of scalars. + \Tester\Assert::same(['Action', 'Science Fiction'], $source['genres']); + + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + source: $source, + position: 0, index: '', type: '', id: 'vc-1', score: 0.0, version: 0, + ); + + /** @var \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection $hydrated */ + $hydrated = $entityFactory->create( + $hit, + \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection::class, + $entityManager, + ); + + $genres = []; + foreach ($hydrated->genres as $genre) { + $genres[] = $genre->value(); + } + + \Tester\Assert::same(['Action', 'Science Fiction'], $genres); + } + + + public function testAnEmptyValueCollectionStaysEmpty(): void + { + /** @var \Spameri\Elastic\EntityManager $entityManager */ + $entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class); + /** @var \Spameri\Elastic\Factory\EntityFactory $entityFactory */ + $entityFactory = $this->container->getByType(\Spameri\Elastic\Factory\EntityFactory::class); + + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + source: ['genres' => []], + position: 0, index: '', type: '', id: 'vc-2', score: 0.0, version: 0, + ); + + /** @var \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection $hydrated */ + $hydrated = $entityFactory->create( + $hit, + \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection::class, + $entityManager, + ); + + \Tester\Assert::same(0, \iterator_count($hydrated->genres->getIterator())); + } + + + public function testAnAbsentValueCollectionIsNotAnError(): void + { + /** @var \Spameri\Elastic\EntityManager $entityManager */ + $entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class); + /** @var \Spameri\Elastic\Factory\EntityFactory $entityFactory */ + $entityFactory = $this->container->getByType(\Spameri\Elastic\Factory\EntityFactory::class); + + // A document written before the field existed. It has to read as empty + // rather than throw, or one old document takes down a whole index. + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + source: [], + position: 0, index: '', type: '', id: 'vc-3', score: 0.0, version: 0, + ); + + /** @var \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection $hydrated */ + $hydrated = $entityFactory->create( + $hit, + \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection::class, + $entityManager, + ); + + \Tester\Assert::same(0, \iterator_count($hydrated->genres->getIterator())); + } + + + public function testNullMembersAreNotTurnedIntoValues(): void + { + /** @var \Spameri\Elastic\EntityManager $entityManager */ + $entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class); + /** @var \Spameri\Elastic\Factory\EntityFactory $entityFactory */ + $entityFactory = $this->container->getByType(\Spameri\Elastic\Factory\EntityFactory::class); + + $hit = new \Spameri\ElasticQuery\Response\Result\Hit( + source: ['genres' => ['Action', NULL, '', 'Drama']], + position: 0, index: '', type: '', id: 'vc-4', score: 0.0, version: 0, + ); + + /** @var \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection $hydrated */ + $hydrated = $entityFactory->create( + $hit, + \SpameriTests\Elastic\Data\Entity\EntityWithValueCollection::class, + $entityManager, + ); + + $genres = []; + foreach ($hydrated->genres as $genre) { + $genres[] = $genre->value(); + } + + \Tester\Assert::same(['Action', 'Drama'], $genres); + } + +} + +(new ValueCollectionTest())->run();