# PHP Todo List A small todo-list application: a REST API written in PHP (Slim 4) backed by an SQLite file, plus a Vue 3 + TypeScript PWA frontend in [web/](web/). ## Status | Stage | Scope | State | |-------|-------|-------| | 1 | Auth API — register, login, `GET /me` | ✅ done | | 2 | Frontend shell — Vite PWA, auth-gated routing, register/login pages | ✅ done | | 3 | Todo list + item CRUD API | ✅ done | | 4 | Todo UI in the frontend | planned | Registration signs the user in immediately, with the account's email marked unverified (`user.email_verified` is `false` until a future stage adds a verification endpoint). ## Run with Docker The only requirement is Docker with the Compose plugin. ```bash docker compose up -d ``` This builds a PHP 8.3 + Apache image, applies migrations, and serves the API at (e.g. `curl http://localhost:8080/api/health`). - The project directory is bind-mounted into the container, so editing PHP source takes effect without a rebuild (within ~2s, due to the opcache revalidation interval). `vendor/` is used from the host — run `composer` once first if it is missing (see "Run without Docker" below, or `docker compose run --rm --entrypoint composer app install`). - The SQLite database and the generated JWT signing key live in the `storage` named volume, mounted at `/var/www/storage` (outside the bind-mounted source), so they survive `docker compose restart` / `down` + `up`. - Rebuild only after changing the `Dockerfile`: `docker compose up -d --build`. - `docker compose down -v` removes the volume and gives you a clean database. - Override settings via the environment or a `.env` file in this directory (Compose substitutes `APP_DEBUG`, `JWT_SECRET`, `JWT_TTL` — see [docker-compose.yml](docker-compose.yml)). ## Run without Docker Requires PHP 8.1+ with the `pdo_sqlite` and `mbstring` extensions, plus [Composer](https://getcomposer.org/). On Fedora: ```bash sudo dnf install php-cli php-pdo php-mbstring composer ``` ### Setup ```bash composer install cp .env.example .env # optional; sane defaults are used without it composer migrate # creates storage/database.sqlite and its tables ``` ### Running ```bash composer serve # http://localhost:8080 (php -S localhost:8080 -t public) ``` Any web server can serve the app as long as the document root is `public/` and unknown paths fall through to `public/index.php`. ## Frontend The Vue/TypeScript PWA lives in [web/](web/) and talks to this API. With the API running (`docker compose up -d`): ```bash cd web npm install npm run dev # http://localhost:5173, proxies /api to localhost:8080 ``` Or run it inside Compose alongside the API: ```bash docker compose --profile frontend up -d ``` Unauthenticated visitors are redirected to `/login`; `/register` creates an account and signs in immediately. See [web/README.md](web/README.md). ## Configuration All settings are optional environment variables (read from `.env` or the real environment). See [.env.example](.env.example). | Variable | Default | Purpose | |----------|---------|---------| | `APP_DEBUG` | `false` | Include exception details in error responses | | `DATABASE_PATH` | `storage/database.sqlite` | SQLite file location | | `JWT_SECRET` | auto-generated into `storage/secret.key` | Token signing key | | `JWT_TTL` | `86400` | Token lifetime in seconds | ## API Base path: `/api`. All request and response bodies are JSON; send `Content-Type: application/json`. ### `GET /api/health` ```json { "status": "ok" } ``` ### `POST /api/auth/register` Request: ```json { "email": "ada@example.com", "password": "correct horse battery staple" } ``` `201 Created`: ```json { "user": { "id": 1, "email": "ada@example.com", "email_verified": false, "email_verified_at": null, "created_at": "2026-09-03T12:00:00Z" }, "token": "", "expires_at": "2026-09-04T12:00:00+00:00" } ``` New accounts are created with an unverified email (`email_verified: false`). Errors: `422` invalid input, `409` email already registered. Validation: `email` must be a valid address (≤ 255 chars); `password` must be 8–72 characters. ### `POST /api/auth/login` Request: ```json { "email": "ada@example.com", "password": "correct horse battery staple" } ``` `200 OK`: same shape as register. `401` on bad credentials (the message does not say whether it was the email or the password that was wrong). ### `GET /api/me` Requires `Authorization: Bearer `. `200 OK`: ```json { "user": { "id": 1, "email": "ada@example.com", "email_verified": false, "email_verified_at": null, "created_at": "2026-09-03T12:00:00Z" } } ``` `401` if the header is missing, malformed, or the token is invalid/expired. ### Todo lists All routes below require `Authorization: Bearer `. A list belongs to one owner (the creator); another user's list — or a missing one — always responds `404`. | Method | Path | Purpose | |--------|------|---------| | `GET` | `/api/lists` | the caller's lists, newest first | | `POST` | `/api/lists` | create a list | | `GET` | `/api/lists/{id}` | one list | | `PATCH` | `/api/lists/{id}` | update `title` and/or `description` | | `DELETE` | `/api/lists/{id}` | delete the list and its items (`204`) | Create/update body: `title` (required on create, 1–255 chars), `description` (optional, ≤ 2000 chars, defaults to `""`). `PATCH` needs at least one field. List representation: ```json { "list": { "id": 1, "title": "Shopping", "description": "For the week", "owner_id": 1, "item_count": 3, "completed_count": 1, "created_at": "2026-09-03T12:00:00Z", "updated_at": "2026-09-03T12:00:00Z" } } ``` `GET /api/lists` returns `{ "lists": [ … ] }`. ### Todo items Scoped to a list; the parent list's ownership is checked first (`404` otherwise). | Method | Path | Purpose | |--------|------|---------| | `GET` | `/api/lists/{id}/items` | items, ordered by `position` then `id` | | `POST` | `/api/lists/{id}/items` | add an item | | `GET` | `/api/lists/{id}/items/{itemId}` | one item | | `PATCH` | `/api/lists/{id}/items/{itemId}` | update `text`, `complete`, and/or `position` | | `DELETE` | `/api/lists/{id}/items/{itemId}` | delete the item (`204`) | Create body: `text` (required, 1–1000 chars), `complete` (optional bool, default `false`), `position` (optional integer ≥ 0; when omitted the item is appended after the current highest position). `PATCH` needs at least one field. `position` is a plain sort key the client manages — updating one item never renumbers its siblings. Item representation: ```json { "item": { "id": 10, "list_id": 1, "text": "Milk", "complete": false, "position": 0, "created_at": "2026-09-03T12:00:00Z", "updated_at": "2026-09-03T12:00:00Z" } } ``` `GET …/items` returns `{ "items": [ … ] }`. ### Error shape Every error response looks like: ```json { "error": { "message": "The submitted data was invalid.", "details": { "email": ["Email must be a valid address."] } } } ``` `details` is present only when relevant (e.g. validation). ## Try it ```bash BASE=http://localhost:8080 curl -s -X POST $BASE/api/auth/register \ -H 'Content-Type: application/json' \ -d '{"email":"ada@example.com","password":"password123"}' TOKEN=$(curl -s -X POST $BASE/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"ada@example.com","password":"password123"}' | grep -o '"token":"[^"]*"' | cut -d'"' -f4) curl -s $BASE/api/me -H "Authorization: Bearer $TOKEN" LIST=$(curl -s -X POST $BASE/api/lists \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"title":"Shopping","description":"For the week"}' \ | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) curl -s -X POST $BASE/api/lists/$LIST/items \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"text":"Milk"}' curl -s $BASE/api/lists/$LIST/items -H "Authorization: Bearer $TOKEN" ``` ## Tests ```bash composer install # installs phpunit (require-dev) vendor/bin/phpunit ``` ## Layout ``` public/index.php Front controller src/bootstrap.php App wiring and route definitions src/Support/Config.php Environment-driven configuration src/Support/Database.php PDO/SQLite connection src/Auth/JwtService.php Issue/verify JWTs src/Auth/AuthMiddleware.php Bearer-token authentication src/Http/JsonErrorHandler.php Uniform JSON error envelope src/Http/Controllers/ Request handlers (Auth, TodoList, TodoItem) src/Repository/ Database access (User, TodoList, TodoItem) src/Support/Validator.php Request-body validation helper migrations/*.sql Schema, applied by bin/migrate.php Dockerfile PHP 8.3 + Apache image docker-compose.yml One-command local stack (API; web via --profile frontend) docker/ Apache vhost + container entrypoint web/ Vue 3 + TypeScript + Vite PWA frontend ``` ## Provenance This project was generated with [Claude Code](https://claude.com/claude-code).