diff --git a/Dockerfile b/Dockerfile index 339fc04..fb4aa70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.0-cli-alpine3.13 +FROM php:8.4-cli-alpine3.22 RUN apk update && \ apk add --no-cache \ diff --git a/composer.json b/composer.json index 7d6f5ef..5c51902 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ } }, "require": { - "php": "^8.0", + "php": "^8.4", "ext-json": "*", "ramsey/uuid": "^4.2" }, diff --git a/src/Domain/Model/ValueObject/CollectionValueObject.php b/src/Domain/Model/ValueObject/CollectionValueObject.php index 58c92a7..313bf5d 100644 --- a/src/Domain/Model/ValueObject/CollectionValueObject.php +++ b/src/Domain/Model/ValueObject/CollectionValueObject.php @@ -3,10 +3,22 @@ namespace PcComponentes\Ddd\Domain\Model\ValueObject; -class CollectionValueObject implements \Iterator, \Countable, ValueObject +/** + * Generic collection value object. + * + * @template TKey of array-key + * @template TValue + * @implements \IteratorAggregate + */ +class CollectionValueObject implements \IteratorAggregate, \Countable, ValueObject { + /** + * @var array + * Typed collection items. + */ private array $items; + /** @param array $items */ final private function __construct(array $items) { $this->items = $items; @@ -17,9 +29,20 @@ public static function from(array $items): static return new static($items); } - public function current(): mixed + /** @return \Traversable */ + public function getIterator(): \Traversable { - return \current($this->items); + return new \ArrayIterator($this->items); + } + + /** @return TValue|null */ + public function current() + { + $key = $this->key(); + + return null === $key + ? null + : $this->items[$key]; } public function next(): void @@ -27,6 +50,7 @@ public function next(): void \next($this->items); } + /** @return TKey|null */ public function key(): string|int|null { return \key($this->items); @@ -47,26 +71,45 @@ public function count(): int return \count($this->items); } + /** @param callable(TValue, TKey): void $func */ public function walk(callable $func): void { \array_walk($this->items, $func); } + /** + * @param callable(TValue): bool $func + * @return TValue|null + */ + public function findOne(callable $func) + { + return \array_find($this->items, $func); + } + + /** @param callable(TValue): bool $func */ public function filter(callable $func): static { return static::from(\array_values(\array_filter($this->items, $func))); } + /** @param callable(TValue): TValue $func */ public function map(callable $func): static { return static::from(\array_map($func, $this->items)); } + /** + * @template TCarry + * @param callable(TCarry, TValue): TCarry $func + * @param TCarry $initial + * @return TCarry + */ public function reduce(callable $func, $initial) { return \array_reduce($this->items, $func, $initial); } + /** @param callable(TValue, TValue): int $func */ public function sort(callable $func): static { $items = $this->items; @@ -99,21 +142,25 @@ public function equivalentTo(self $other): bool return $a->equalTo($b); } + /** @return array */ final public function jsonSerialize(): array { return $this->items; } + /** @return TValue|null */ public function first() { return $this->items[\array_key_first($this->items)] ?? null; } + /** @return array */ public function value(): array { return $this->items; } + /** @param TValue $item */ protected function addItem($item): static { $items = $this->items; @@ -122,6 +169,7 @@ protected function addItem($item): static return static::from($items); } + /** @param TValue $item */ protected function removeItem($item): static { return $this->filter( diff --git a/src/Domain/Model/ValueObject/DateTimeRangeValeObject.php b/src/Domain/Model/ValueObject/DateTimeRangeValeObject.php deleted file mode 100644 index a63d825..0000000 --- a/src/Domain/Model/ValueObject/DateTimeRangeValeObject.php +++ /dev/null @@ -1,9 +0,0 @@ -setTimezone($timeZone)); } - final public static function fromFormat(string $format, string $str): static|false + final public static function fromFormat(string $format, string $str): static { return static::createFromFormat($format, $str, new \DateTimeZone(self::TIME_ZONE)); } final public static function createFromTimestamp(float|int $timestamp): static { - $dateTime = self::fromFormat('U.u', \number_format((float) $timestamp, 6, '.', '')); - - \assert(false !== $dateTime, 'Unexpected error on create date time from timestamp'); - - return $dateTime; + return self::fromFormat('U.u', \number_format((float) $timestamp, 6, '.', '')); } final public static function fromTimestamp(int|float $timestamp): static diff --git a/src/Domain/Model/ValueObject/DateValueObject.php b/src/Domain/Model/ValueObject/DateValueObject.php index eeb7244..54ed6f8 100644 --- a/src/Domain/Model/ValueObject/DateValueObject.php +++ b/src/Domain/Model/ValueObject/DateValueObject.php @@ -18,7 +18,7 @@ public static function from(string $str): static { $timeZone = new \DateTimeZone(self::TIME_ZONE); - return (new static($str, $timeZone))->setTimezone($timeZone); + return (new static($str, $timeZone))->setTimezone($timeZone)->setTime(0, 0, 0, 0); } final public static function now(): static @@ -40,11 +40,11 @@ final public static function createFromFormat( string $format, string $datetime, ?\DateTimeZone $timezone = null - ): static|false { + ): static { $datetime = parent::createFromFormat($format, $datetime, $timezone); if (false === $datetime) { - return false; + throw new \InvalidArgumentException('Invalid date format'); } $timeZone = new \DateTimeZone(self::TIME_ZONE); @@ -52,18 +52,14 @@ final public static function createFromFormat( return static::createFromInterface($datetime->setTimezone($timeZone)); } - final public static function fromFormat(string $format, string $str): static|false + final public static function fromFormat(string $format, string $str): static { return static::createFromFormat($format, $str, new \DateTimeZone(self::TIME_ZONE)); } final public static function createFromTimestamp(float|int $timestamp): static { - $dateTime = self::fromFormat('U.u', \number_format((float) $timestamp, 6, '.', '')); - - \assert(false !== $dateTime, 'Unexpected error on create date time from timestamp'); - - return $dateTime; + return self::fromFormat('U.u', \number_format((float) $timestamp, 6, '.', '')); } final public static function fromTimestamp(int|float $timestamp): static diff --git a/src/Domain/Model/ValueObject/UniqueId.php b/src/Domain/Model/ValueObject/UniqueId.php index 7a05fbd..84864ac 100644 --- a/src/Domain/Model/ValueObject/UniqueId.php +++ b/src/Domain/Model/ValueObject/UniqueId.php @@ -5,10 +5,30 @@ class UniqueId extends StringValueObject { + private const VALID_PATTERN = '/^[A-Z0-9]{10}$/'; + + public static function from(string $value): static + { + if (false === self::isValid($value)) { + throw new \InvalidArgumentException('Invalid UniqueId.'); + } + + return parent::from($value); + } + public static function create(): static { + $value = \base_convert(\uniqid(), 16, 36); + $value = \str_pad($value, 10, '0', \STR_PAD_LEFT); + $value = \substr($value, -10); + return self::from( - \strtoupper(\base_convert(\uniqid(), 16, 36)), + \strtoupper($value), ); } + + public static function isValid(string $value): bool + { + return 1 === \preg_match(self::VALID_PATTERN, $value); + } } diff --git a/tests/Domain/Model/ValueObject/CollectionValueObjectTest.php b/tests/Domain/Model/ValueObject/CollectionValueObjectTest.php index 29c68ce..90849bc 100644 --- a/tests/Domain/Model/ValueObject/CollectionValueObjectTest.php +++ b/tests/Domain/Model/ValueObject/CollectionValueObjectTest.php @@ -4,6 +4,7 @@ namespace PcComponentes\Ddd\Tests\Domain\Model\ValueObject; use PcComponentes\Ddd\Domain\Model\ValueObject\CollectionValueObject; +use PcComponentes\Ddd\Domain\Model\ValueObject\FloatValueObject; use PHPUnit\Framework\TestCase; class CollectionValueObjectTest extends TestCase @@ -95,6 +96,19 @@ static function ($current) use (&$iterated) { $this->assertEquals([1, 2, 3, 4], $iterated); } + /** @test */ + public function given_collection_when_iterate_with_foreach_then_keep_items_and_keys() + { + $collection = CollectionValueObject::from(['a' => 1, 'b' => 2]); + $iterated = []; + + foreach ($collection as $key => $item) { + $iterated[$key] = $item; + } + + $this->assertEquals(['a' => 1, 'b' => 2], $iterated); + } + /** @test */ public function given_two_identical_collections_when_ask_to_check_equality_then_return_true() { @@ -129,12 +143,12 @@ public function equivalentCollectionValues(): array [['1', '1', '1', '4'], ['4', '1', '1', '1']], [ [ - FloatValueObjectTested::from(1.1), - FloatValueObjectTested::from(6.0), + FloatValueObject::from(1.1), + FloatValueObject::from(6.0), ], [ - FloatValueObjectTested::from(1.1), - FloatValueObjectTested::from(6.0), + FloatValueObject::from(1.1), + FloatValueObject::from(6.0), ], ], [[$objet1, $objet2], [$objet1, $objet2]], @@ -170,12 +184,12 @@ public function differentCollectionValues(): array [['1', '1', '4', '4'], ['4', '1', '1', '1']], [ [ - FloatValueObjectTested::from(1.1), - FloatValueObjectTested::from(6.0), + FloatValueObject::from(1.1), + FloatValueObject::from(6.0), ], [ - FloatValueObjectTested::from(1.3), - FloatValueObjectTested::from(6.0), + FloatValueObject::from(1.3), + FloatValueObject::from(6.0), ], ], ]; @@ -215,6 +229,26 @@ public function given_collection_when_ask_to_remove_item_then_return_new_collect $this->assertEquals([1, 2, 4], $newCollection->jsonSerialize()); } + /** @test */ + public function given_an_empty_collection_when_ask_to_obtain_current_item_then_return_null() + { + $collection = CollectionValueObject::from([]); + + $this->assertNull($collection->current()); + } + + /** @test */ + public function given_a_collection_when_ask_to_obtain_current_item_then_return_expected_item() + { + $collection = CollectionValueObject::from([1, 2, 3, 4]); + + $this->assertSame(1, $collection->current()); + + $collection->next(); + + $this->assertSame(2, $collection->current()); + } + /** @test */ public function given_an_empty_collection_when_ask_to_obtain_first_item_then_return_null() { diff --git a/tests/Domain/Model/ValueObject/DateTimeRangeValeObjectTest.php b/tests/Domain/Model/ValueObject/DateTimeRangeValeObjectTest.php deleted file mode 100644 index e373604..0000000 --- a/tests/Domain/Model/ValueObject/DateTimeRangeValeObjectTest.php +++ /dev/null @@ -1,116 +0,0 @@ -assertEquals(DateTimeValueObject::from('2000-01-01 00:00:00'), $range->start()); - $this->assertEquals(DateTimeValueObject::from('2019-12-31 23:59:59'), $range->end()); - } - - /** @test */ - public function given_invalid_range_when_creating_range_then_throw_exception(): void - { - $this->expectException(\InvalidArgumentException::class); - - DateTimeRangeValeObject::from( - DateTimeValueObject::from('2019-12-31 23:59:59'), - DateTimeValueObject::from('2000-01-01 00:00:00'), - ); - } - - /** @test */ - public function given_equal_dates_when_creating_range_then_throw_exception(): void - { - $this->expectException(\InvalidArgumentException::class); - - DateTimeRangeValeObject::from( - DateTimeValueObject::from('2000-01-01 00:00:00'), - DateTimeValueObject::from('2000-01-01 00:00:00'), - ); - } - - /** @test */ - public function given_expired_range_when_ask_if_expired_then_return_expected_info(): void - { - $range = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2000-01-01 00:00:00'), - DateTimeValueObject::from('2001-12-31 23:59:59'), - ); - - $this->assertFalse($range->isInDate()); - $this->assertTrue($range->isExpired()); - $this->assertFalse($range->isNotStarted()); - } - - /** @test */ - public function given_in_date_range_when_ask_if_in_date_then_return_expected_info(): void - { - $range = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2020-01-01 00:00:00'), - DateTimeValueObject::from('2100-12-31 23:59:59'), - ); - - $this->assertTrue($range->isInDate()); - $this->assertFalse($range->isExpired()); - $this->assertFalse($range->isNotStarted()); - } - - /** @test */ - public function given_not_started_range_when_ask_if_not_started_then_return_expected_info(): void - { - $range = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2050-01-01 00:00:00'), - DateTimeValueObject::from('2100-12-31 23:59:59'), - ); - - $this->assertFalse($range->isInDate()); - $this->assertFalse($range->isExpired()); - $this->assertTrue($range->isNotStarted()); - } - - /** @test */ - public function given_two_equal_ranges_when_ask_to_check_equality_then_return_true(): void - { - $range = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2000-01-01 00:00:00'), - DateTimeValueObject::from('2019-12-31 23:59:59'), - ); - - $other = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2000-01-01 00:00:00'), - DateTimeValueObject::from('2019-12-31 23:59:59'), - ); - - $this->assertTrue($range->equalTo($other)); - } - - /** @test */ - public function given_two_different_ranges_when_ask_to_check_equality_then_return_false(): void - { - $range = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2000-01-01 00:00:00'), - DateTimeValueObject::from('2019-12-31 23:59:59'), - ); - - $other = DateTimeRangeValeObject::from( - DateTimeValueObject::from('2000-01-01 00:00:01'), - DateTimeValueObject::from('2019-12-31 23:59:59'), - ); - - $this->assertFalse($range->equalTo($other)); - } -} diff --git a/tests/Domain/Model/ValueObject/UniqueIdTest.php b/tests/Domain/Model/ValueObject/UniqueIdTest.php index ac186a5..5d16cc3 100644 --- a/tests/Domain/Model/ValueObject/UniqueIdTest.php +++ b/tests/Domain/Model/ValueObject/UniqueIdTest.php @@ -28,7 +28,7 @@ public function given_uid_class_when_ask_to_generate_an_uid_then_return_uid_inst { $uid = UniqueId::create(); $this->assertInstanceOf(UniqueId::class, $uid); - $this->assertMatchesRegularExpression('/^[0-9A-Z]{10}$/i', $uid->value()); + $this->assertTrue(UniqueId::isValid($uid->value())); } /** @@ -53,4 +53,29 @@ public function given_two_different_uids_when_ask_to_check_equality_then_return_ $this->assertFalse($str->equalTo($other)); } + + /** + * @test + */ + public function given_invalid_uid_value_when_construct_then_fail() + { + $this->expectException(\InvalidArgumentException::class); + UniqueId::from('12345'); + } + + /** + * @test + */ + public function given_invalid_uid_value_when_ask_is_valid_then_return_false() + { + self::assertFalse(UniqueId::isValid('12345')); + } + + /** + * @test + */ + public function given_valid_uid_value_when_ask_is_valid_then_return_true() + { + self::assertTrue(UniqueId::isValid('H7HVQ2U72X')); + } }