Skip to content

Mi1y/task-manager

Repository files navigation

Task Manager

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.

Hero


Architecture

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/

Technology Stack

  • PHP 8.4 / Symfony 7
  • PostgreSQL 16
  • GraphQLOverblogGraphQLBundle
  • Doctrine ORM
  • Symfony Messenger — Command Bus (CQRS)
  • LexikJWTAuthenticationBundle — JWT Authentication
  • PHPUnit — Unit Tests
  • Docker & Docker Compose

Key Patterns

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

Getting Started

Prerequisites

  • Docker & Docker Compose

1. Build and start containers

docker compose up -d --build

2. Generate JWT keypair

docker compose exec app php bin/console lexik:jwt:generate-keypair

This creates RSA keys in config/jwt/ used for authentication token signing. Keys are .gitignored — each environment generates its own.

3. Run database migrations

docker compose exec app php bin/console doctrine:migrations:migrate -n

4. Synchronize users from external API

docker compose exec app php bin/console app:sync-users

This 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;"

5. Access GraphiQL Playground

Open http://localhost:8000/graphiql in your browser.


Authentication & Security (JWT)

The GraphQL API is secured with JWT. You must be authenticated to execute queries and mutations.

Obtain a JWT Token

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"}'

GraphiQL Auto-Login (Dev Only)

Since the embedded GraphiQL interface doesn't natively support HTTP Header injection, a developer-friendly workaround is implemented.

  1. Copy the token returned from the login endpoint.
  2. Set it in your .env file:
    APP_JWT_TOKEN=your_generated_jwt_token_here
  3. Rebuild:
    docker compose up -d --build

The DevJwtAutoInjectSubscriber will automatically inject this token into GraphiQL requests in the dev environment.


GraphQL API Reference

Access the playground at http://localhost:8000/graphiql.

Queries

Get All Tasks

Returns all tasks in the system (admin sees all, regular users see only their assigned tasks).

query GetAllTasks {
  tasks {
    id
    name
    description
    status
    assignedUserId
  }
}

Get Tasks by Assigned User

query GetTasksByUser {
  tasks(assignedUserId: 1) {
    id
    name
    status
    user {
      name
      email
    }
  }
}
Parameter Type Description
assignedUserId Int Filter tasks by assigned user's external ID

Get Current User

query Me {
  me {
    externalId
    name
    email
  }
}

Get Task History (Event Sourcing)

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"
      }
    ]
  }
}

Mutations

Create Task

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

Change Task Status

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

Complete Workflow

# 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 } }

Event Sourcing Architecture

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).

Architectural Decision

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.


Running Tests

docker compose exec app php bin/phpunit

Tests cover:

  • TaskFactory — creation, validation, edge cases (empty name)
  • JsonPlaceholderSyncStrategy — HTTP client mocking, data mapping
  • SyncUsersCommandHandler — idempotent sync, stub/mock verification

Project Structure

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

About

Modern web app for managing tasks and users. Features GraphQL API, JWT authentication, pragmatic event sourcing, and Dockerized setup. Built with Symfony 7, PHP 8.4, and PostgreSQL.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages