Skip to content

Commit ab843be

Browse files
HP-2557: Created PriceTypeCollection for simplified work with PriceTypeInterface (#109)
* HP-2557: Created PriceTypeCollection for simplified work with PriceTypeInterface * HP-2557: moved PriceTypeCollection class into appropriate directory in accordance with DDD architecture * HP-2557: moved PriceTypeInterface into appropriate directory in accordance with DDD architecture * HP-2557: covered PriceTypeCollection with PHPUnit * HP-2557: created PriceTypeCollection::names() to avoid code duplicate * HP-2557: ensure PriceTypeCollection contains only instances of PriceTypeInterface * HP-2557: created `InvalidPriceTypeCollectionException::becauseContainsNonPriceType()` method because it makes the domain reason for the exception explicit and self-documenting
1 parent 439554f commit ab843be

File tree

4 files changed

+184
-1
lines changed

4 files changed

+184
-1
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace hiqdev\php\billing\product\Domain\Model\Price\Exception;
6+
7+
use InvalidArgumentException;
8+
9+
class InvalidPriceTypeCollectionException extends InvalidArgumentException
10+
{
11+
public static function becauseContainsNonPriceType(mixed $value): self
12+
{
13+
$given = is_object($value) ? get_class($value) : gettype($value);
14+
15+
return new self(sprintf(
16+
'PriceTypeCollection can only contain instances of PriceTypeInterface. Got: %s',
17+
$given
18+
));
19+
}
20+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace hiqdev\php\billing\product\Domain\Model\Price;
6+
7+
use Countable;
8+
use hiqdev\php\billing\product\Domain\Model\Price\Exception\InvalidPriceTypeCollectionException;
9+
use IteratorAggregate;
10+
use Traversable;
11+
12+
class PriceTypeCollection implements IteratorAggregate, Countable
13+
{
14+
/**
15+
* @var string[] - flipped type names for fast search
16+
*/
17+
private array $flippedTypeNames;
18+
19+
public function __construct(private readonly array $types = [])
20+
{
21+
$this->assertAllPriceTypes($types);
22+
$this->flippedTypeNames = array_flip($this->names());
23+
}
24+
25+
private function assertAllPriceTypes(array $types): void
26+
{
27+
foreach ($types as $type) {
28+
if (!$type instanceof PriceTypeInterface) {
29+
throw InvalidPriceTypeCollectionException::becauseContainsNonPriceType($type);
30+
}
31+
}
32+
}
33+
34+
public function names(): array
35+
{
36+
return array_map(fn(PriceTypeInterface $t) => $t->name(), $this->types);
37+
}
38+
39+
/**
40+
* @return Traversable<int, PriceTypeInterface>
41+
*/
42+
public function getIterator(): Traversable
43+
{
44+
return new \ArrayIterator($this->types);
45+
}
46+
47+
public function has(string $priceType): bool
48+
{
49+
return array_key_exists($priceType, $this->flippedTypeNames);
50+
}
51+
52+
public function count(): int
53+
{
54+
return count($this->types);
55+
}
56+
57+
public function hasItems(): bool
58+
{
59+
return $this->count() > 0;
60+
}
61+
}

src/product/price/PriceTypeInterface.php renamed to src/product/Domain/Model/Price/PriceTypeInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php declare(strict_types=1);
22

3-
namespace hiqdev\php\billing\product\price;
3+
namespace hiqdev\php\billing\product\Domain\Model\Price;
44

55
interface PriceTypeInterface
66
{
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace hiqdev\php\billing\tests\unit\product\Domain\Model\Price;
6+
7+
use hiqdev\php\billing\product\Domain\Model\Price\Exception\InvalidPriceTypeCollectionException;
8+
use hiqdev\php\billing\product\Domain\Model\Price\PriceTypeCollection;
9+
use hiqdev\php\billing\product\Domain\Model\Price\PriceTypeInterface;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class PriceTypeCollectionTest extends TestCase
13+
{
14+
public function testEmptyCollection(): void
15+
{
16+
$collection = new PriceTypeCollection([]);
17+
18+
$this->assertCount(0, $collection);
19+
$this->assertFalse($collection->hasItems());
20+
$this->assertFalse($collection->has('any'));
21+
}
22+
23+
public function testCountAndHasItems(): void
24+
{
25+
$type = $this->createPriceType('hourly');
26+
$collection = new PriceTypeCollection([$type]);
27+
28+
$this->assertCount(1, $collection);
29+
$this->assertTrue($collection->hasItems());
30+
}
31+
32+
private function createPriceType(string $name): PriceTypeInterface
33+
{
34+
return new class($name) implements PriceTypeInterface {
35+
public function __construct(private string $name) {}
36+
public function name(): string { return $this->name; }
37+
};
38+
}
39+
40+
public function testHasReturnsTrueForExistingType(): void
41+
{
42+
$hourly = $this->createPriceType('hourly');
43+
$monthly = $this->createPriceType('monthly');
44+
45+
$collection = new PriceTypeCollection([$hourly, $monthly]);
46+
47+
$this->assertTrue($collection->has('hourly'));
48+
$this->assertTrue($collection->has('monthly'));
49+
$this->assertFalse($collection->has('discount'));
50+
}
51+
52+
public function testIteratorReturnsAllTypes(): void
53+
{
54+
$types = [
55+
$this->createPriceType('hourly'),
56+
$this->createPriceType('fixed'),
57+
];
58+
59+
$collection = new PriceTypeCollection($types);
60+
$collectedNames = [];
61+
62+
foreach ($collection as $type) {
63+
$this->assertInstanceOf(PriceTypeInterface::class, $type);
64+
$collectedNames[] = $type->name();
65+
}
66+
67+
$this->assertSame(['hourly', 'fixed'], $collectedNames);
68+
}
69+
70+
public function testHandlesDuplicateNamesGracefully(): void
71+
{
72+
// Duplicates in the array should still work for iteration, though flipped array will only store last
73+
$hourly1 = $this->createPriceType('hourly');
74+
$hourly2 = $this->createPriceType('hourly');
75+
$collection = new PriceTypeCollection([$hourly1, $hourly2]);
76+
77+
// Both objects exist in types
78+
$this->assertCount(2, $collection);
79+
// But "has" should still return true for 'hourly'
80+
$this->assertTrue($collection->has('hourly'));
81+
}
82+
83+
public function testNames(): void
84+
{
85+
$type = $this->createPriceType('hourly');
86+
$monthly = $this->createPriceType('monthly');
87+
88+
$collection = new PriceTypeCollection([$type, $monthly]);
89+
90+
$this->assertSame(['hourly', 'monthly'], $collection->names());
91+
}
92+
93+
public function testThrowsExceptionWhenInvalidItemProvided(): void
94+
{
95+
$invalidItem = new \stdClass(); // not a PriceTypeInterface instance
96+
97+
$this->expectException(InvalidPriceTypeCollectionException::class);
98+
$this->expectExceptionMessage('PriceTypeCollection can only contain instances of PriceTypeInterface. Got: stdClas');
99+
100+
new PriceTypeCollection([$invalidItem]);
101+
}
102+
}

0 commit comments

Comments
 (0)