A modern, dockerized task management application built with Symfony, PHP, and PostgreSQL.
The project demonstrates clean architecture (DDD), CQRS, pragmatic Event Sourcing, and design pattern usage.
| Layer | Responsibility | Location |
|---|---|---|
| Domain | Core business logic, models, events, repository interfaces | src/Domain/ |
| Application | Use cases, CQRS commands/handlers, sync strategies | src/Application/ |
| Infrastructure | Persistence (Doctrine), GraphQL resolvers, external APIs, security | src/Infrastructure/ |
- PHP 8.4 / Symfony 7
- PostgreSQL 16
- GraphQL — OverblogGraphQLBundle
- Doctrine ORM
- Symfony Messenger — Command Bus (CQRS)
- LexikJWTAuthenticationBundle — JWT Authentication
- PHPUnit — Unit Tests
- Docker & Docker Compose
| Pattern | Usage |
|---|---|
| Pragmatic Event Sourcing | Domain events (TaskCreatedEvent, TaskStatusUpdatedEvent) are recorded in an append-only event store via Doctrine Listener + Symfony Messenger |
| Factory Pattern | TaskFactory validates and creates Task aggregates |
| Strategy Pattern | UserSyncStrategyInterface abstracts user synchronization sources (e.g., JSONPlaceholder) |
| CQRS | Commands dispatched via Messenger; queries handled by dedicated resolvers |
- Docker & Docker Compose
docker compose up -d --builddocker compose exec app php bin/console lexik:jwt:generate-keypairThis creates RSA keys in config/jwt/ used for authentication token signing. Keys are .gitignored — each environment generates its own.
docker compose exec app php bin/console doctrine:migrations:migrate -ndocker compose exec app php bin/console app:sync-usersThis command fetches users from JSONPlaceholder, saves them to the database, assigns ROLE_ADMIN to user with external ID 1 (and ROLE_USER to others), and sets a default password (password123) for authentication.
To verify synchronization:
docker compose exec database psql -U app -d app -c "SELECT external_id, name, email FROM users;"Open http://localhost:8000/graphiql in your browser.
The GraphQL API is secured with JWT. You must be authenticated to execute queries and mutations.
User with external ID 1 (Sincere@april.biz) has Admin privileges.
curl -X POST http://localhost:8000/api/login_check \
-H "Content-Type: application/json" \
-d '{"username": "Sincere@april.biz", "password": "password123"}'Since the embedded GraphiQL interface doesn't natively support HTTP Header injection, a developer-friendly workaround is implemented.
- Copy the token returned from the login endpoint.
- Set it in your
.envfile:APP_JWT_TOKEN=your_generated_jwt_token_here
- Rebuild:
docker compose up -d --build
The DevJwtAutoInjectSubscriber will automatically inject this token into GraphiQL requests in the dev environment.
Access the playground at http://localhost:8000/graphiql.
Returns all tasks in the system (admin sees all, regular users see only their assigned tasks).
query GetAllTasks {
tasks {
id
name
description
status
assignedUserId
}
}query GetTasksByUser {
tasks(assignedUserId: 1) {
id
name
status
user {
name
email
}
}
}| Parameter | Type | Description |
|---|---|---|
assignedUserId |
Int |
Filter tasks by assigned user's external ID |
query Me {
me {
externalId
name
email
}
}Retrieve the complete audit trail of changes for a specific task.
query TaskHistory {
taskHistory(taskId: "019cae53-cd16-7476-a116-118159a43ef8") {
eventType
payload
createdAt
}
}| Parameter | Type | Description |
|---|---|---|
taskId |
String! (UUID) |
The unique identifier of the task |
Example Response
{
"data": {
"taskHistory": [
{
"eventType": "TaskCreatedEvent",
"payload": {
"name": "Fix bug #42",
"description": "NullPointerException",
"assignedUserId": 2
},
"createdAt": "2025-03-02T10:30:00+00:00"
},
{
"eventType": "TaskStatusUpdatedEvent",
"payload": {
"oldStatus": "TODO",
"newStatus": "IN_PROGRESS"
},
"createdAt": "2025-03-02T10:35:00+00:00"
}
]
}
}mutation CreateTask {
createTask(
name: "Implement logging system",
description: "Add comprehensive logging with different log levels",
assignedUserId: 1
)
}Returns the UUID of the newly created task.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
String! |
✅ | Task name |
description |
String |
❌ | Detailed description |
assignedUserId |
Int |
❌ | External ID of user to assign |
mutation ChangeTaskStatus {
changeTaskStatus(
taskId: "019cae53-cd16-7476-a116-118159a43ef8",
status: "IN_PROGRESS"
)
}| Parameter | Type | Description |
|---|---|---|
taskId |
String! (UUID) |
Task identifier |
status |
String! |
TODO, IN_PROGRESS, or DONE |
# Step 1: Create a task
mutation { createTask(name: "API Docs", description: "Write docs", assignedUserId: 1) }
# Step 2: List tasks to get the UUID
query { tasks(assignedUserId: 1) { id name status } }
# Step 3: Change status
mutation { changeTaskStatus(taskId: "<uuid>", status: "IN_PROGRESS") }
# Step 4: View history
query { taskHistory(taskId: "<uuid>") { eventType payload createdAt } }Every operation on a task generates domain events that are recorded in an append-only event store:
| Event | Trigger | Payload |
|---|---|---|
TaskCreatedEvent |
New task created | name, description, assignedUserId |
TaskStatusUpdatedEvent |
Status changed | oldStatus, newStatus |
Events are collected within the domain model, dispatched via a Doctrine Listener (DomainEventDispatcherSubscriber), and persisted by a Messenger handler (DomainEventLogHandler).
This is a pragmatic event log approach rather than full Event Sourcing with replay. The current state lives in the tasks table, while events provide a complete audit trail. This design is intentional for the scope of this project — the architecture is prepared for extension to full ES (event replay, projections, snapshots) without API changes.
docker compose exec app php bin/phpunitTests cover:
TaskFactory— creation, validation, edge cases (empty name)JsonPlaceholderSyncStrategy— HTTP client mocking, data mappingSyncUsersCommandHandler— idempotent sync, stub/mock verification
src/
├── Domain/ # Core business logic
│ ├── Event/ # Domain events (TaskCreatedEvent, ...)
│ ├── Exception/ # Domain exceptions
│ ├── Factory/ # TaskFactory (Factory Pattern)
│ ├── Model/ # Aggregates: Task, User, TaskStatus
│ └── Repository/ # Repository interfaces
├── Application/ # Use cases
│ ├── Command/ # CQRS Commands & Handlers
│ └── Sync/ # UserSyncStrategyInterface (Strategy Pattern)
└── Infrastructure/ # Framework integration
├── Api/ # JSONPlaceholder client
├── Command/ # Console commands
├── EventSubscriber/ # Doctrine event listener, JWT subscriber
├── GraphQL/ # Resolvers, Types
├── Message/ # Messenger handlers (event log)
└── Persistence/ # Doctrine repositories, entities
