theUniC ea4e82628a README.md ❤️
2022-04-09 21:56:56 +02:00
2021-12-08 17:56:27 +01:00
2021-12-26 19:30:32 +01:00
2022-01-31 08:06:00 +01:00
2022-03-26 19:00:52 +01:00
2022-04-09 09:04:50 +02:00
2022-02-02 16:06:28 +01:00
2022-04-03 21:22:06 +02:00
2021-11-05 19:56:41 +01:00
README.md ❤️
2022-04-09 21:56:56 +02:00
2020-06-02 12:13:08 +02:00
2021-09-14 19:42:59 +02:00
2021-09-14 19:42:59 +02:00
2022-03-18 00:12:49 +01:00
2022-02-23 23:04:17 +01:00
2021-12-26 19:30:32 +01:00
README.md ❤️
2022-04-09 21:56:56 +02:00

cheeper logo
Hexagonal Architecture + DDD + CQRS by Example Book code repository

CI status PHP Version Symfony Version Built with love

Cheeper is a Twitter clone used in the book CQRS By Example as a reference implementation. Here you will find an example of applying Hexagonal Architecture, DDD (tactical patterns) and CQRS (Command query responsibility segregation) in a real use case PHP application.

Take a look around, play a bit with the code, and if you like what you see ... Give us a star 💫

📕 Check out the book · Report an issue in the book · Report an issue in the code

Table of contents

About the Book

Command-Query Responsibility Segregation is an architectural style for developing applications that split the Domain Model in Read and Write operations in order to maximize semantics, performance, and scalability. What are all the benefits of CQRS? What are the drawbacks? In which cases does it worth applying it? How does it relate to Hexagonal Architecture? How do we properly implement the Write Model and Read Models? How do we keep in sync both sides? What are the following steps to move towards Event Sourcing? This book will answer all these questions and many more, guided through lots of practical examples. Brought to you by the same authors behind "Domain-Driven Design in PHP".

Go checkout the book

Code Structure

TBC

Running the application

Requirements

  • Docker 👉 To run all services in your computer without installing all the dependencies.
  • Docker Compose 👉 To orchestrate the services. If you're on Mac or Windows and you have installed Docker Desktop you already have Docker Compose installed. If you're on Linux, you can check out installation here.

Getting started

The first thing you should do is to have a fresh copy of the code. To do so just clone the repository

git clone https://github.com/cqrs-by-example/ddd-cqrs-example

Then, in order to get environment set up properly, you should have a .env.local file with all the correct values 👇

cp .env.docker.dist .env.local

And you're ready to go! 🤘

Set all the infrastructure up

As the code runs fully on docker, several make targets have been carefully prepared in order to make the interaction with the code as easy and smooth as possible. To start all the services just run 👇

make start

This make target, runs docker compose, and ensures all services are up and running. Next, once all the services have been started, we must make sure the database and queues start empty and database migrations are executed successfully. To do so, just run

make infrastructure

Finally to stop all services just run

make stop

And now you're ready to execute all the demo steps! 🚀

Demo time!

Cheeper starts totally as a blank application. There is no Author, Cheeps, Follows, etc. Let's see what happens with the existing Projections when

Follower Counters Query

http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/followers-counter
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/followers-counter
http --json --check-status --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/followers-counter

Expected output

{
    "_meta": [],
    "data": "Author \"a64a52cc-3ee9-4a15-918b-099e18b43119\" does not exist"
}

Author Timeline

http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/timeline

Expected output

{
    "_meta": [],
    "data": "Author \"1da1366f-b066-4514-9b29-7346df41e371\" does not exist"
}
...

Adding Authors

Cheeper is configured so signing up new Authors is a synchronous operations. This mean that once the HTTP request made to the Cheeper API is finished, the Author will be in the database without having to perform any other action. As soon the API receives the request, the SignUpCommand is created and delegated to the Command Bus. The Command Bus will check the routing configurations and it will pass the Command to the SignUpCommandHandler that will perform all the actions required to sign up a new Author.

http --json --body POST http://127.0.0.1:8000/chapter7/author author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" username="bob" email="bob@bob.com"
http --json --body POST http://127.0.0.1:8000/chapter7/author author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" username="alice" email="alice@alice.com"
http --json --body POST http://127.0.0.1:8000/chapter7/author author_id="1da1366f-b066-4514-9b29-7346df41e371" username="charlie" email="charlie@charlie.com"

Expected output

{
    "author_id": "a64a52cc-3ee9-4a15-918b-099e18b43119",
    "message_id": "d3c90181-c2c1-4e5c-90e9-99a799197b88"
}
...

The response to the HTTP request and the Authors in the database are not the only outcome generated. In RabbitMQ, there are store multiple events of type NewAuthorSigned. Those Domain Events are waiting to be handled. In order to do so, they will have to be consumed. Until not getting consumed, the Follower Counters Query will still return a 404 HTTP Status Code.

Consuming Events

php bin/console messenger:consume events_async

Expected output

08:59:13 INFO      [messenger] Received message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO      [messenger] Message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned handled by Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned","handler" => "Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle"]
08:59:13 INFO      [messenger] Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned was handled successfully (acknowledging to transport). ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO      [messenger] Received message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO      [messenger] Message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned handled by Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned","handler" => "Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle"]
08:59:13 INFO      [messenger] Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned was handled successfully (acknowledging to transport). ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO      [messenger] Received message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO      [messenger] Message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned handled by Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned","handler" => "Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle"]
08:59:13 INFO      [messenger] Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned was handled successfully (acknowledging to transport). ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { …},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]

There is one NewAuthorSigned event to handle for each new Author signed in. That's a total of three. In Cheeper, the NewAuthorSigned is handled by the SymfonyNewAuthorSignedEventHandler class. Chapter 6 showed that SymfonyNewAuthorSignedEventHandler is

Now, that all the Domain Events are processed, and their corresponding Projections are calculated, the queries to check how many followers - an Author has - are not empty anymore.

Follower Counters Query

http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/followers-counter
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/followers-counter
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/followers-counter

Expected output

{
    "_meta": {
        "message_id": "8db6fc1d-41d5-4f6b-8950-e5766eee53e9"
    },
    "data": {
        "authorId": "1da1366f-b066-4514-9b29-7346df41e371",
        "authorUsername": "charlie",
        "numberOfFollowers": 0
    }
}
...

Author Timeline

http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/timeline

Expected output

{
    "cheeps": []
}

Following other Authors

Following an Author is a Command that is defined as asynchronous. This means that once the request to the Cheeper API is done, the Command will wait to be processed by a worker. The initial HTTP request to the API returns really fast, but the changes in the database will not be done until the Command is processed.

http --json --body POST http://127.0.0.1:8000/chapter7/follow follow_id="8cc71bf2-f827-4c92-95a5-43bb1bc622ad" from_author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" to_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119"
http --json --body POST http://127.0.0.1:8000/chapter7/follow follow_id="f3088920-841e-4577-a3c2-efdc80f0dea5" from_author_id="1da1366f-b066-4514-9b29-7346df41e371" to_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119"

Consuming Events

php bin/console messenger:consume events_async

Follower Counters Query

http --json http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/followers-counter
http --json http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/followers-counter
http --json http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/followers-counter

Expected output

{
    "_meta": {
        "message_id": "0e9df870-894f-4d37-a140-4d598941fa4f"
    },
    "data": {
        "authorId": "a64a52cc-3ee9-4a15-918b-099e18b43119",
        "authorUsername": "bob",
        "numberOfFollowers": 1
    }
}

Posting Cheeps

http --json --body POST http://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 --body POST http://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 --body POST http://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"

Author Timeline

http --json http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/timeline
http --json http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/timeline
http --json http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/timeline
Languages
PHP 97.8%
Makefile 1.3%
Twig 0.7%
Dockerfile 0.2%