[Chapter 4] Improving Code and Books Examples

This commit is contained in:
Carlos Buenosvinos
2022-02-13 19:40:49 +01:00
parent c17db82010
commit 57ea5be1ee
19 changed files with 188 additions and 59 deletions

View File

@@ -43,20 +43,22 @@ framework:
# Consider Signing up an Author is a sync command.
# With a proper UX, almost all the commands should
# be run asynchronously.
Cheeper\AllChapters\Application\Command\Author\SignUp: async_commands
Cheeper\AllChapters\Application\Command\AsyncCommand: async_commands
Cheeper\AllChapters\Application\Command\SyncCommand: sync_commands
# TODO: Review this comment
# Domain Events are sent to asynchronous
# transport to be processed later.
Cheeper\AllChapters\DomainModel\DomainEvent: [events, projections]
# Chapter 4
Cheeper\Chapter4\Application\Author\Command\SignUpWithoutEvents\SignUpCommand: async_commands
# Chapter 5
# Queries are executed synchronously
# in the same request that data is requested.
Cheeper\Chapter5\Application\Query\Query: query
# Chapter 6
Cheeper\Chapter6\Application\Command\Author\Follow: async_commands
Cheeper\Chapter6\Application\Command\Author\FollowCommand: async_commands
# End of Chapter 6

View File

@@ -80,11 +80,19 @@ services:
Cheeper\Chapter6\Infrastructure\:
resource: '../src/Cheeper/Chapter6/Infrastructure/**/*.php'
# CHAPTER 5
# CHAPTER 4
Cheeper\Chapter4\DomainModel\Author\AuthorRepository:
class: 'Cheeper\Chapter4\Infrastructure\DomainModel\Author\DoctrineOrmAuthorRepository'
Cheeper\Chapter4\DomainModel\Cheep\CheepRepository:
class: 'Cheeper\Chapter4\Infrastructure\DomainModel\Cheep\DoctrineOrmCheepRepository'
Cheeper\Chapter4\DomainModel\Follow\FollowRepository:
class: 'Cheeper\Chapter4\Infrastructure\DomainModel\Follow\DoctrineOrmFollowRepository'
# CHAPTER 5
Cheeper\Chapter5\Infrastructure\Application\Query\SymfonyQueryBus: ~
Architecture\CQRS\Infrastructure\Persistence\Doctrine\DoctrineFollowersRepository: ~
# CHAPTER 6
# CHAPTER 6
# Cheeper\Chapter6\Infrastructure\Application\Projector\Author\SymfonyAuthorFollowedHandler:
# resource: '../src/Cheeper/Infrastructure/**/*.php'

View File

@@ -8,8 +8,6 @@
<directory suffix=".php">src/Cheeper</directory>
</include>
<exclude>
<file>src/Cheeper/Infrastructure/Persistence/InMemoryCheeps.php</file>
<directory>src/Cheeper/Infrastructure</directory>
<directory>src/Cheeper/Chapter5/Infrastructure</directory>
<directory>src/Cheeper/Chapter6/Infrastructure</directory>
</exclude>
@@ -21,7 +19,8 @@
</php>
<testsuites>
<testsuite name="full">
<directory>tests/</directory>
<directory>tests/Cheeper/Tests/Chapter2</directory>
<directory>tests/Cheeper/Tests/Chapter4</directory>
</testsuite>
</testsuites>
<extensions>

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace App\Command;
use Cheeper\Chapter6\Application\Command\Author\Follow;
use Cheeper\Chapter6\Application\Command\Author\FollowCommand as ApplicationFollowCommand;
use Cheeper\Chapter6\Application\Command\Author\WithDomainEvents\FollowHandler;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -38,7 +38,7 @@ final class FollowCommand extends Command
$io->info(sprintf('Making %s follow %s', $from, $to));
$this->followHandler->__invoke(
Follow::fromAuthorIdToAuthorId(
ApplicationFollowCommand::fromAuthorIdToAuthorId(
from: $from,
to: $to
)

View File

@@ -8,7 +8,7 @@ use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\Chapter2\Author;
use Cheeper\Chapter2\Hexagonal\DomainModel\AuthorRepository;
//snippet author-service
//snippet autho r-service
final class AuthorService
{
public function __construct(

View File

@@ -6,6 +6,7 @@ namespace Cheeper\Chapter4\DomainModel\Cheep;
use Cheeper\Chapter4\DomainModel\DomainEvent;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
// snippet cheep-posted-domain-event
@@ -23,10 +24,10 @@ final class CheepPosted implements DomainEvent
public static function fromCheep(Cheep $cheep): self
{
return new self(
$cheep->cheepId()->toString(),
$cheep->id()->toString(),
$cheep->authorId()->toString(),
$cheep->cheepMessage()->message(),
$cheep->cheepDate()->date(),
$cheep->message()->message(),
$cheep->date()->format(DateTimeInterface::ATOM),
new DateTimeImmutable(
timezone: new DateTimeZone("UTC")
)

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Cheeper\Chapter4\Infrastructure\DomainModel\Author;
use Cheeper\Chapter4\DomainModel\Author\Author;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Author\UserName;
use Cheeper\Chapter4\DomainModel\Author\AuthorRepository;
use Doctrine\ORM\EntityManagerInterface;
//snippet doctrine-orm-authors
final class DoctrineOrmAuthorRepository implements AuthorRepository
{
public function __construct(
private EntityManagerInterface $em
) {
}
public function ofId(AuthorId $authorId): ?Author
{
return $this->em
->getRepository(Author::class)
->findOneBy([
'authorId' => $authorId->id(),
]);
}
public function ofUserName(UserName $userName): ?Author
{
return $this->em
->getRepository(Author::class)
->findOneBy(['userName' => $userName->userName()]);
}
public function add(Author $author): void
{
$this->em->persist($author);
}
}
//end-snippet

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Cheeper\Chapter4\Infrastructure\DomainModel\Cheep;
use Cheeper\AllChapters\DomainModel\Cheep\CheepId;
use Cheeper\Chapter4\DomainModel\Cheep\Cheep;
use Cheeper\Chapter4\DomainModel\Cheep\CheepRepository;
use Doctrine\ORM\EntityManagerInterface;
use Ramsey\Uuid\Uuid;
//snippet doctrine-orm-cheeps
final class DoctrineOrmCheepRepository implements CheepRepository
{
//ignore
public function __construct(
private EntityManagerInterface $em
) {
}
public function add(Cheep $cheep): void
{
$this->em->persist($cheep);
}
//end-ignore
public function ofId(CheepId $cheepId): ?Cheep
{
return $this->em->find(Cheep::class, Uuid::fromString($cheepId->id()));
}
}
//end-snippet

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Cheeper\Chapter4\Infrastructure\DomainModel\Follow;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\Chapter4\DomainModel\Follow\Follow;
use Cheeper\Chapter4\DomainModel\Follow\FollowRepository;
use Doctrine\ORM\EntityManagerInterface;
//snippet doctrine-orm-follows
final class DoctrineOrmFollowRepository implements FollowRepository
{
public function __construct(
private EntityManagerInterface $em
) {
}
public function numberOfFollowersFor(AuthorId $authorId): int
{
return 13;
}
public function add(Follow $follow): void
{
$this->em->persist($follow);
}
public function ofFromAuthorIdAndToAuthorId(AuthorId $fromAuthorId, AuthorId $toAuthorId): ?Follow
{
return $this->em
->getRepository(Follow::class)->findOneBy([
'fromAuthorId' => $fromAuthorId,
'toAuthorId' => $toAuthorId,
]);
}
public function toAuthorId(AuthorId $authorId): array
{
return $this->em
->getRepository(Follow::class)
->findBy(['toAuthorId' => $authorId]);
}
}
//end-snippet

View File

@@ -6,7 +6,7 @@ namespace Cheeper\Chapter5\Application\Query\CountFollowersHandlerWithRepositori
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Author\AuthorRepository;
use Cheeper\Chapter4\DomainModel\Author\AuthorRepository;
use Cheeper\Chapter5\Application\Query\CountFollowers;
use Cheeper\Chapter5\Application\Query\CountFollowersResponse;
use Cheeper\Chapter5\DomainModel\Follow\Followers;

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Cheeper\Chapter6\Application\Command\Author;
//snippet follow-command
final class Follow
final class FollowCommand
{
private function __construct(
private string $fromAuthorId,

View File

@@ -9,7 +9,7 @@ use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Author\AuthorRepository;
use Cheeper\AllChapters\DomainModel\Follow\Follows;
use Cheeper\Chapter6\Application\Command\Author\Follow;
use Cheeper\Chapter6\Application\Command\Author\FollowCommand;
use Cheeper\Chapter6\Application\Event\EventBus;
//snippet follow-handler-with-event
@@ -24,7 +24,7 @@ final class FollowHandler
) {
}
public function __invoke(Follow $command): void
public function __invoke(FollowCommand $command): void
{
$fromAuthorId = AuthorId::fromString($command->fromAuthorId());
$toAuthorId = AuthorId::fromString($command->toAuthorId());

View File

@@ -4,12 +4,10 @@ declare(strict_types=1);
namespace Cheeper\Chapter6\Application\Command\Author\WithIdempotency;
use Cheeper\AllChapters\DomainModel\Author\Author;
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Author\AuthorRepository;
use Cheeper\AllChapters\DomainModel\Follow\Follows;
use Cheeper\Chapter6\Application\Command\Author\Follow;
use Cheeper\Chapter6\Application\Command\Author\FollowCommand;
use Cheeper\Chapter6\Application\Event\EventBus;
final class FollowHandler
@@ -24,7 +22,7 @@ final class FollowHandler
}
//snippet idempotency-example
public function __invoke(Follow $command): void
public function __invoke(FollowCommand $command): void
{
$fromAuthorId = AuthorId::fromString($command->fromAuthorId());
$toAuthorId = AuthorId::fromString($command->toAuthorId());

View File

@@ -4,12 +4,10 @@ declare(strict_types=1);
namespace Cheeper\Chapter6\Application\Command\Author\WithoutDomainEvents;
use Cheeper\AllChapters\DomainModel\Author\Author;
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Author\AuthorRepository;
use Cheeper\AllChapters\DomainModel\Follow\Follows;
use Cheeper\Chapter6\Application\Command\Author\Follow;
use Cheeper\Chapter6\Application\Command\Author\FollowCommand;
//snippet follow-handler-without-event
final class FollowHandler
@@ -20,7 +18,7 @@ final class FollowHandler
) {
}
public function __invoke(Follow $command): void
public function __invoke(FollowCommand $command): void
{
$fromAuthorId = AuthorId::fromString($command->fromAuthorId());
$toAuthorId = AuthorId::fromString($command->toAuthorId());

View File

@@ -6,6 +6,8 @@ namespace Cheeper\Tests\Chapter2;
use Cheeper\Chapter2\Author;
use Cheeper\Chapter2\Hexagonal\Application\CheepService;
use Cheeper\Chapter2\Hexagonal\DomainModel\AuthorRepository;
use Cheeper\Chapter2\Hexagonal\DomainModel\CheepRepository;
use Cheeper\Chapter2\Layered\AuthorDAO;
use Cheeper\Chapter2\Layered\CheepDAO;
use PHPUnit\Framework\TestCase;
@@ -13,14 +15,14 @@ use PHPUnit\Framework\TestCase;
//snippet cheep-service-test
final class CheepServiceTest extends TestCase
{
private CheepDAO $cheeps;
private AuthorDAO $authors;
private CheepRepository $cheeps;
private AuthorRepository $authors;
private CheepService $cheepService;
public function setUp(): void
{
$this->cheeps = \Mockery::mock(CheepDAO::class);
$this->authors = \Mockery::mock(AuthorDAO::class);
$this->cheeps = \Mockery::mock(CheepRepository::class);
$this->authors = \Mockery::mock(AuthorRepository::class);
$this->cheepService = new CheepService($this->authors, $this->cheeps);
}
@@ -29,7 +31,7 @@ final class CheepServiceTest extends TestCase
{
$this->expectException(\RuntimeException::class);
$this->authors->allows('byUsername')->andReturns(null);
$this->authors->allows('ofUserName')->andReturns(null);
$this->cheepService->postCheep('irrelevant', 'irrelevant');
}
@@ -37,7 +39,7 @@ final class CheepServiceTest extends TestCase
/** @test */
public function itShouldAddCheep(): void
{
$this->authors->allows('byUsername')->andReturns(self::anAuthor());
$this->authors->allows('ofUsername')->andReturns(self::anAuthor());
$this->cheeps->expects('add');
$cheep = $this->cheepService->postCheep('irrelevant', 'message');

View File

@@ -6,7 +6,7 @@ namespace Cheeper\Tests\Chapter4\Application\Cheep\Command;
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Cheep\CheepId;
use Cheeper\AllChapters\DomainModel\Cheep\CheepPosted;
use Cheeper\Chapter4\DomainModel\Cheep\CheepPosted;
use Cheeper\Chapter4\Application\Cheep\Command\PostCheepCommand;
use Cheeper\Chapter4\Application\Cheep\Command\PostCheepCommandHandler;
use Cheeper\Chapter4\Infrastructure\Application\InMemoryEventBus;

View File

@@ -2,14 +2,14 @@
declare(strict_types=1);
namespace Cheeper\Tests\AllChapters\DomainModel\Author;
namespace Cheeper\Tests\Chapter4\DomainModel\Author;
use Cheeper\AllChapters\DomainModel\Author\Author;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Author\BirthDate;
use Cheeper\AllChapters\DomainModel\Author\EmailAddress;
use Cheeper\AllChapters\DomainModel\Author\UserName;
use Cheeper\AllChapters\DomainModel\Author\Website;
use Cheeper\Chapter4\DomainModel\Author\Author;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;

View File

@@ -4,26 +4,21 @@ declare(strict_types=1);
namespace Cheeper\Tests\Chapter6\Application\Command\Author\WithDomainEvents;
use Cheeper\Chapter4\DomainModel\Author\Author;
use Cheeper\Chapter4\Infrastructure\DomainModel\Author\InMemoryAuthorRepository;
use Cheeper\Chapter6\Application\Command\Author\WithDomainEvents\FollowHandler;
use Cheeper\Chapter6\Infrastructure\Application\Event\InMemoryEventBus;
use Cheeper\AllChapters\DomainModel\Follow\AuthorFollowed;
use Cheeper\AllChapters\DomainModel\Follow\Follow as FollowRelation;
use Cheeper\Chapter6\Application\Command\Author\Follow;
use Cheeper\AllChapters\DomainModel\Author\Author;
use Cheeper\AllChapters\DomainModel\Follow\Follow;
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Follow\FollowId;
use Cheeper\AllChapters\DomainModel\Author\EmailAddress;
use Cheeper\AllChapters\DomainModel\Author\UserName;
use Cheeper\AllChapters\Infrastructure\Persistence\InMemoryAuthorRepository;
use Cheeper\AllChapters\Infrastructure\Persistence\InMemoryFollows;
use Cheeper\Tests\Helper\SendsCommands;
use PHPUnit\Framework\TestCase;
final class FollowHandlerTest extends TestCase
{
use SendsCommands;
private const AUTHOR_ID_FROM = '400ea77d-0c8c-44f2-abe8-db05d0852966';
private const AUTHOR_ID_TO = '52d8f0b5-544f-46e0-84dc-f8b513391a0e';
@@ -32,6 +27,16 @@ final class FollowHandlerTest extends TestCase
private const EMAIL_KEYVAN = 'keyvan.akbary@gmail.com';
private const EMAIL_CARLOS = 'carlos.buenosvinos@gmail.com';
private InMemoryAuthorRepository $authorRepository;
private InMemoryEventBus $eventBus;
protected function setUp(): void
{
$this->authorRepository = new InMemoryAuthorRepository();
$this->authorRepository = new InMemoryAuthorRepository();
$this->eventBus = new InMemoryEventBus();
}
/** @test */
public function givenTwoNonExistingAuthorsWhenFollowingOneToAnotherOneNonExistingAuthorExceptionShouldBeThrown(): void
{
@@ -45,7 +50,7 @@ final class FollowHandlerTest extends TestCase
{
$this->expectException(AuthorDoesNotExist::class);
$this->authors->add(
$this->authorRepository->add(
$this->buildAuthor(
self::AUTHOR_ID_TO,
self::USERNAME_CARLOS,
@@ -61,7 +66,7 @@ final class FollowHandlerTest extends TestCase
{
$this->expectException(AuthorDoesNotExist::class);
$this->authors->add(
$this->authorRepository->add(
$this->buildAuthor(
self::AUTHOR_ID_FROM,
self::USERNAME_CARLOS,
@@ -91,8 +96,8 @@ final class FollowHandlerTest extends TestCase
EmailAddress::from(self::EMAIL_KEYVAN)
);
$this->authors->add($fromAuthor);
$this->authors->add($toAuthor);
$this->authorRepository->add($fromAuthor);
$this->authorRepository->add($toAuthor);
$this->follows->add(
FollowRelation::fromAuthorToAuthor(
FollowId::fromString($followId),
@@ -130,8 +135,8 @@ final class FollowHandlerTest extends TestCase
EmailAddress::from(self::EMAIL_KEYVAN)
);
$this->authors->add($fromAuthor);
$this->authors->add($toAuthor);
$this->authorRepository->add($fromAuthor);
$this->authorRepository->add($toAuthor);
$this->runHandler($fromAuthorId, $toAuthorId);
@@ -159,7 +164,7 @@ final class FollowHandlerTest extends TestCase
$this->eventBus->reset();
(new FollowHandler(
$this->authors,
$this->authorRepository,
$this->follows,
$this->eventBus
))(

View File

@@ -4,24 +4,19 @@ declare(strict_types=1);
namespace Cheeper\Tests\Chapter6\Application\Command\Author\WithoutDomainEvents;
use Cheeper\Chapter4\DomainModel\Author\Author;
use Cheeper\Chapter6\Application\Command\Author\WithoutDomainEvents\FollowHandler;
use Cheeper\AllChapters\DomainModel\Follow\Follow as FollowRelation;
use Cheeper\Chapter6\Application\Command\Author\Follow;
use Cheeper\AllChapters\DomainModel\Author\Author;
use Cheeper\Chapter6\Application\Command\Author\FollowCommand;
use Cheeper\AllChapters\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
use Cheeper\AllChapters\DomainModel\Follow\FollowId;
use Cheeper\AllChapters\DomainModel\Author\EmailAddress;
use Cheeper\AllChapters\DomainModel\Author\UserName;
use Cheeper\AllChapters\Infrastructure\Persistence\InMemoryAuthorRepository;
use Cheeper\AllChapters\Infrastructure\Persistence\InMemoryFollows;
use Cheeper\Tests\Helper\SendsCommands;
use PHPUnit\Framework\TestCase;
final class FollowHandlerTest extends TestCase
{
use SendsCommands;
private const AUTHOR_ID_FROM = '400ea77d-0c8c-44f2-abe8-db05d0852966';
private const AUTHOR_ID_TO = '52d8f0b5-544f-46e0-84dc-f8b513391a0e';
@@ -149,7 +144,7 @@ final class FollowHandlerTest extends TestCase
$this->authors,
$this->follows,
))(
Follow::fromAuthorIdToAuthorId(
FollowCommand::fromAuthorIdToAuthorId(
$fromAuthorId,
$toAuthorId
)