Run ./docker/build.sh
to build project and load fixtures. You might have to run it with sudo
depending on your
system configuration.
Open http://127.0.0.1:8080
to get access to Swagger UI.
Authenticate with one of 2 already existing users
[email protected]
/password
[email protected]
/password
or register your own user running bin/console todocler:users:register-user [uuid] [email] [password]
console command.
Receive authentication token via /auth/token
endpoint. That token MUST be supplied with every request as a header
Authorization: Bearer <token>
- Swagger UI allows to setup that header using modal shown after clicking Authorize
button (you can find it at the top of the page). Just open the modal and paste Bearer <token>
into the input.
This Project is a modular monolith with 2 modules - which in this case aligns with bounded contexts - Productivity
& Users
and additionally
a very thin Shared(Kernel)
. The separation (of logic) between modules is very naive because of the simple nature of application itself.
This architecture ensures that modules could be split into separated (micro-)services at any time without much refactoring needed.
Both modules are event-sourced and layered (DDD-style) while being driven by CQRS. They are loosely coupled, deploying messaging to make existing coupling one-directional (think: direct acyclic graph). Both modules share the same physical event store, but logically they are separated - it is just a pattern that allows for easy debugging, because events coming from different services in the same log (store) are temporally monotonic (ordered). Any other storage, besides aforementioned event store & messaging queue, is private to module using it.
This module is responsible for creating users and indirectly for clients' authentication.
It provides internally:
RegisterUser
commandFindUser
query.IsUserRegistered
query.
Data for those queries is provided by dedicated RegisteredUsers projection. This projection uses Doctrine ORM for persistence.
Thanks to that with little to no effort it was also easy to integrate (through configuration) RegisteredUser
entity with Symfony security and
use it for authentication.
Users
module has no outside dependencies and in order to make it stay that way it publishes integration events - via another projection - to external queue which later can be consumed by downstream clients... meaning other modules.
The underlying queue mechanism - albeit abstracted - uses RabbitMQ which also handles message deduplication (nice to have in at-least-once delivery environment).
Only a single console command todocler:users:register-user for registering new users is exposed as an outside interface of this module. Excluding authentication endpoint handled by Symfony.
I considered rewriting this module in a classic ORM-only way and showcase some safe inter-module messaging techniques (e.g. transactional outbox), but due to time constraints, I didn't.
This module is responsible for TODO projects and theirs tasks.
It provides internally:
CreateProject
commandRenameProject
commandRemoveProject
commandCreateTask
commandCompleteTask
commandRemoveTask
commandBrowseProjects
queryBrowseTasks
query
Data for those queries is provided by dedicated Projects
projection. This projection uses Doctrine ORM for persistence.
Thanks to that it was possible to integrate ApiPlatform with projection entities and expose them as a configurable REST interface.
Productivity
module has a dependency on Users
module:
- One of the features of the application is that
Productivity
module must create first welcoming list (through process manager) for the user as soon as possible after registration. I used dedicated sensor for that - it translates any data into internal events. In this case, it's wrapped around by RabbitMQ consumer and listens for AMQP messages that are published byUsers
module. - Sometimes
Productivity
module has to retrieve details of a user or check whether user with given email exists. I could use above mentioned integration events fromUsers
module and create local projection of registered user, but I chose different method:Productivity
module declares facade with tight set of methods it requires fromUsers
module. In the current implementation it just runs internal queries, but in case of splitting the modules it could be easily swapped with HTTP implementation. This facade might serve as an anti-corruption layer in the future, when domain concepts (of a user) between our two modules start to noticeably diverge.
Except REST API powered by ApiPlatform this module exposes 2 console commands todocler:productivity:create-project and todocler:productivity:create-task as an outside interface.
Employing event sourcing has some drawbacks - mainly eventual consistency. Usually, eventual consistency is not a problem at all, it is just a different way of thinking about data and its availability. Nevertheless, there are ways of dealing with technical dissonance resulting from EC, which I would be happy to discuss.
For event sourcing part of this project, I used Streak - framework supplying all the tools needed to work with event-sourced aggregate roots, sagas/process managers, projections, etc. It helps to alleviate problems of transactions, concurrency control, snapshotting and more.
Everything you need to run this project with is dockerized. Please refer to docker-compose.yaml file and docker/ directory.
Here are the tools I used to achieve the best quality possible.
Unit tests & their coverage are the first and foremost determinants of a quality. Please refer to phpunit.xml.dist file and tests/ directory.
Run phpunit via docker-compose run --rm php xphp bin/phpunit --color=always
Automated refactoring according to the set of configurable rules. Please refer to rector.php file.
Run via docker-compose run --rm --no-deps php bin/rector --ansi
Validates your topmost architecture, looking for dependencies where they should not be. Please refer to depfile.yaml file.
Run via docker-compose run --rm --no-deps php bin/deptrac
Regulates coding standards. Especially useful for teams. Please refer to .php_cs.dist file.
Run via docker-compose run --rm --no-deps php bin/php-cs-fixer fix --diff
Runs all the above in tandem. I used Github Actions. Please refer to .github/workflows/ci.yaml file.
- GraphQL API
- Better REST API design - current solution due to ApiPlatform shortcomings is good enough, but IMHO suboptimal.
- Strong schema for integration events (protobuf, etc)
- BDD tests