diff --git a/src/product/Domain/Model/Price/Exception/InvalidPriceTypeCollectionException.php b/src/product/Domain/Model/Price/Exception/InvalidPriceTypeCollectionException.php new file mode 100644 index 00000000..c3bc435e --- /dev/null +++ b/src/product/Domain/Model/Price/Exception/InvalidPriceTypeCollectionException.php @@ -0,0 +1,20 @@ +assertAllPriceTypes($types); + $this->flippedTypeNames = array_flip($this->names()); + } + + private function assertAllPriceTypes(array $types): void + { + foreach ($types as $type) { + if (!$type instanceof PriceTypeInterface) { + throw InvalidPriceTypeCollectionException::becauseContainsNonPriceType($type); + } + } + } + + public function names(): array + { + return array_map(fn(PriceTypeInterface $t) => $t->name(), $this->types); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return new \ArrayIterator($this->types); + } + + public function has(string $priceType): bool + { + return array_key_exists($priceType, $this->flippedTypeNames); + } + + public function count(): int + { + return count($this->types); + } + + public function hasItems(): bool + { + return $this->count() > 0; + } +} diff --git a/src/product/price/PriceTypeInterface.php b/src/product/Domain/Model/Price/PriceTypeInterface.php similarity index 63% rename from src/product/price/PriceTypeInterface.php rename to src/product/Domain/Model/Price/PriceTypeInterface.php index 5fddfc52..38301775 100644 --- a/src/product/price/PriceTypeInterface.php +++ b/src/product/Domain/Model/Price/PriceTypeInterface.php @@ -1,6 +1,6 @@ assertCount(0, $collection); + $this->assertFalse($collection->hasItems()); + $this->assertFalse($collection->has('any')); + } + + public function testCountAndHasItems(): void + { + $type = $this->createPriceType('hourly'); + $collection = new PriceTypeCollection([$type]); + + $this->assertCount(1, $collection); + $this->assertTrue($collection->hasItems()); + } + + private function createPriceType(string $name): PriceTypeInterface + { + return new class($name) implements PriceTypeInterface { + public function __construct(private string $name) {} + public function name(): string { return $this->name; } + }; + } + + public function testHasReturnsTrueForExistingType(): void + { + $hourly = $this->createPriceType('hourly'); + $monthly = $this->createPriceType('monthly'); + + $collection = new PriceTypeCollection([$hourly, $monthly]); + + $this->assertTrue($collection->has('hourly')); + $this->assertTrue($collection->has('monthly')); + $this->assertFalse($collection->has('discount')); + } + + public function testIteratorReturnsAllTypes(): void + { + $types = [ + $this->createPriceType('hourly'), + $this->createPriceType('fixed'), + ]; + + $collection = new PriceTypeCollection($types); + $collectedNames = []; + + foreach ($collection as $type) { + $this->assertInstanceOf(PriceTypeInterface::class, $type); + $collectedNames[] = $type->name(); + } + + $this->assertSame(['hourly', 'fixed'], $collectedNames); + } + + public function testHandlesDuplicateNamesGracefully(): void + { + // Duplicates in the array should still work for iteration, though flipped array will only store last + $hourly1 = $this->createPriceType('hourly'); + $hourly2 = $this->createPriceType('hourly'); + $collection = new PriceTypeCollection([$hourly1, $hourly2]); + + // Both objects exist in types + $this->assertCount(2, $collection); + // But "has" should still return true for 'hourly' + $this->assertTrue($collection->has('hourly')); + } + + public function testNames(): void + { + $type = $this->createPriceType('hourly'); + $monthly = $this->createPriceType('monthly'); + + $collection = new PriceTypeCollection([$type, $monthly]); + + $this->assertSame(['hourly', 'monthly'], $collection->names()); + } + + public function testThrowsExceptionWhenInvalidItemProvided(): void + { + $invalidItem = new \stdClass(); // not a PriceTypeInterface instance + + $this->expectException(InvalidPriceTypeCollectionException::class); + $this->expectExceptionMessage('PriceTypeCollection can only contain instances of PriceTypeInterface. Got: stdClas'); + + new PriceTypeCollection([$invalidItem]); + } +}