From 0d95983ced8e4f628398ef056558380640f7cd2c Mon Sep 17 00:00:00 2001 From: Julian Torices Date: Wed, 29 Jan 2025 11:54:14 +0100 Subject: [PATCH 1/6] breaking changes: added validation to UniqueId VO. Delete deprecated date time range VO. --- .../ValueObject/CollectionValueObject.php | 10 +- .../ValueObject/DateTimeRangeValeObject.php | 9 -- src/Domain/Model/ValueObject/UniqueId.php | 22 +++- .../DateTimeRangeValeObjectTest.php | 116 ------------------ .../Domain/Model/ValueObject/UniqueIdTest.php | 27 +++- 5 files changed, 52 insertions(+), 132 deletions(-) delete mode 100644 src/Domain/Model/ValueObject/DateTimeRangeValeObject.php delete mode 100644 tests/Domain/Model/ValueObject/DateTimeRangeValeObjectTest.php diff --git a/src/Domain/Model/ValueObject/CollectionValueObject.php b/src/Domain/Model/ValueObject/CollectionValueObject.php index 226664f..e9caf16 100644 --- a/src/Domain/Model/ValueObject/CollectionValueObject.php +++ b/src/Domain/Model/ValueObject/CollectionValueObject.php @@ -95,6 +95,11 @@ public function first() return $this->items[array_key_first($this->items)] ?? null; } + public function value(): array + { + return $this->items; + } + protected function addItem($item): static { $items = $this->items; @@ -109,9 +114,4 @@ protected function removeItem($item): static static fn ($current) => $current !== $item, ); } - - public function value(): array - { - return $this->items; - } } 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 @@ -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')); + } } From f9f67cdcda77fe68a124f314be00c7ec3586ee40 Mon Sep 17 00:00:00 2001 From: "cristian.almohalla" Date: Tue, 11 Feb 2025 11:54:13 +0100 Subject: [PATCH 2/6] feat: throw exception on invalid date time arguments --- .../Model/ValueObject/DateTimeValueObject.php | 30 ++++++++----------- .../Model/ValueObject/DateValueObject.php | 30 ++++++++----------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/Domain/Model/ValueObject/DateTimeValueObject.php b/src/Domain/Model/ValueObject/DateTimeValueObject.php index 04eac10..345c2b4 100644 --- a/src/Domain/Model/ValueObject/DateTimeValueObject.php +++ b/src/Domain/Model/ValueObject/DateTimeValueObject.php @@ -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,20 +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 = \is_int($timestamp) - ? self::fromFormat('U', (string) $timestamp) - : self::fromFormat('U.u', \number_format($timestamp, 6, '.', '')); - - \assert(false !== $dateTime, 'Unexpected error on create date time from timestamp'); - - return $dateTime; + return self::fromFormat('U.u', \number_format($timestamp, 6, '.', '')); } final public static function fromTimestamp(int|float $timestamp): static @@ -83,12 +77,12 @@ final public function value(): string return $this->format(self::FORMAT); } - final public function modify(string $modifier): static | false + final public function modify(string $modifier): static { $dateTime = parent::modify($modifier); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid date modifier'); } return static::createFromInterface($dateTime); @@ -99,34 +93,34 @@ final public function add(\DateInterval $interval): static return parent::add($interval); } - final public function setDate(int $year, int $month, int $day): static | false + final public function setDate(int $year, int $month, int $day): static { $dateTime = parent::setDate($year, $month, $day); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid date provided'); } return $dateTime; } - final public function setISODate(int $year, int $week, int $dayOfWeek = 1): static | false + final public function setISODate(int $year, int $week, int $dayOfWeek = 1): static { $dateTime = parent::setISODate($year, $week, $dayOfWeek); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid ISO date provided'); } return $dateTime; } - final public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static | false + final public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static { $dateTime = parent::setTime($hour, $minute, $second, $microsecond); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid time provided'); } return $dateTime; diff --git a/src/Domain/Model/ValueObject/DateValueObject.php b/src/Domain/Model/ValueObject/DateValueObject.php index f7a1b79..86b420c 100644 --- a/src/Domain/Model/ValueObject/DateValueObject.php +++ b/src/Domain/Model/ValueObject/DateValueObject.php @@ -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,20 +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 = \is_int($timestamp) - ? self::fromFormat('U', (string) $timestamp) - : self::fromFormat('U.u', \number_format($timestamp, 6, '.', '')); - - \assert(false !== $dateTime, 'Unexpected error on create date time from timestamp'); - - return $dateTime; + return self::fromFormat('U.u', \number_format($timestamp, 6, '.', '')); } final public static function fromTimestamp(int|float $timestamp): static @@ -83,12 +77,12 @@ final public function value(): string return $this->format(self::FORMAT); } - final public function modify(string $modifier): static | false + final public function modify(string $modifier): static { $dateTime = parent::modify($modifier); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid date modifier'); } return static::createFromInterface($dateTime); @@ -99,34 +93,34 @@ final public function add(\DateInterval $interval): static return parent::add($interval); } - final public function setDate(int $year, int $month, int $day): static | false + final public function setDate(int $year, int $month, int $day): static { $dateTime = parent::setDate($year, $month, $day); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid date provided'); } return $dateTime; } - final public function setISODate(int $year, int $week, int $dayOfWeek = 1): static | false + final public function setISODate(int $year, int $week, int $dayOfWeek = 1): static { $dateTime = parent::setISODate($year, $week, $dayOfWeek); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid ISO date provided'); } return $dateTime; } - final public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static | false + final public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static { $dateTime = parent::setTime($hour, $minute, $second, $microsecond); if (false === $dateTime) { - return false; + throw new \InvalidArgumentException('Invalid time provided'); } return $dateTime; From 1220b70d0e4b13b2314fecda96a1a22ec28f3258 Mon Sep 17 00:00:00 2001 From: "cristian.almohalla" Date: Tue, 11 Feb 2025 11:55:02 +0100 Subject: [PATCH 3/6] feat: ensure date value object has zero time --- src/Domain/Model/ValueObject/DateValueObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Model/ValueObject/DateValueObject.php b/src/Domain/Model/ValueObject/DateValueObject.php index 86b420c..4e146a8 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 From daaa07cf9c4983ea3b1845d4efd3b5334973733e Mon Sep 17 00:00:00 2001 From: "cristian.almohalla" Date: Tue, 29 Jul 2025 12:10:21 +0200 Subject: [PATCH 4/6] feat: add find function to CollectionValueObject --- Dockerfile | 2 +- composer.json | 2 +- src/Domain/Model/ValueObject/CollectionValueObject.php | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) 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..60d8cd6 100644 --- a/src/Domain/Model/ValueObject/CollectionValueObject.php +++ b/src/Domain/Model/ValueObject/CollectionValueObject.php @@ -52,6 +52,11 @@ public function walk(callable $func): void \array_walk($this->items, $func); } + public function find(callable $func) + { + return \array_find($this->items, $func); + } + public function filter(callable $func): static { return static::from(\array_values(\array_filter($this->items, $func))); From d67060b0e14aff2b4b0ab5745f5b1dfdf4c3a73f Mon Sep 17 00:00:00 2001 From: "cristian.almohalla" Date: Mon, 11 Aug 2025 16:01:52 +0200 Subject: [PATCH 5/6] refactor: array find function name --- src/Domain/Model/ValueObject/CollectionValueObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Model/ValueObject/CollectionValueObject.php b/src/Domain/Model/ValueObject/CollectionValueObject.php index 60d8cd6..0e01c35 100644 --- a/src/Domain/Model/ValueObject/CollectionValueObject.php +++ b/src/Domain/Model/ValueObject/CollectionValueObject.php @@ -52,7 +52,7 @@ public function walk(callable $func): void \array_walk($this->items, $func); } - public function find(callable $func) + public function findOne(callable $func) { return \array_find($this->items, $func); } From 148f647171f5bd35226307073557be6eb0ddecbc Mon Sep 17 00:00:00 2001 From: "cristian.almohalla" Date: Fri, 13 Mar 2026 22:29:24 +0100 Subject: [PATCH 6/6] Improve CollectionValueObject iteration typing --- .../ValueObject/CollectionValueObject.php | 49 ++++++++++++++++-- .../ValueObject/CollectionValueObjectTest.php | 50 ++++++++++++++++--- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/Domain/Model/ValueObject/CollectionValueObject.php b/src/Domain/Model/ValueObject/CollectionValueObject.php index 0e01c35..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,31 +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; @@ -104,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; @@ -127,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/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() {