diff --git a/README.md b/README.md
index 0499290..04c6e35 100644
--- a/README.md
+++ b/README.md
@@ -40,3 +40,24 @@ And to stop all services just run
This code can also be run using Symfony Local Webserver.
+### Fixtures
+
+ make database
+
+#### Adding Authors
+
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/author author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" username="bob" email="bob@bob.com"
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/author author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" username="alice" email="alice@alice.com"
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/author author_id="1da1366f-b066-4514-9b29-7346df41e371" username="charlie" email="charlie@charlie.com"
+
+#### Posting Cheeps
+
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/cheep cheep_id="28bc90bd-2dfb-4b71-962f-81f02b0b3149" author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" message="Hello world! This is Bob!"
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/cheep cheep_id="04efc3af-59a3-4695-803f-d37166c3af56" author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" message="Hello world! This is Alice!"
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/cheep cheep_id="8a5539e6-3be2-4fa7-906e-179efcfca46b" author_id="1da1366f-b066-4514-9b29-7346df41e371" message="Hello world! This is Charlie!"
+
+#### Following other Authors
+
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/follow follow_id="8cc71bf2-f827-4c92-95a5-43bb1bc622ad" from_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" to_author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f"
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/follow follow_id="f3088920-841e-4577-a3c2-efdc80f0dea5" from_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" to_author_id="1da1366f-b066-4514-9b29-7346df41e371"
+ http --json --verify false POST https://127.0.0.1:8000/chapter7/follow follow_id="45ea9e38-d821-4c8c-8619-362bf57f4c56" from_author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" to_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119"
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 7f18c59..1d5cd1a 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -23,3 +23,9 @@ doctrine:
dir: '%kernel.project_dir%/src/Cheeper/Infrastructure/Persistence/doctrine-mappings'
prefix: 'Cheeper\DomainModel'
alias: CheeperDomainModel
+ CheeperChapter7DomainModel:
+ is_bundle: false
+ type: xml
+ dir: '%kernel.project_dir%/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings'
+ prefix: 'Cheeper\Chapter7\DomainModel'
+ alias: CheeperChapter7DomainModel
diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml
index ed4b427..4129df9 100644
--- a/config/packages/messenger.yaml
+++ b/config/packages/messenger.yaml
@@ -37,9 +37,10 @@ framework:
Cheeper\Application\Command\AsyncCommand: async_commands
Cheeper\Application\Command\SyncCommand: sync_commands
- # Cheeper\Chapter7\Application\Command\Author\SignUp: chapter7_sync_commands
- Cheeper\Chapter7\Application\Command\Author\SignUp: chapter7_async_commands
- Cheeper\Chapter7\Application\Command\Author\Follow: chapter7_async_commands
+ # Cheeper\Chapter7\Application\Command\Author\SignUp: chapter7_async_commands
+ Cheeper\Chapter7\Application\Command\Author\SignUp: chapter7_sync_commands
+ Cheeper\Chapter7\Application\Command\Author\Follow: chapter7_sync_commands
+ Cheeper\Chapter7\Application\Command\Cheep\PostCheep: chapter7_sync_commands
# Domain Events are sent to asynchronous
# transport to be processed later.
diff --git a/config/services.yaml b/config/services.yaml
index 69a560e..584b58c 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -17,6 +17,9 @@ services:
# ignore
Cheeper\DomainModel\Cheep\Cheeps $cheeps: '@Cheeper\Infrastructure\Persistence\DoctrineOrmCheeps'
Cheeper\DomainModel\Author\Authors $authors: '@Cheeper\Infrastructure\Persistence\DoctrineOrmAuthors'
+ Cheeper\Chapter7\DomainModel\Author\Authors $authors: '@Cheeper\Chapter7\Infrastructure\Persistence\DoctrineOrmAuthors'
+ Cheeper\Chapter7\DomainModel\Cheep\Cheeps $cheeps: '@Cheeper\Chapter7\Infrastructure\Persistence\DoctrineOrmCheeps'
+ Cheeper\Chapter7\DomainModel\Follow\Follows $follows: '@Cheeper\Chapter7\Infrastructure\Persistence\DoctrineOrmFollows'
Cheeper\DomainModel\Author\Authors $authorsRepository: '@Cheeper\Infrastructure\Persistence\DoctrineOrmAuthors'
Cheeper\DomainModel\Follow\Follows $follows: '@Cheeper\Infrastructure\Persistence\DoctrineOrmFollows'
#end-ignore
@@ -99,11 +102,25 @@ services:
- { name: messenger.message_handler, bus: event.bus }
class: 'Cheeper\Chapter6\Infrastructure\Application\Projector\Author\SymfonyAuthorFollowedHandler'
+ # CHAPTER 7
+ Cheeper\Chapter7\Infrastructure\:
+ resource: '../src/Cheeper/Chapter7/Infrastructure/**/*.php'
+
Cheeper\Chapter7\Application\Command\Author\SignUpHandler:
tags:
- { name: messenger.message_handler, bus: command.bus }
class: 'Cheeper\Chapter7\Application\Command\Author\SignUpHandler'
+ Cheeper\Chapter7\Application\Command\Cheep\PostCheepHandler:
+ tags:
+ - { name: messenger.message_handler, bus: command.bus }
+ class: 'Cheeper\Chapter7\Application\Command\Cheep\PostCheepHandler'
+
+ Cheeper\Chapter7\Application\Command\Author\FollowHandler:
+ tags:
+ - { name: messenger.message_handler, bus: command.bus }
+ class: 'Cheeper\Chapter7\Application\Command\Author\FollowHandler'
+
App\EventListener\UserCreatedListener:
tags:
- name: "doctrine.orm.entity_listener"
diff --git a/src/App/Controller/Chapter7/DefaultController.php b/src/App/Controller/Chapter7/DefaultController.php
deleted file mode 100644
index 42af9c7..0000000
--- a/src/App/Controller/Chapter7/DefaultController.php
+++ /dev/null
@@ -1,37 +0,0 @@
-commandBus->handle(
- Follow::fromAuthorToAuthor(
- FollowId::nextIdentity(),
- AuthorId::nextIdentity(),
- AuthorId::nextIdentity(),
- )
- );
-
- return $this->json("hello");
- }
-}
diff --git a/src/App/Controller/Chapter7/FollowAuthorController.php b/src/App/Controller/Chapter7/FollowAuthorController.php
new file mode 100644
index 0000000..b1441c9
--- /dev/null
+++ b/src/App/Controller/Chapter7/FollowAuthorController.php
@@ -0,0 +1,60 @@
+getRequestContentInJson($request)
+ );
+
+ $commandBus->handle($command);
+
+ $httpContent = [
+ 'message_id' => $command->messageId()?->toString()
+ ];
+ } catch (
+ AuthorDoesNotExist
+ |InvalidArgumentException $exception
+ ) {
+ $httpCode = Response::HTTP_INTERNAL_SERVER_ERROR;
+ $httpContent = ['message' => $exception->getMessage()];
+ }
+
+ return $this->buildJsonResponse($httpContent, $httpCode);
+ }
+
+ private function getRequestContentInJson(Request $request): mixed
+ {
+ return \Safe\json_decode(
+ $request->getContent(),
+ true
+ );
+ }
+
+ private function buildJsonResponse(array $httpContent, int $httpCode): JsonResponse
+ {
+ return $this->json(
+ data: $httpContent,
+ status: $httpCode,
+ );
+ }
+}
diff --git a/src/App/Controller/Chapter7/PostCheepController.php b/src/App/Controller/Chapter7/PostCheepController.php
new file mode 100644
index 0000000..227e64f
--- /dev/null
+++ b/src/App/Controller/Chapter7/PostCheepController.php
@@ -0,0 +1,59 @@
+getRequestContentInJson($request)
+ );
+
+ $commandBus->handle($command);
+ $httpContent = [
+ 'message_id' => $command->messageId()?->toString(),
+ 'cheep_id' => $command->authorId(),
+ ];
+ } catch (
+ AuthorDoesNotExist
+ |InvalidArgumentException $exception
+ ) {
+ $httpCode = Response::HTTP_INTERNAL_SERVER_ERROR;
+ $httpContent = ['message' => $exception->getMessage()];
+ }
+
+ return $this->buildJsonResponse($httpContent, $httpCode);
+ }
+
+ private function getRequestContentInJson(Request $request): mixed
+ {
+ return \Safe\json_decode(
+ $request->getContent(),
+ true
+ );
+ }
+
+ private function buildJsonResponse(array $httpContent, int $httpCode): JsonResponse
+ {
+ return $this->json(
+ data: $httpContent,
+ status: $httpCode,
+ );
+ }
+}
diff --git a/src/App/Controller/Chapter7/SignUpAuthorController.php b/src/App/Controller/Chapter7/SignUpAuthorController.php
index d2067ca..6f4a4e4 100644
--- a/src/App/Controller/Chapter7/SignUpAuthorController.php
+++ b/src/App/Controller/Chapter7/SignUpAuthorController.php
@@ -6,7 +6,10 @@ namespace App\Controller\Chapter7;
use App\Messenger\CommandBus;
use Cheeper\Chapter7\Application\Command\Author\SignUp;
+use Cheeper\DomainModel\Author\AuthorAlreadyExists;
+use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -14,21 +17,28 @@ use Symfony\Component\Routing\Annotation\Route;
final class SignUpAuthorController extends AbstractController
{
#[Route("/chapter7/author", methods: ["POST"])]
- public function follow(Request $request, CommandBus $commandBus): Response
+ public function __invoke(Request $request, CommandBus $commandBus): Response
{
- $command = SignUp::fromArray(
- $this->getRequestContentInJson($request)
- );
+ $httpCode = Response::HTTP_ACCEPTED;
+ try {
+ $command = SignUp::fromArray(
+ $this->getRequestContentInJson($request)
+ );
- $commandBus->handle($command);
-
- return $this->json(
- data: [
- 'message_id' => $command->messageId()->toString(),
+ $commandBus->handle($command);
+ $httpContent = [
+ 'message_id' => $command->messageId()?->toString(),
'author_id' => $command->authorId(),
- ],
- status: Response::HTTP_CREATED,
- );
+ ];
+ } catch (
+ AuthorAlreadyExists
+ |InvalidArgumentException $exception
+ ) {
+ $httpCode = Response::HTTP_INTERNAL_SERVER_ERROR;
+ $httpContent = ['message' => $exception->getMessage()];
+ }
+
+ return $this->buildJsonResponse($httpContent, $httpCode);
}
private function getRequestContentInJson(Request $request): mixed
@@ -38,5 +48,13 @@ final class SignUpAuthorController extends AbstractController
true
);
}
+
+ private function buildJsonResponse(array $httpContent, int $httpCode): JsonResponse
+ {
+ return $this->json(
+ data: $httpContent,
+ status: $httpCode,
+ );
+ }
}
diff --git a/src/App/Messenger/FromScratch.php b/src/App/Messenger/FromScratch.php
index 1927599..e254e8a 100644
--- a/src/App/Messenger/FromScratch.php
+++ b/src/App/Messenger/FromScratch.php
@@ -2,6 +2,8 @@
declare(strict_types=1);
+namespace App\Messenger;
+
use Cheeper\Application\Command\Cheep\PostCheep;
use Cheeper\Application\Command\Cheep\PostCheepHandler;
use Cheeper\Chapter6\Infrastructure\Application\Event\InMemoryEventBus;
diff --git a/src/Cheeper/Chapter7/Application/Command/Author/Follow.php b/src/Cheeper/Chapter7/Application/Command/Author/Follow.php
index 8ce7797..4be235a 100644
--- a/src/Cheeper/Chapter7/Application/Command/Author/Follow.php
+++ b/src/Cheeper/Chapter7/Application/Command/Author/Follow.php
@@ -5,15 +5,18 @@ declare(strict_types=1);
namespace Cheeper\Chapter7\Application\Command\Author;
use Cheeper\Chapter7\Application\MessageTrait;
+use Cheeper\DomainModel\Follow\FollowId;
final class Follow
{
use MessageTrait;
private function __construct(
+ private string $followId,
private string $fromAuthorId,
private string $toAuthorId
) {
+ $this->stampAsNewMessage();
}
public function fromAuthorId(): string
@@ -26,8 +29,22 @@ final class Follow
return $this->toAuthorId;
}
+ public function followId(): string
+ {
+ return $this->followId;
+ }
+
public static function fromAuthorIdToAuthorId(string $from, string $to): self
{
- return new self($from, $to);
+ return new self($from, $to, FollowId::nextIdentity()->toString());
}
-}
+
+ public static function fromArray(array $array): self
+ {
+ return new self(
+ $array['follow_id'] ?? '',
+ $array['from_author_id'] ?? '',
+ $array['to_author_id'] ?? '',
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Cheeper/Chapter7/Application/Command/Author/FollowHandler.php b/src/Cheeper/Chapter7/Application/Command/Author/FollowHandler.php
index 0663826..f3d3f6f 100644
--- a/src/Cheeper/Chapter7/Application/Command/Author/FollowHandler.php
+++ b/src/Cheeper/Chapter7/Application/Command/Author/FollowHandler.php
@@ -5,11 +5,11 @@ declare(strict_types=1);
namespace Cheeper\Chapter7\Application\Command\Author;
use Cheeper\Chapter6\Application\Event\EventBus;
-use Cheeper\DomainModel\Author\Author;
+use Cheeper\Chapter7\DomainModel\Author\Author;
+use Cheeper\Chapter7\DomainModel\Author\Authors;
+use Cheeper\Chapter7\DomainModel\Follow\Follows;
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\DomainModel\Author\AuthorId;
-use Cheeper\DomainModel\Author\Authors;
-use Cheeper\DomainModel\Follow\Follows;
final class FollowHandler
{
diff --git a/src/Cheeper/Chapter7/Application/Command/Author/SignUp.php b/src/Cheeper/Chapter7/Application/Command/Author/SignUp.php
index c5c8cc4..f920e01 100644
--- a/src/Cheeper/Chapter7/Application/Command/Author/SignUp.php
+++ b/src/Cheeper/Chapter7/Application/Command/Author/SignUp.php
@@ -23,6 +23,7 @@ final class SignUp
private ?string $website = null,
private ?string $birthDate = null,
) {
+ $this->stampAsNewMessage();
}
//ignore
@@ -68,11 +69,11 @@ final class SignUp
public static function fromArray(array $array): self
{
- return (new self(
+ return new self(
$array['author_id'] ?? '',
$array['username'] ?? '',
$array['email'] ?? '',
- ))->stampAsNewMessage();
+ );
}
//end-ignore
}
diff --git a/src/Cheeper/Chapter7/Application/Command/Author/SignUpHandler.php b/src/Cheeper/Chapter7/Application/Command/Author/SignUpHandler.php
index b37ec5b..b236b66 100644
--- a/src/Cheeper/Chapter7/Application/Command/Author/SignUpHandler.php
+++ b/src/Cheeper/Chapter7/Application/Command/Author/SignUpHandler.php
@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace Cheeper\Chapter7\Application\Command\Author;
use Cheeper\Chapter6\Application\Event\EventBus;
-use Cheeper\DomainModel\Author\Author;
+use Cheeper\Chapter7\DomainModel\Author\Author;
+use Cheeper\Chapter7\DomainModel\Author\Authors;
use Cheeper\DomainModel\Author\AuthorAlreadyExists;
use Cheeper\DomainModel\Author\AuthorId;
-use Cheeper\DomainModel\Author\Authors;
use Cheeper\DomainModel\Author\BirthDate;
use Cheeper\DomainModel\Author\EmailAddress;
use Cheeper\DomainModel\Author\UserName;
diff --git a/src/Cheeper/Chapter7/Application/Command/Cheep/PostCheep.php b/src/Cheeper/Chapter7/Application/Command/Cheep/PostCheep.php
new file mode 100644
index 0000000..11b5ec9
--- /dev/null
+++ b/src/Cheeper/Chapter7/Application/Command/Cheep/PostCheep.php
@@ -0,0 +1,46 @@
+stampAsNewMessage();
+ }
+
+ public static function fromArray(array $array): self
+ {
+ return new self(
+ $array['cheep_id'] ?? '',
+ $array['author_id'] ?? '',
+ $array['message'] ?? '',
+ );
+ }
+
+ public function cheepId(): string
+ {
+ return $this->cheepId;
+ }
+
+ public function authorId(): string
+ {
+ return $this->authorId;
+ }
+
+ public function message(): string
+ {
+ return $this->message;
+ }
+}
+//end-snippet
diff --git a/src/Cheeper/Chapter7/Application/Command/Cheep/PostCheepHandler.php b/src/Cheeper/Chapter7/Application/Command/Cheep/PostCheepHandler.php
new file mode 100644
index 0000000..b78ac7a
--- /dev/null
+++ b/src/Cheeper/Chapter7/Application/Command/Cheep/PostCheepHandler.php
@@ -0,0 +1,49 @@
+authorId());
+ $cheepId = CheepId::fromString($command->cheepId());
+ $message = CheepMessage::write($command->message());
+
+ $author = $this->authors->ofId($authorId);
+ $this->checkAuthorExists($author, $authorId);
+
+ $cheep = Cheep::compose($authorId, $cheepId, $message);
+ $this->cheeps->add($cheep);
+
+ $this->eventBus->notifyAll($cheep->domainEvents());
+ }
+
+ private function checkAuthorExists(?Author $author, AuthorId $authorId): void
+ {
+ if (null === $author) {
+ throw AuthorDoesNotExist::withAuthorIdOf($authorId);
+ }
+ }
+}
+//end-snippet
diff --git a/src/Cheeper/Chapter7/DomainModel/Author/Author.php b/src/Cheeper/Chapter7/DomainModel/Author/Author.php
index abfb442..9c0b092 100644
--- a/src/Cheeper/Chapter7/DomainModel/Author/Author.php
+++ b/src/Cheeper/Chapter7/DomainModel/Author/Author.php
@@ -5,32 +5,126 @@ declare(strict_types=1);
namespace Cheeper\Chapter7\DomainModel\Author;
use Cheeper\Chapter7\DomainModel\Follow\Follow;
-use Cheeper\DomainModel\Author\Author as AuthorChapter6;
use Cheeper\DomainModel\Author\AuthorId;
+use Cheeper\DomainModel\Author\BirthDate;
+use Cheeper\DomainModel\Author\EmailAddress;
+use Cheeper\DomainModel\Author\UserName;
+use Cheeper\DomainModel\Author\Website;
use Cheeper\DomainModel\Follow\FollowId;
+use Cheeper\DomainModel\TriggerEventsTrait;
use DateTimeImmutable;
+use InvalidArgumentException;
-class Author extends AuthorChapter6
+final class Author
{
- protected function __construct(
- protected string $authorId,
- protected string $userName,
- protected string $email,
- protected ?string $name = null,
- protected ?string $biography = null,
- protected ?string $location = null,
- protected ?string $website = null,
- protected ?DateTimeImmutable $birthDate = null,
+ use TriggerEventsTrait;
+
+ private function __construct(
+ private string $authorId,
+ private string $userName,
+ private string $email,
+ private ?string $name = null,
+ private ?string $biography = null,
+ private ?string $location = null,
+ private ?string $website = null,
+ private ?DateTimeImmutable $birthDate = null,
) {
$this->setName($name);
$this->setBiography($biography);
$this->setLocation($location);
$this->notifyDomainEvent(
- NewAuthorSigned::fromAuthor($this)
+ $this->buildNewAuthorSignedDomainEvent()
);
}
+ protected function buildNewAuthorSignedDomainEvent(): NewAuthorSigned
+ {
+ return NewAuthorSigned::fromAuthor($this);
+ }
+
+ public static function signUp(
+ AuthorId $authorId,
+ UserName $userName,
+ EmailAddress $email,
+ ?string $name = null,
+ ?string $biography = null,
+ ?string $location = null,
+ ?Website $website = null,
+ ?BirthDate $birthDate = null
+ ): static {
+ return new static(
+ $authorId->toString(),
+ $userName->userName(),
+ $email->value(),
+ $name,
+ $biography,
+ $location,
+ $website?->toString(),
+ $birthDate?->date()
+ );
+ }
+
+ protected function setName(?string $name): void
+ {
+ $this->name = $this->checkIsNotNull($name, 'Name cannot be empty');
+ }
+
+ protected function setBiography(?string $biography): void
+ {
+ $this->biography = $this->checkIsNotNull($biography, 'Biography cannot be empty');
+ }
+
+ protected function setLocation(?string $location): void
+ {
+ $this->location = $this->checkIsNotNull($location, 'Location cannot be empty');
+ }
+
+ public function authorId(): AuthorId
+ {
+ return AuthorId::fromString($this->authorId);
+ }
+
+ public function userId(): AuthorId
+ {
+ return AuthorId::fromString($this->authorId);
+ }
+
+ public function userName(): UserName
+ {
+ return UserName::pick($this->userName);
+ }
+
+ public function email(): EmailAddress
+ {
+ return EmailAddress::from($this->email);
+ }
+
+ public function name(): ?string
+ {
+ return $this->name;
+ }
+
+ public function biography(): ?string
+ {
+ return $this->biography;
+ }
+
+ public function location(): ?string
+ {
+ return $this->location;
+ }
+
+ public function website(): ?Website
+ {
+ return $this->website !== null ? Website::fromString($this->website) : null;
+ }
+
+ public function birthDate(): ?BirthDate
+ {
+ return $this->birthDate !== null ? BirthDate::fromString($this->birthDate->format('Y-m-d')) : null;
+ }
+
public function followAuthorId(AuthorId $toFollow): Follow
{
return Follow::fromAuthorToAuthor(
@@ -39,4 +133,13 @@ class Author extends AuthorChapter6
toAuthorId: $toFollow
);
}
-}
+
+ private function checkIsNotNull(?string $value, string $errorMessage): ?string
+ {
+ if ('' === $value) {
+ throw new InvalidArgumentException($errorMessage);
+ }
+
+ return $value;
+ }
+}
\ No newline at end of file
diff --git a/src/Cheeper/Chapter7/DomainModel/Author/Authors.php b/src/Cheeper/Chapter7/DomainModel/Author/Authors.php
new file mode 100644
index 0000000..2d8b219
--- /dev/null
+++ b/src/Cheeper/Chapter7/DomainModel/Author/Authors.php
@@ -0,0 +1,17 @@
+authorId()->toString(),
+ new DateTimeImmutable(
+ timezone: new DateTimeZone("UTC")
+ )
+ );
+ }
+
+ public function authorId(): string
+ {
+ return $this->authorId;
+ }
+
+ public function occurredOn(): DateTimeImmutable
+ {
+ return $this->occurredOn;
+ }
}
+// end-snippet
diff --git a/src/Cheeper/Chapter7/DomainModel/Cheep/Cheep.php b/src/Cheeper/Chapter7/DomainModel/Cheep/Cheep.php
new file mode 100644
index 0000000..891a125
--- /dev/null
+++ b/src/Cheeper/Chapter7/DomainModel/Cheep/Cheep.php
@@ -0,0 +1,66 @@
+notifyDomainEvent(
+ CheepPosted::fromCheep($this)
+ );
+ }
+
+ public static function compose(AuthorId $authorId, CheepId $cheepId, CheepMessage $cheepMessage): self
+ {
+ $cheepDate = new CheepDate(
+ (new \DateTimeImmutable(timezone: new \DateTimeZone('UTC')))->format('Y-m-d H:i:s')
+ );
+
+ return new self(
+ $authorId,
+ $cheepId,
+ $cheepMessage,
+ $cheepDate
+ );
+ }
+
+ final public function authorId(): AuthorId
+ {
+ return $this->authorId;
+ }
+
+ final public function cheepId(): CheepId
+ {
+ return $this->cheepId;
+ }
+
+ final public function cheepMessage(): CheepMessage
+ {
+ return $this->cheepMessage;
+ }
+
+ final public function cheepDate(): CheepDate
+ {
+ return $this->cheepDate;
+ }
+
+ final public function recomposeWith(CheepMessage $cheepMessage): void
+ {
+ $this->cheepMessage = $cheepMessage;
+ }
+}
diff --git a/src/Cheeper/Chapter7/DomainModel/Cheep/CheepPosted.php b/src/Cheeper/Chapter7/DomainModel/Cheep/CheepPosted.php
new file mode 100644
index 0000000..8e2cfd5
--- /dev/null
+++ b/src/Cheeper/Chapter7/DomainModel/Cheep/CheepPosted.php
@@ -0,0 +1,65 @@
+stampAsNewMessage();
+ }
+
+ public static function fromCheep(Cheep $cheep): self
+ {
+ return new self(
+ $cheep->cheepId()->toString(),
+ $cheep->authorId()->toString(),
+ $cheep->cheepMessage()->message(),
+ $cheep->cheepDate()->date(),
+ new DateTimeImmutable(
+ timezone: new DateTimeZone("UTC")
+ )
+ );
+ }
+
+ public function cheepId(): string
+ {
+ return $this->cheepId;
+ }
+
+ public function authorId(): string
+ {
+ return $this->authorId;
+ }
+
+ public function cheepMessage(): string
+ {
+ return $this->cheepMessage;
+ }
+
+ public function cheepDate(): string
+ {
+ return $this->cheepDate;
+ }
+
+ public function occurredOn(): DateTimeImmutable
+ {
+ return $this->occurredOn;
+ }
+}
+// end-snippet
diff --git a/src/Cheeper/Chapter7/DomainModel/Cheep/Cheeps.php b/src/Cheeper/Chapter7/DomainModel/Cheep/Cheeps.php
new file mode 100644
index 0000000..e664620
--- /dev/null
+++ b/src/Cheeper/Chapter7/DomainModel/Cheep/Cheeps.php
@@ -0,0 +1,15 @@
+stampAsNewMessage();
}
public static function fromFollow(Follow $follow): self
diff --git a/src/Cheeper/Chapter7/DomainModel/Follow/Follow.php b/src/Cheeper/Chapter7/DomainModel/Follow/Follow.php
index 1bdb617..e57e102 100644
--- a/src/Cheeper/Chapter7/DomainModel/Follow/Follow.php
+++ b/src/Cheeper/Chapter7/DomainModel/Follow/Follow.php
@@ -4,23 +4,68 @@ declare(strict_types=1);
namespace Cheeper\Chapter7\DomainModel\Follow;
-use Cheeper\DomainModel\Follow\Follow as FollowChapter6;
use Cheeper\DomainModel\Author\AuthorId;
use Cheeper\DomainModel\Follow\FollowId;
use Cheeper\DomainModel\TriggerEventsTrait;
-class Follow extends FollowChapter6
+// snippet follow-entity-with-events
+final class Follow
{
use TriggerEventsTrait;
protected function __construct(
- protected FollowId $followId,
- protected AuthorId $fromAuthorId,
- protected AuthorId $toAuthorId,
+ private string $followId,
+ private string $fromAuthorId,
+ private string $toAuthorId,
) {
$this->notifyDomainEvent(
AuthorFollowed::fromFollow($this)
- ->stampAsNewMessage()
+ );
+
+ /**
+ * As an alternative, we can use a Singleton
+ * implementing an Observer pattern with
+ * Subscribers that will publish the triggered
+ * Domain Events into a queue system like
+ * Rabbit. It's useful for Legacy projects
+ * because you can trigger any Domain Event
+ * from any place in your code, not only
+ * Entities.
+ *
+ * DomainEventPublisher::getInstance()
+ * ->notifyDomainEvent(
+ * AuthorFollowed::fromFollow($this)
+ * )
+ * );
+ */
+ }
+
+ public static function fromAuthorToAuthor(
+ FollowId $followId,
+ AuthorId $fromAuthorId,
+ AuthorId $toAuthorId,
+ ): static
+ {
+ return new static(
+ followId: $followId->toString(),
+ fromAuthorId: $fromAuthorId->toString(),
+ toAuthorId: $toAuthorId->toString()
);
}
+
+ public function fromAuthorId(): AuthorId
+ {
+ return AuthorId::fromString($this->fromAuthorId);
+ }
+
+ public function toAuthorId(): AuthorId
+ {
+ return AuthorId::fromString($this->toAuthorId);
+ }
+
+ public function followId(): FollowId
+ {
+ return FollowId::fromString($this->followId);
+ }
}
+// end-snippet
\ No newline at end of file
diff --git a/src/Cheeper/Chapter7/DomainModel/Follow/Follows.php b/src/Cheeper/Chapter7/DomainModel/Follow/Follows.php
new file mode 100644
index 0000000..ece2349
--- /dev/null
+++ b/src/Cheeper/Chapter7/DomainModel/Follow/Follows.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings/Cheep.Cheep.orm.xml b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings/Cheep.Cheep.orm.xml
new file mode 100644
index 0000000..a823bff
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings/Cheep.Cheep.orm.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings/Follow.Follow.orm.xml b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings/Follow.Follow.orm.xml
new file mode 100644
index 0000000..9000288
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineMappings/Follow.Follow.orm.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmAuthors.php b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmAuthors.php
new file mode 100644
index 0000000..ee8bb8f
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmAuthors.php
@@ -0,0 +1,42 @@
+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
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmCheeps.php b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmCheeps.php
new file mode 100644
index 0000000..d5de767
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmCheeps.php
@@ -0,0 +1,33 @@
+em->persist($cheep);
+ }
+ //end-ignore
+
+ public function ofId(CheepId $cheepId): ?Cheep
+ {
+ return $this->em->find(Cheep::class, Uuid::fromString($cheepId->id()));
+ }
+}
+//end-snippet
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmFollows.php b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmFollows.php
new file mode 100644
index 0000000..c03b2ec
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/DoctrineOrmFollows.php
@@ -0,0 +1,49 @@
+em->persist($follow);
+ }
+
+ public function ofFromAuthorIdAndToAuthorId(AuthorId $fromAuthorId, AuthorId $toAuthorId): ?Follow
+ {
+ $repository = $this->em->getRepository(Follow::class);
+
+ return $repository->findOneBy([
+ 'fromAuthorId' => $fromAuthorId,
+ 'toAuthorId' => $toAuthorId,
+ ]);
+ }
+
+ public function toAuthorId(AuthorId $authorId): array
+ {
+ $repository = $this->em->getRepository(Follow::class);
+
+ return $repository->findBy(['toAuthorId' => $authorId]);
+ }
+}
+//end-snippet
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryAuthors.php b/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryAuthors.php
new file mode 100644
index 0000000..dc97c54
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryAuthors.php
@@ -0,0 +1,59 @@
+authors = [];
+ }
+
+ public function ofId(AuthorId $authorId): ?Author
+ {
+ $candidate = head(
+ select($this->authors, fn (Author $u): bool => $u->authorId()->equals($authorId))
+ );
+
+ if (null === $candidate) {
+ return $candidate;
+ }
+
+ return $candidate;
+ }
+
+ public function ofUserName(UserName $userName): ?Author
+ {
+ $candidate = head(
+ select($this->authors, fn (Author $u): bool => $u->userName()->equalsTo($userName))
+ );
+
+ if (null === $candidate) {
+ return null;
+ }
+
+ return $candidate;
+ }
+
+ public function add(Author $author): void
+ {
+ $candidate = head(
+ select($this->authors, fn (Author $u): bool => $u->authorId()->equals($author->authorId()))
+ );
+
+ if ((null !== $candidate && $candidate !== $author) || null === $candidate) {
+ $this->authors[$author->authorId()->toString()] = $author;
+ }
+ }
+}
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryCheeps.php b/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryCheeps.php
new file mode 100644
index 0000000..042062e
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryCheeps.php
@@ -0,0 +1,27 @@
+items[$cheep->cheepId()->toString()] = $cheep;
+ }
+
+ public function ofId(CheepId $cheepId): ?Cheep
+ {
+ return $this->items[$cheepId->toString()] ?? null;
+ }
+}
+//end-snippet
diff --git a/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryFollows.php b/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryFollows.php
new file mode 100644
index 0000000..b621da8
--- /dev/null
+++ b/src/Cheeper/Chapter7/Infrastructure/Persistence/InMemoryFollows.php
@@ -0,0 +1,68 @@
+ */
+ public array $collection = [];
+
+ public function ofId(FollowId $followId): ?Follow
+ {
+ $candidate = head(
+ select($this->collection, fn (Follow $u): bool => $u->followId()->equals($followId))
+ );
+
+ if (null === $candidate) {
+ return null;
+ }
+
+ return $candidate;
+ }
+
+ public function add(Follow $follow): void
+ {
+ $candidate = head(
+ select($this->collection, fn (Follow $u): bool => $u->fromAuthorId()->equals($follow->fromAuthorId()) && $u->toAuthorId()->equals($follow->toAuthorId()))
+ );
+
+ if ((null !== $candidate && $candidate != $follow) || null === $candidate) {
+ $this->collection[$follow->followId()->toString()] = $follow;
+ }
+ }
+
+ public function numberOfFollowersFor(AuthorId $authorId): int
+ {
+ return reduce_left(
+ $this->collection,
+ function(Follow $f, string $key, array $collection, int $initial) use($authorId): int {
+ return $initial + ($f->fromAuthorId()->equals($authorId) ? 1 : 0);
+ },
+ 0
+ );
+ }
+
+ public function ofFromAuthorIdAndToAuthorId(AuthorId $fromAuthorId, AuthorId $toAuthorId): ?Follow
+ {
+ $candidate = head(
+ select($this->collection, fn (Follow $u): bool => $u->fromAuthorId()->equals($fromAuthorId) && $u->toAuthorId()->equals($toAuthorId))
+ );
+
+ return $candidate ?? null;
+ }
+
+ public function toAuthorId(AuthorId $authorId): array
+ {
+ return select($this->collection, fn (Follow $f): bool => $f->toAuthorId()->equals($authorId));
+ }
+}
diff --git a/src/Cheeper/DomainModel/Author/Author.php b/src/Cheeper/DomainModel/Author/Author.php
index 29531e3..db619ea 100644
--- a/src/Cheeper/DomainModel/Author/Author.php
+++ b/src/Cheeper/DomainModel/Author/Author.php
@@ -8,8 +8,9 @@ use Cheeper\DomainModel\Follow\Follow;
use Cheeper\DomainModel\Follow\FollowId;
use Cheeper\DomainModel\TriggerEventsTrait;
use DateTimeImmutable;
+use InvalidArgumentException;
-class Author
+final class Author
{
use TriggerEventsTrait;
@@ -28,10 +29,15 @@ class Author
$this->setLocation($location);
$this->notifyDomainEvent(
- NewAuthorSigned::fromAuthor($this)
+ $this->buildNewAuthorSignedDomainEvent()
);
}
+ protected function buildNewAuthorSignedDomainEvent(): NewAuthorSigned
+ {
+ return NewAuthorSigned::fromAuthor($this);
+ }
+
public static function signUp(
AuthorId $authorId,
UserName $userName,
@@ -126,7 +132,7 @@ class Author
private function checkIsNotNull(?string $value, string $errorMessage): ?string
{
if ('' === $value) {
- throw new \InvalidArgumentException($errorMessage);
+ throw new InvalidArgumentException($errorMessage);
}
return $value;
diff --git a/src/Cheeper/DomainModel/Author/NewAuthorSigned.php b/src/Cheeper/DomainModel/Author/NewAuthorSigned.php
index 800f32f..93a9443 100644
--- a/src/Cheeper/DomainModel/Author/NewAuthorSigned.php
+++ b/src/Cheeper/DomainModel/Author/NewAuthorSigned.php
@@ -17,9 +17,9 @@ class NewAuthorSigned implements DomainEvent
) {
}
- public static function fromAuthor(Author $author): self
+ public static function fromAuthor(Author $author): static
{
- return new self(
+ return new static(
$author->authorId()->toString(),
new DateTimeImmutable(
timezone: new DateTimeZone("UTC")
diff --git a/tests/Cheeper/Tests/Chapter7/Application/Command/Author/FollowHandlerTest.php b/tests/Cheeper/Tests/Chapter7/Application/Command/Author/FollowHandlerTest.php
index aa50f3d..1d6e831 100644
--- a/tests/Cheeper/Tests/Chapter7/Application/Command/Author/FollowHandlerTest.php
+++ b/tests/Cheeper/Tests/Chapter7/Application/Command/Author/FollowHandlerTest.php
@@ -4,11 +4,15 @@ declare(strict_types=1);
namespace Cheeper\Tests\Chapter7\Application\Command\Author;
+use Cheeper\Chapter6\Infrastructure\Application\Event\InMemoryEventBus;
use Cheeper\Chapter7\Application\Command\Author\FollowHandler;
use Cheeper\Chapter7\Application\Command\Author\Follow;
use Cheeper\Chapter7\DomainModel\Author\Author;
+use Cheeper\Chapter7\DomainModel\Follow\Follow as FollowRelation;
use Cheeper\Chapter7\DomainModel\Follow\AuthorFollowed;
-use Cheeper\DomainModel\Follow\Follow as FollowRelation;
+use Cheeper\Chapter7\Infrastructure\Persistence\InMemoryAuthors;
+use Cheeper\Chapter7\Infrastructure\Persistence\InMemoryFollows;
+
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\DomainModel\Author\AuthorId;
use Cheeper\DomainModel\Follow\FollowId;
@@ -19,8 +23,7 @@ use PHPUnit\Framework\TestCase;
final class FollowHandlerTest extends TestCase
{
- use SendsCommands;
-
+ private const FOLLOW_ID = '337df284-d475-4cbd-89af-12d7451f73f1';
private const AUTHOR_ID_FROM = '400ea77d-0c8c-44f2-abe8-db05d0852966';
private const AUTHOR_ID_TO = '52d8f0b5-544f-46e0-84dc-f8b513391a0e';
@@ -29,12 +32,23 @@ final class FollowHandlerTest extends TestCase
private const EMAIL_KEYVAN = 'keyvan.akbary@gmail.com';
private const EMAIL_CARLOS = 'carlos.buenosvinos@gmail.com';
+ protected function setUp(): void
+ {
+ $this->authors = new InMemoryAuthors();
+ $this->follows = new InMemoryFollows();
+ $this->eventBus = new InMemoryEventBus();
+ }
+
/** @test */
public function givenTwoNonExistingAuthorsWhenFollowingOneToAnotherOneNonExistingAuthorExceptionShouldBeThrown(): void
{
$this->expectException(AuthorDoesNotExist::class);
- $this->runHandler(self::AUTHOR_ID_FROM, self::AUTHOR_ID_TO);
+ $this->runHandler(
+ self::FOLLOW_ID,
+ self::AUTHOR_ID_FROM,
+ self::AUTHOR_ID_TO
+ );
}
/** @test */
@@ -50,7 +64,11 @@ final class FollowHandlerTest extends TestCase
)
);
- $this->runHandler(self::AUTHOR_ID_FROM, self::AUTHOR_ID_TO);
+ $this->runHandler(
+ self::FOLLOW_ID,
+ self::AUTHOR_ID_FROM,
+ self::AUTHOR_ID_TO
+ );
}
/** @test */
@@ -66,7 +84,11 @@ final class FollowHandlerTest extends TestCase
)
);
- $this->runHandler(self::AUTHOR_ID_FROM, self::AUTHOR_ID_TO);
+ $this->runHandler(
+ self::FOLLOW_ID,
+ self::AUTHOR_ID_FROM,
+ self::AUTHOR_ID_TO
+ );
}
/** @test */
@@ -74,7 +96,6 @@ final class FollowHandlerTest extends TestCase
{
$fromAuthorId = self::AUTHOR_ID_FROM;
$toAuthorId = self::AUTHOR_ID_TO;
- $followId = '51d8ffff-123f-78e1-48fc-f8b513391a0e';
$fromAuthor = Author::signUp(
AuthorId::fromString($fromAuthorId),
@@ -90,15 +111,20 @@ final class FollowHandlerTest extends TestCase
$this->authors->add($fromAuthor);
$this->authors->add($toAuthor);
+
$this->follows->add(
FollowRelation::fromAuthorToAuthor(
- FollowId::fromString($followId),
+ FollowId::fromString(self::FOLLOW_ID),
$fromAuthor->authorId(),
$toAuthor->authorId(),
)
);
- $this->runHandler($fromAuthorId, $toAuthorId);
+ $this->runHandler(
+ self::FOLLOW_ID,
+ self::AUTHOR_ID_FROM,
+ self::AUTHOR_ID_TO
+ );
$this->assertCount(
1,
@@ -130,7 +156,11 @@ final class FollowHandlerTest extends TestCase
$this->authors->add($fromAuthor);
$this->authors->add($toAuthor);
- $command = $this->runHandler($fromAuthorId, $toAuthorId);
+ $command = $this->runHandler(
+ self::FOLLOW_ID,
+ $fromAuthorId,
+ $toAuthorId
+ );
$this->assertCount(
1,
@@ -155,14 +185,19 @@ final class FollowHandlerTest extends TestCase
);
}
- private function runHandler(string $fromAuthorId, string $toAuthorId): Follow
+ private function runHandler(
+ string $followId,
+ string $fromAuthorId,
+ string $toAuthorId
+ ): Follow
{
$this->eventBus->reset();
- $command = (Follow::fromAuthorIdToAuthorId(
- $fromAuthorId,
- $toAuthorId
- ))->stampAsNewMessage();
+ $command = Follow::fromArray([
+ 'follow_id' => $followId,
+ 'from_author_id' => $fromAuthorId,
+ 'to_author_id' => $toAuthorId
+ ]);
(new FollowHandler(
$this->authors,