From b073a187287ca61b050592da41448467c41ab6b2 Mon Sep 17 00:00:00 2001 From: Carlos Buenosvinos Date: Thu, 17 Feb 2022 00:09:27 +0100 Subject: [PATCH] Clock --- src/Cheeper/AllChapters/DomainModel/Clock.php | 39 ++++++++ .../DomainModel/Clock/ClockStrategy.php | 12 +++ .../Clock/DateCollectionClockStrategy.php | 31 ++++++ .../Clock/DefaultClockStrategy.php | 19 ++++ .../SignUpWithEvents/SignUpCommandHandler.php | 4 +- .../DomainModel/Author/AuthorFollowed.php | 6 +- .../DomainModel/Author/NewAuthorSigned.php | 6 +- .../DomainModel/Cheep/CheepPosted.php | 5 +- .../DomainModel/TriggerEventsTrait.php | 11 ++- .../CountFollowersProjectionHandler.php | 5 + .../Command/FollowCommandHandlerTest.php | 4 - .../SignUpCommandHandlerTest.php | 29 +++++- .../FollowCommandHandlerTest.php | 3 - .../CountFollowersProjectionHandlerTest.php | 99 +++++++++++++++++++ 14 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 src/Cheeper/AllChapters/DomainModel/Clock.php create mode 100644 src/Cheeper/AllChapters/DomainModel/Clock/ClockStrategy.php create mode 100644 src/Cheeper/AllChapters/DomainModel/Clock/DateCollectionClockStrategy.php create mode 100644 src/Cheeper/AllChapters/DomainModel/Clock/DefaultClockStrategy.php create mode 100644 tests/Cheeper/Tests/Chapter7/Application/Author/Projection/CountFollowersProjectionHandlerTest.php diff --git a/src/Cheeper/AllChapters/DomainModel/Clock.php b/src/Cheeper/AllChapters/DomainModel/Clock.php new file mode 100644 index 0000000..fe53070 --- /dev/null +++ b/src/Cheeper/AllChapters/DomainModel/Clock.php @@ -0,0 +1,39 @@ +strategy = new DefaultClockStrategy(); + } + + public static function instance(): static + { + if (null === static::$instance) { + static::$instance = new static(); + } + + return static::$instance; + } + + public function now(): DateTimeImmutable { + return $this->strategy->now(); + } + + public function changeStrategy(ClockStrategy $clockStrategy): self { + $this->strategy = $clockStrategy; + + return $this; + } +} \ No newline at end of file diff --git a/src/Cheeper/AllChapters/DomainModel/Clock/ClockStrategy.php b/src/Cheeper/AllChapters/DomainModel/Clock/ClockStrategy.php new file mode 100644 index 0000000..51a34fd --- /dev/null +++ b/src/Cheeper/AllChapters/DomainModel/Clock/ClockStrategy.php @@ -0,0 +1,12 @@ +iterator = 0; + } + + public function now(): DateTimeImmutable + { + if (empty($this->collection)) { + throw new \InvalidArgumentException('Date collection is empty'); + } + + $currentDate = $this->collection[$this->iterator]; + $this->iterator = ($this->iterator + 1) % count($this->collection); + + return $currentDate; + } +} \ No newline at end of file diff --git a/src/Cheeper/AllChapters/DomainModel/Clock/DefaultClockStrategy.php b/src/Cheeper/AllChapters/DomainModel/Clock/DefaultClockStrategy.php new file mode 100644 index 0000000..818fa28 --- /dev/null +++ b/src/Cheeper/AllChapters/DomainModel/Clock/DefaultClockStrategy.php @@ -0,0 +1,19 @@ +authors->add($author); - $this->eventBus->notifyAll($author->domainEvents()); + $this->eventBus->notifyAll( + $author->retrieveAndFlushDomainEvents() + ); } private function checkAuthorDoesNotAlreadyExistByUsername(?Author $author, UserName $userName): void diff --git a/src/Cheeper/Chapter4/DomainModel/Author/AuthorFollowed.php b/src/Cheeper/Chapter4/DomainModel/Author/AuthorFollowed.php index d22d2ec..ab5d651 100644 --- a/src/Cheeper/Chapter4/DomainModel/Author/AuthorFollowed.php +++ b/src/Cheeper/Chapter4/DomainModel/Author/AuthorFollowed.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Cheeper\Chapter4\DomainModel\Author; +use Cheeper\AllChapters\DomainModel\Clock; use Cheeper\Chapter4\DomainModel\DomainEvent; use Cheeper\Chapter4\DomainModel\Follow\Follow; use DateTimeImmutable; -use DateTimeZone; // snippet author-followed-domain-event final class AuthorFollowed implements DomainEvent @@ -26,9 +26,7 @@ final class AuthorFollowed implements DomainEvent $follow->followId()->toString(), $follow->fromAuthorId()->toString(), $follow->toAuthorId()->toString(), - new DateTimeImmutable( - timezone: new DateTimeZone("UTC") - ) + Clock::instance()->now() ); } diff --git a/src/Cheeper/Chapter4/DomainModel/Author/NewAuthorSigned.php b/src/Cheeper/Chapter4/DomainModel/Author/NewAuthorSigned.php index c12025e..95768fa 100644 --- a/src/Cheeper/Chapter4/DomainModel/Author/NewAuthorSigned.php +++ b/src/Cheeper/Chapter4/DomainModel/Author/NewAuthorSigned.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Cheeper\Chapter4\DomainModel\Author; +use Cheeper\AllChapters\DomainModel\Clock; use Cheeper\Chapter4\DomainModel\DomainEvent; use DateTimeImmutable; -use DateTimeZone; // snippet new-author-signed-domain-event class NewAuthorSigned implements DomainEvent @@ -21,9 +21,7 @@ class NewAuthorSigned implements DomainEvent { return new static( $author->authorId()->toString(), - new DateTimeImmutable( - timezone: new DateTimeZone("UTC") - ) + Clock::instance()->now() ); } diff --git a/src/Cheeper/Chapter4/DomainModel/Cheep/CheepPosted.php b/src/Cheeper/Chapter4/DomainModel/Cheep/CheepPosted.php index 337ab33..98ecaba 100644 --- a/src/Cheeper/Chapter4/DomainModel/Cheep/CheepPosted.php +++ b/src/Cheeper/Chapter4/DomainModel/Cheep/CheepPosted.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Cheeper\Chapter4\DomainModel\Cheep; +use Cheeper\AllChapters\DomainModel\Clock; use Cheeper\Chapter4\DomainModel\DomainEvent; use DateTimeImmutable; use DateTimeInterface; @@ -28,9 +29,7 @@ final class CheepPosted implements DomainEvent $cheep->authorId()->toString(), $cheep->message()->message(), $cheep->date()->format(DateTimeInterface::ATOM), - new DateTimeImmutable( - timezone: new DateTimeZone("UTC") - ) + Clock::instance()->now() ); } diff --git a/src/Cheeper/Chapter4/DomainModel/TriggerEventsTrait.php b/src/Cheeper/Chapter4/DomainModel/TriggerEventsTrait.php index 25fb4e4..e2dd97e 100644 --- a/src/Cheeper/Chapter4/DomainModel/TriggerEventsTrait.php +++ b/src/Cheeper/Chapter4/DomainModel/TriggerEventsTrait.php @@ -16,12 +16,21 @@ trait TriggerEventsTrait return $this->domainEvents; } + /** @return DomainEvent[] */ + public function retrieveAndFlushDomainEvents(): array + { + $events = $this->domainEvents(); + $this->resetDomainEvent(); + + return $events; + } + public function notifyDomainEvent(DomainEvent $domainEvent): void { $this->domainEvents[] = $domainEvent; } - public function resetDomainEvent(): void + private function resetDomainEvent(): void { $this->domainEvents = []; } diff --git a/src/Cheeper/Chapter7/Application/Author/Projection/CountFollowersProjectionHandler.php b/src/Cheeper/Chapter7/Application/Author/Projection/CountFollowersProjectionHandler.php index 4a4dafe..7eb06b4 100644 --- a/src/Cheeper/Chapter7/Application/Author/Projection/CountFollowersProjectionHandler.php +++ b/src/Cheeper/Chapter7/Application/Author/Projection/CountFollowersProjectionHandler.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Cheeper\Chapter7\Application\Author\Projection; +use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist; use Cheeper\AllChapters\DomainModel\Author\AuthorId; use Doctrine\ORM\EntityManagerInterface; use Redis; @@ -32,6 +33,10 @@ final class CountFollowersProjectionHandler ['authorId' => $authorId->toString()] ); + if (false === $result) { + throw AuthorDoesNotExist::withAuthorIdOf($authorId); + } + $projectionResult = [ 'id' => $authorId->toString(), 'username' => $result['username'], diff --git a/tests/Cheeper/Tests/Chapter4/Application/Author/Command/FollowCommandHandlerTest.php b/tests/Cheeper/Tests/Chapter4/Application/Author/Command/FollowCommandHandlerTest.php index 561352a..5198148 100644 --- a/tests/Cheeper/Tests/Chapter4/Application/Author/Command/FollowCommandHandlerTest.php +++ b/tests/Cheeper/Tests/Chapter4/Application/Author/Command/FollowCommandHandlerTest.php @@ -21,17 +21,13 @@ use Ramsey\Uuid\Uuid; final class FollowCommandHandlerTest extends TestCase { - private InMemoryCheepRepository $cheepRepository; private InMemoryAuthorRepository $authorRepository; - private InMemoryEventBus $eventBus; private InMemoryFollowRepository $followRepository; protected function setUp(): void { - $this->cheepRepository = new InMemoryCheepRepository(); $this->authorRepository = new InMemoryAuthorRepository(); $this->followRepository = new InMemoryFollowRepository(); - $this->eventBus = new InMemoryEventBus(); } /** @test */ diff --git a/tests/Cheeper/Tests/Chapter4/Application/Author/Command/SignUpWithEvents/SignUpCommandHandlerTest.php b/tests/Cheeper/Tests/Chapter4/Application/Author/Command/SignUpWithEvents/SignUpCommandHandlerTest.php index 9b453a4..0e08631 100644 --- a/tests/Cheeper/Tests/Chapter4/Application/Author/Command/SignUpWithEvents/SignUpCommandHandlerTest.php +++ b/tests/Cheeper/Tests/Chapter4/Application/Author/Command/SignUpWithEvents/SignUpCommandHandlerTest.php @@ -6,6 +6,8 @@ namespace Cheeper\Tests\Chapter4\Application\Author\Command\SignUpWithEvents; use Cheeper\AllChapters\DomainModel\Author\AuthorAlreadyExists; use Cheeper\AllChapters\DomainModel\Author\UserName; +use Cheeper\AllChapters\DomainModel\Clock; +use Cheeper\AllChapters\DomainModel\Clock\DateCollectionClockStrategy; use Cheeper\Chapter4\Application\Author\Command\SignUpWithEvents\SignUpCommandHandler; use Cheeper\Chapter4\Application\Author\Command\SignUpWithoutEvents\SignUpCommand; use Cheeper\Chapter4\DomainModel\Author\NewAuthorSigned; @@ -14,16 +16,29 @@ use Cheeper\Chapter4\Infrastructure\DomainModel\Author\InMemoryAuthorRepository; use DateTimeImmutable; use PHPUnit\Framework\TestCase; use Ramsey\Uuid\Uuid; +use function Functional\first; final class SignUpCommandHandlerTest extends TestCase { private InMemoryAuthorRepository $authorRepository; private InMemoryEventBus $eventBus; + private DateTimeImmutable $today; protected function setUp(): void { $this->authorRepository = new InMemoryAuthorRepository(); $this->eventBus = new InMemoryEventBus(); + $this->today = $this->getToday(); + Clock::instance()->changeStrategy( + new DateCollectionClockStrategy([$this->today]) + ); + } + + protected function tearDown(): void + { + Clock::instance()->changeStrategy( + new Clock\DefaultClockStrategy() + ); } /** @test */ @@ -97,8 +112,13 @@ final class SignUpCommandHandlerTest extends TestCase $this->assertNull($actualAuthor->birthDate()); $events = $this->eventBus->events(); + + /** @var NewAuthorSigned $firstEvent */ + $firstEvent = first($events); $this->assertCount(1, $events); - $this->assertSame(NewAuthorSigned::class, $events[0]::class); + $this->assertSame(NewAuthorSigned::class, $firstEvent::class); + $this->assertSame($actualAuthor->authorId()->toString(), $firstEvent->authorId()); + $this->assertSame($this->today, $firstEvent->occurredOn()); } /** @test */ @@ -249,4 +269,11 @@ final class SignUpCommandHandlerTest extends TestCase ) ); } + + protected function getToday(): DateTimeImmutable + { + return new DateTimeImmutable( + 'now', new \DateTimeZone('UTC') + ); + } } diff --git a/tests/Cheeper/Tests/Chapter6/Application/Author/Command/WithoutDomainEvents/FollowCommandHandlerTest.php b/tests/Cheeper/Tests/Chapter6/Application/Author/Command/WithoutDomainEvents/FollowCommandHandlerTest.php index eb5a624..af15bf8 100644 --- a/tests/Cheeper/Tests/Chapter6/Application/Author/Command/WithoutDomainEvents/FollowCommandHandlerTest.php +++ b/tests/Cheeper/Tests/Chapter6/Application/Author/Command/WithoutDomainEvents/FollowCommandHandlerTest.php @@ -11,7 +11,6 @@ use Cheeper\AllChapters\DomainModel\Author\UserName; use Cheeper\AllChapters\DomainModel\Follow\FollowId; use Cheeper\Chapter4\DomainModel\Author\Author; use Cheeper\Chapter4\DomainModel\Follow\Follow; -use Cheeper\Chapter4\Infrastructure\Application\InMemoryEventBus; use Cheeper\Chapter4\Infrastructure\DomainModel\Author\InMemoryAuthorRepository; use Cheeper\Chapter4\Infrastructure\DomainModel\Follow\InMemoryFollowRepository; use Cheeper\Chapter6\Application\Author\Command\FollowCommand; @@ -29,14 +28,12 @@ final class FollowCommandHandlerTest extends TestCase private const EMAIL_CARLOS = 'carlos.buenosvinos@gmail.com'; private InMemoryAuthorRepository $authorRepository; - private InMemoryEventBus $eventBus; private InMemoryFollowRepository $followRepository; protected function setUp(): void { $this->authorRepository = new InMemoryAuthorRepository(); $this->followRepository = new InMemoryFollowRepository(); - $this->eventBus = new InMemoryEventBus(); } /** @test */ diff --git a/tests/Cheeper/Tests/Chapter7/Application/Author/Projection/CountFollowersProjectionHandlerTest.php b/tests/Cheeper/Tests/Chapter7/Application/Author/Projection/CountFollowersProjectionHandlerTest.php new file mode 100644 index 0000000..cdbeb16 --- /dev/null +++ b/tests/Cheeper/Tests/Chapter7/Application/Author/Projection/CountFollowersProjectionHandlerTest.php @@ -0,0 +1,99 @@ +expectException(AuthorDoesNotExist::class); + $this->expectExceptionMessage('Author "1c22ed61-c305-44dd-a558-f261f434f583" does not exist'); + + $redisMock = $this->createMock(\Redis::class); +// $redisMock +// ->expects($this->once()) +// ->method('set') +// ; + + $dbMock = $this->buildEntityManagerMockReturning(false); + + $handler = new CountFollowersProjectionHandler( + $redisMock, + $dbMock + ); + + $handler( + CountFollowersProjection::ofAuthor($authorId) + ); + } + + /** + * @atest + * @Given + * @When + * @Then + */ + public function authorExistingWithMoreThanOneFollowers(): void + { + $authorId = '1c22ed61-c305-44dd-a558-f261f434f583'; + + $this->expectException(AuthorDoesNotExist::class); + $this->expectExceptionMessage('Author "1c22ed61-c305-44dd-a558-f261f434f583" does not exist'); + + $redisMock = $this->createMock(\Redis::class); +// $redisMock +// ->expects($this->once()) +// ->method('set') +// ; + + $dbMock = $this->buildEntityManagerMockReturning(false); + + $handler = new CountFollowersProjectionHandler( + $redisMock, + $dbMock + ); + + $handler( + CountFollowersProjection::ofAuthor($authorId) + ); + } + + private function buildEntityManagerMockReturning($fakeReturn) { + $mock = $this->createStub(EntityManagerInterface::class); + + $connectionMock = new class($fakeReturn) { + public function __construct(private $toReturn) { + } + + public function fetchAssociative($query, $params): mixed { + return $this->toReturn; + } + }; + + $mock->method('getConnection')->willReturn( + $connectionMock + ); + + return $mock; + } +}