Clock
This commit is contained in:
39
src/Cheeper/AllChapters/DomainModel/Clock.php
Normal file
39
src/Cheeper/AllChapters/DomainModel/Clock.php
Normal 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;
|
||||
}
|
||||
}
|
||||
12
src/Cheeper/AllChapters/DomainModel/Clock/ClockStrategy.php
Normal file
12
src/Cheeper/AllChapters/DomainModel/Clock/ClockStrategy.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\AllChapters\DomainModel\Clock;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface ClockStrategy
|
||||
{
|
||||
public function now(): DateTimeImmutable;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user