This commit is contained in:
Carlos Buenosvinos
2022-02-17 00:09:27 +01:00
parent f6eb45fd29
commit b073a18728
14 changed files with 252 additions and 21 deletions

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Cheeper\AllChapters\DomainModel;
use Cheeper\AllChapters\DomainModel\Clock\ClockStrategy;
use Cheeper\AllChapters\DomainModel\Clock\DefaultClockStrategy;
use DateTimeImmutable;
class Clock
{
protected static ?Clock $instance = null;
protected ClockStrategy $strategy;
private function __construct()
{
$this->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;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Cheeper\AllChapters\DomainModel\Clock;
use DateTimeImmutable;
interface ClockStrategy
{
public function now(): DateTimeImmutable;
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Cheeper\AllChapters\DomainModel\Clock;
use DateTimeImmutable;
class DateCollectionClockStrategy implements ClockStrategy
{
private int $iterator;
public function __construct(
private array $collection = []
)
{
$this->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;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Cheeper\AllChapters\DomainModel\Clock;
use DateTimeImmutable;
class DefaultClockStrategy implements ClockStrategy
{
public function now(): DateTimeImmutable {
return new DateTimeImmutable(
'now',
new \DateTimeZone(
'UTC'
)
);
}
}

View File

@@ -54,7 +54,9 @@ final class SignUpCommandHandler
);
$this->authors->add($author);
$this->eventBus->notifyAll($author->domainEvents());
$this->eventBus->notifyAll(
$author->retrieveAndFlushDomainEvents()
);
}
private function checkAuthorDoesNotAlreadyExistByUsername(?Author $author, UserName $userName): void

View File

@@ -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()
);
}

View File

@@ -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()
);
}

View File

@@ -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()
);
}

View File

@@ -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 = [];
}

View File

@@ -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'],

View File

@@ -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 */

View File

@@ -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')
);
}
}

View File

@@ -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 */

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Cheeper\Tests\Chapter7\Application\Author\Projection;
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Cheep\CheepDate;
use Cheeper\AllChapters\DomainModel\Cheep\CheepId;
use Cheeper\AllChapters\DomainModel\Cheep\CheepMessage;
use Cheeper\Chapter7\Application\Author\Projection\CountFollowersProjection;
use Cheeper\Chapter7\Application\Author\Projection\CountFollowersProjectionHandler;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
final class CountFollowersProjectionHandlerTest extends TestCase
{
/**
* @test
* @Given
* @When
* @Then
*/
public function authorNonExistingOrWithoutFollowers(): 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)
);
}
/**
* @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;
}
}