From 3a35e2292bedb07dcdf75c9eaf7479186eb89aff Mon Sep 17 00:00:00 2001 From: turegjorup Date: Fri, 22 May 2026 10:53:40 +0200 Subject: [PATCH 1/3] chore: add Claude Code setup adapted to this repo Adds CLAUDE.md, project-level .claude/ config, two agents (pr-readiness, reload-fixtures) and an update-api-spec skill, all targeted at this repo's Taskfile / Elasticsearch / API Platform setup. settings.local.json is gitignored. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/agents/pr-readiness.md | 28 ++++ .claude/agents/reload-fixtures.md | 25 ++++ .claude/settings.json | 169 ++++++++++++++++++++++++ .claude/skills/update-api-spec/SKILL.md | 18 +++ .gitignore | 4 + CLAUDE.md | 90 +++++++++++++ 6 files changed, 334 insertions(+) create mode 100644 .claude/agents/pr-readiness.md create mode 100644 .claude/agents/reload-fixtures.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/update-api-spec/SKILL.md create mode 100644 CLAUDE.md diff --git a/.claude/agents/pr-readiness.md b/.claude/agents/pr-readiness.md new file mode 100644 index 0000000..52b9ee3 --- /dev/null +++ b/.claude/agents/pr-readiness.md @@ -0,0 +1,28 @@ +--- +name: pr-readiness +description: Run all CI-equivalent checks locally before creating a PR +model: haiku +--- + +Mirror what `.github/workflows/*.yaml` runs on a PR. Every step executes inside Docker (the project +ships only its `phpfpm` / `prettier` / `markdownlint` services — there is no `node` service and no +JS/CSS to lint). Stop early if a critical check fails. + +## Checks + +1. **Composer validate**: `docker compose exec -T phpfpm composer validate --strict` +2. **Composer normalize (dry-run)**: `docker compose exec -T phpfpm composer normalize --dry-run` +3. **PHP coding standards**: `task coding-standards:php:check` +4. **Twig coding standards**: `task coding-standards:twig:check` +5. **YAML coding standards**: `task coding-standards:yaml:check` +6. **Markdown coding standards**: `task coding-standards:markdown:check` +7. **PHPStan (level 6)**: `task code-analysis:phpstan` +8. **Test fixtures + API tests**: `task fixtures:load:test --yes && task api:test`. Tests hit a real Elasticsearch, so fixtures must be loaded first. If the load fails with "No alive nodes", run `docker compose up --detach --wait` and retry — see the `reload-fixtures` agent for the full recovery dance. +9. **API spec up to date** (mirrors `.github/workflows/api-spec.yml`): + - `task api:spec:export` + - `git diff --exit-code public/spec.yaml` — must be clean. +10. **CHANGELOG updated**: `git diff develop -- CHANGELOG.md` should show at least one entry under `## [Unreleased]`. + +## Output + +Report a summary table with columns: Check Name, Status (pass/fail), and error output for failures. diff --git a/.claude/agents/reload-fixtures.md b/.claude/agents/reload-fixtures.md new file mode 100644 index 0000000..3f516b2 --- /dev/null +++ b/.claude/agents/reload-fixtures.md @@ -0,0 +1,25 @@ +--- +name: reload-fixtures +description: Reload Elasticsearch fixtures and recover from a not-ready cluster +model: haiku +--- + +This project's "data layer" is Elasticsearch — there is no Doctrine database to migrate. After changing +API resources, filters, or anything that affects search behavior you generally want to reload fixtures +so subsequent manual / test runs see a consistent dataset. + +Two flavours of fixtures exist (see `src/Model/IndexName.php` for the seven index names): + +- **Dev fixtures** — pulled from the `event-database-imports` repo on GitHub: `task fixtures:load` +- **Test fixtures** — read from `tests/resources/*.json` and used by the PHPUnit suite: `task fixtures:load:test --yes` + +## Steps + +1. Confirm with the user (or take it from their prompt) whether to load **dev** or **test** fixtures. +2. Make sure the stack is up: `docker compose up --detach --wait`. The `--wait` is important — Elasticsearch is slow to become ready and the fixture loader fails fast with "No alive nodes" otherwise. +3. Run the load command (`task fixtures:load` or `task fixtures:load:test --yes`). The Taskfile prompts for confirmation unless `--yes` is passed. +4. If the command fails with "No alive nodes" or any Elasticsearch connection error: + - Poll the cluster health endpoint until it returns HTTP 200: + `docker compose exec elasticsearch curl 'http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=5s' --verbose` + - Re-run the fixture load command. +5. Report which indices were loaded and any non-fatal warnings (the Taskfile sets `ignore_error: true` because some fixtures emit a benign `Warning: Undefined array key "entityId"`). diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9c33183 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,169 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "env": { + "COMPOSE_USER": "deploy" + }, + "permissions": { + "allow": [ + "Bash(cat:*)", + "Bash(diff:*)", + "Bash(echo:*)", + "Bash(find:*)", + "Bash(gh:*)", + "Bash(git:*)", + "Bash(grep:*)", + "Bash(head:*)", + "Bash(ls:*)", + "Bash(pwd)", + "Bash(tail:*)", + "Bash(task:*)", + "Bash(tree:*)", + "Bash(wc:*)", + "Bash(which:*)", + "Bash(docker compose exec:*)", + "Bash(docker compose run:*)", + "Bash(docker compose up:*)", + "Bash(docker compose ps:*)", + "Bash(docker compose logs:*)", + "Bash(docker compose top:*)", + "Bash(docker compose config:*)", + "Bash(docker compose pull:*)", + "Bash(docker compose images:*)", + "Bash(docker network:*)" + ], + "deny": [ + "Bash(rm -rf:*)", + "Bash(gh issue delete:*)", + "Bash(gh release delete:*)", + "Bash(gh repo delete:*)", + "Bash(gh label delete:*)", + "Read(./.env.local)", + "Read(./.env.local.*)", + "Read(./config/secrets/*)" + ], + "ask": [ + "Bash(docker compose down:*)", + "Bash(docker compose stop:*)", + "Bash(docker compose rm:*)", + "Bash(docker compose restart:*)", + "Bash(gh issue create:*)", + "Bash(gh issue close:*)", + "Bash(gh issue edit:*)", + "Bash(gh issue comment:*)", + "Bash(gh pr create:*)", + "Bash(gh pr close:*)", + "Bash(gh pr merge:*)", + "Bash(gh pr edit:*)", + "Bash(gh pr comment:*)", + "Bash(gh pr review:*)", + "Bash(gh release create:*)", + "Bash(gh release edit:*)", + "Bash(gh repo create:*)", + "Bash(gh label create:*)", + "Bash(gh label edit:*)", + "Bash(git push:*)", + "Bash(git branch -d:*)", + "Bash(git branch -D:*)", + "Bash(git tag -d:*)", + "Bash(git tag -a:*)", + "Bash(git tag :*)", + "Bash(git reset:*)", + "Bash(git rebase:*)", + "Bash(git merge:*)", + "Bash(git stash drop:*)", + "Bash(git clean:*)", + "Bash(git checkout -- :*)", + "Bash(git restore:*)", + "Bash(git commit:*)" + ] + }, + "hooks": { + "SessionStart": [ + { + "matcher": "startup", + "hooks": [ + { + "type": "command", + "command": "docker compose up --detach --quiet-pull 2>/dev/null || true", + "timeout": 60, + "statusMessage": "Starting Docker services..." + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in */composer.lock|*/yarn.lock|*/.env.local|*/.env.local.*) echo 'BLOCKED: Do not edit lock files or .env.local directly' >&2; exit 1 ;; esac" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in *.php) REL_PATH=\"${CLAUDE_FILE_PATH#$CLAUDE_PROJECT_DIR/}\"; docker compose exec -T phpfpm vendor/bin/php-cs-fixer fix --quiet \"$REL_PATH\" 2>/dev/null || true ;; esac", + "timeout": 30 + }, + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in *.php) REL_PATH=\"${CLAUDE_FILE_PATH#$CLAUDE_PROJECT_DIR/}\"; docker compose exec -T phpfpm vendor/bin/phpstan analyse --no-progress --error-format=raw \"$REL_PATH\" 2>/dev/null || true ;; esac", + "timeout": 30 + }, + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in *.twig) REL_PATH=\"${CLAUDE_FILE_PATH#$CLAUDE_PROJECT_DIR/}\"; docker compose exec -T phpfpm vendor/bin/twig-cs-fixer lint --fix \"$REL_PATH\" 2>/dev/null || true ;; esac", + "timeout": 15 + }, + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in */composer.json) docker compose exec -T phpfpm composer normalize --quiet 2>/dev/null || true ;; esac", + "timeout": 30 + }, + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in *.yaml|*.yml) REL_PATH=\"${CLAUDE_FILE_PATH#$CLAUDE_PROJECT_DIR/}\"; docker compose run --rm -T prettier \"$REL_PATH\" --write 2>/dev/null || true ;; esac", + "timeout": 15 + }, + { + "type": "command", + "command": "case \"$CLAUDE_FILE_PATH\" in *.md) REL_PATH=\"${CLAUDE_FILE_PATH#$CLAUDE_PROJECT_DIR/}\"; docker compose run --rm -T markdownlint markdownlint \"$REL_PATH\" --fix 2>/dev/null || true ;; esac", + "timeout": 15 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "docker compose exec -T phpfpm bin/console lint:container 2>/dev/null || true", + "timeout": 30, + "statusMessage": "Validating Symfony DI container..." + } + ] + } + ] + }, + "enabledPlugins": { + "php-lsp@claude-plugins-official": true, + "code-simplifier@claude-plugins-official": true, + "context7@claude-plugins-official": true, + "code-review@claude-plugins-official": true, + "security-guidance@claude-plugins-official": true, + "playwright@claude-plugins-official": true, + "feature-dev@claude-plugins-official": true, + "itkdev-skills@itkdev-marketplace": true + }, + "enabledMcpjsonServers": [ + "context7" + ], + "enableAllProjectMcpServers": true +} diff --git a/.claude/skills/update-api-spec/SKILL.md b/.claude/skills/update-api-spec/SKILL.md new file mode 100644 index 0000000..ccef50a --- /dev/null +++ b/.claude/skills/update-api-spec/SKILL.md @@ -0,0 +1,18 @@ +--- +name: update-api-spec +description: Regenerate and stage the OpenAPI spec after API resource changes +user-invocable: true +--- + +The committed OpenAPI spec lives at `public/spec.yaml` (single YAML file — there is no separate JSON +export). `.github/workflows/api-spec.yml` fails the PR if it drifts from the resources defined under +`src/Api/`, and additionally diffs the spec against the base branch to flag breaking changes. + +After touching anything under `src/Api/Dto/`, `src/Api/State/`, or `src/Api/Filter/`: + +1. Regenerate the spec: `task api:spec:export` + (This is shorthand for `bin/console api:openapi:export --yaml --output=public/spec.yaml --no-interaction`.) +2. Inspect changes: `git diff public/spec.yaml` +3. If the diff is non-empty, stage the file: `git add public/spec.yaml` +4. Summarise what changed — new/removed operations, modified parameters, schema diffs — so the human + reviewer can sanity-check for unintended breaking changes before pushing. diff --git a/.gitignore b/.gitignore index 0f3aac4..e34bf05 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ ###> phpstan/phpstan ### phpstan.neon ###< phpstan/phpstan ### + +###> claude-code ### +/.claude/settings.local.json +###< claude-code ### diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..de3e5e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this project is + +API platform front-end for the Danish event database used by the municipality of Aarhus. This repo serves a **read-only** REST/Hydra API (`/api/v2/...`) backed by **Elasticsearch** — data is *indexed* by a separate project, [`itk-dev/event-database-imports`](https://github.com/itk-dev/event-database-imports). The MariaDB service in `docker-compose.yml` is part of the standard ITK Dev Symfony image but is **not used for domain data** (the `migrations/` and `src/Entity/` directories are empty). + +Stack: PHP 8.3+, Symfony 7.4, API Platform 4.1, Elasticsearch 8.x. Runs entirely in Docker via `itkdev/php8.3-fpm` + nginx. + +## Common commands + +All commands are wrapped in `Taskfile.yml` (run via [Task](https://taskfile.dev)). Most are just `docker compose exec phpfpm …` underneath — useful to know when `task --dry ` to see the actual command. + +### Setup / running + +```shell +docker compose pull +docker compose up --detach --wait # --wait is important: Elasticsearch is slow to become ready +docker compose exec phpfpm composer install +task fixtures:load # loads demo data from event-database-imports into Elasticsearch +``` + +The site is reachable at `http://$(docker compose port nginx 8080)`. Every API call needs an `X-Api-Key` header matching one of the entries in `APP_API_KEYS` (JSON array in `.env.local`). + +### Tests + +```shell +task fixtures:load:test --yes # loads tests/resources/*.json into Elasticsearch — REQUIRED before api:test +task api:test # phpunit +task api:test -- --filter EventsFilter # single test class / pattern +``` + +Tests hit a real Elasticsearch (no mocking) — see `tests/ApiPlatform/AbstractApiTestCase.php`. The hardcoded test API key is `test_api_key`. If a test run dies with "No alive nodes", run `docker compose up --detach --wait` and reload fixtures. + +### Lint / static analysis + +```shell +task coding-standards:check # markdown + php-cs-fixer + twig-cs-fixer + prettier (yaml) +task coding-standards:apply # auto-fix all of the above +task code-analysis # PHPStan, level 6 +``` + +CI (GitHub Actions `pr.yaml`) runs all of these — run them locally before opening a PR. + +### API spec + +`public/spec.yaml` is the committed OpenAPI export and is checked in CI. Regenerate after changing any API resource: + +```shell +task api:spec:export +``` + +## Architecture + +### Request flow + +API Platform resources are **DTOs**, not Doctrine entities. Each resource follows the same pattern — `Event` is the canonical example: + +1. `src/Api/Dto/.php` — `#[ApiResource]` class declaring operations, pagination, and `#[ApiFilter]` attributes. The `$id` property is identifier-only; real data is filled in by the provider. +2. `src/Api/State/RepresentationProvider.php` — implements `ProviderInterface`. Receives the operation + context, asks `AbstractProvider::getFilters()` to compile the declared `#[ApiFilter]` attributes into Elasticsearch query fragments, then calls `IndexInterface::search()` and returns a `SearchResults` (collection) or array (single item). +3. `src/Service/ElasticSearch/ElasticSearchIndex.php` — the only `IndexInterface` implementation. Talks to the Elasticsearch cluster (`INDEX_URL` env var). Paginated results come back via `ElasticSearchPaginator`. + +The seven indices are enumerated in `src/Model/IndexName.php` (`events`, `organizations`, `occurrences`, `daily_occurrences`, `tags`, `vocabularies`, `locations`). Each index has a matching DTO and provider. + +### Filters + +Custom Elasticsearch filters live in `src/Api/Filter/ElasticSearch/` (`MatchFilter`, `IdFilter`, `BooleanFilter`, `DateRangeFilter`, `DateFilter`, `TagFilter`). They implement API Platform's `FilterInterface` but `apply()` returns ES query DSL rather than mutating a Doctrine queryBuilder. `AbstractProvider::getFilters()` separates filter clauses from sort clauses via the `SortFilterInterface` marker. + +When adding a filter to a resource, attach it with `#[ApiFilter(SomeFilter::class, properties: […])]` on the DTO — the provider picks them up automatically through `api_platform.filter_locator` (injected via `config/services.yaml`). + +### Auth + +Stateless. `src/Security/ApiKeyAuthenticator.php` reads `X-Api-Key`; `ApiUserProvider` validates against the JSON-encoded `APP_API_KEYS` env var. `/api/v2/docs` is public; everything else under `/api` requires a valid key (see `config/packages/security.yaml`). There is no role model — any valid key gets full read access. + +### What lives where + +- `src/Api/Dto/` + `src/Api/State/` + `src/Api/Filter/ElasticSearch/` — the API layer (see above). +- `src/Service/ElasticSearch/` — the only data access. If you find yourself adding a new persistence concern, this is where it goes. +- `src/Command/FixturesLoadCommand.php` — loads JSON dumps into Elasticsearch (used by `task fixtures:load*`). +- `src/Model/` — value objects / enums (`IndexName`, `FilterType`, `DateLimit`, `SearchResults`, `DateFilterConfig`). +- `config/reference.php` — auto-generated, excluded from PHP-CS-Fixer (see `.php-cs-fixer.dist.php`). +- `tests/resources/*.json` — fixtures used by the test suite; loaded via `--url=file:///app/tests/resources/.json`. + +## Conventions worth knowing + +- Don't add Doctrine entities or migrations — domain data is owned by `event-database-imports`. New resources mean new DTOs + providers, not entities. +- `paginationMaximumItemsPerPage` on a resource caps client-requested page size. `AbstractProvider::MAX_PAGE_SIZE_FALLBACK = 20` is the safety net when the resource doesn't declare one. +- `failOnDeprecation`, `failOnNotice`, `failOnWarning` are all `true` in `phpunit.dist.xml` — a deprecation warning will fail the suite. +- PHPStan errors about `App\Api\Dto\*::$id` being unused are intentionally ignored (API Platform writes the property reflectively). From fb0a4390941c106348d2f41634fb675c20437089 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Fri, 22 May 2026 10:57:16 +0200 Subject: [PATCH 2/3] style: wrap CLAUDE.md prose at 120 cols for markdownlint Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 67 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index de3e5e3..4408c38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this project is -API platform front-end for the Danish event database used by the municipality of Aarhus. This repo serves a **read-only** REST/Hydra API (`/api/v2/...`) backed by **Elasticsearch** — data is *indexed* by a separate project, [`itk-dev/event-database-imports`](https://github.com/itk-dev/event-database-imports). The MariaDB service in `docker-compose.yml` is part of the standard ITK Dev Symfony image but is **not used for domain data** (the `migrations/` and `src/Entity/` directories are empty). +API platform front-end for the Danish event database used by the municipality of Aarhus. This repo serves a +**read-only** REST/Hydra API (`/api/v2/...`) backed by **Elasticsearch** — data is *indexed* by a separate project, +[`itk-dev/event-database-imports`](https://github.com/itk-dev/event-database-imports). The MariaDB service in +`docker-compose.yml` is part of the standard ITK Dev Symfony image but is **not used for domain data** (the +`migrations/` and `src/Entity/` directories are empty). -Stack: PHP 8.3+, Symfony 7.4, API Platform 4.1, Elasticsearch 8.x. Runs entirely in Docker via `itkdev/php8.3-fpm` + nginx. +Stack: PHP 8.3+, Symfony 7.4, API Platform 4.1, Elasticsearch 8.x. Runs entirely in Docker via +`itkdev/php8.3-fpm` + nginx. ## Common commands -All commands are wrapped in `Taskfile.yml` (run via [Task](https://taskfile.dev)). Most are just `docker compose exec phpfpm …` underneath — useful to know when `task --dry ` to see the actual command. +All commands are wrapped in `Taskfile.yml` (run via [Task](https://taskfile.dev)). Most are just +`docker compose exec phpfpm …` underneath — useful to know when `task --dry ` to see the actual command. ### Setup / running @@ -21,7 +27,8 @@ docker compose exec phpfpm composer install task fixtures:load # loads demo data from event-database-imports into Elasticsearch ``` -The site is reachable at `http://$(docker compose port nginx 8080)`. Every API call needs an `X-Api-Key` header matching one of the entries in `APP_API_KEYS` (JSON array in `.env.local`). +The site is reachable at `http://$(docker compose port nginx 8080)`. Every API call needs an `X-Api-Key` header +matching one of the entries in `APP_API_KEYS` (JSON array in `.env.local`). ### Tests @@ -31,7 +38,9 @@ task api:test # phpunit task api:test -- --filter EventsFilter # single test class / pattern ``` -Tests hit a real Elasticsearch (no mocking) — see `tests/ApiPlatform/AbstractApiTestCase.php`. The hardcoded test API key is `test_api_key`. If a test run dies with "No alive nodes", run `docker compose up --detach --wait` and reload fixtures. +Tests hit a real Elasticsearch (no mocking) — see `tests/ApiPlatform/AbstractApiTestCase.php`. The hardcoded test +API key is `test_api_key`. If a test run dies with "No alive nodes", run `docker compose up --detach --wait` and +reload fixtures. ### Lint / static analysis @@ -55,36 +64,56 @@ task api:spec:export ### Request flow -API Platform resources are **DTOs**, not Doctrine entities. Each resource follows the same pattern — `Event` is the canonical example: +API Platform resources are **DTOs**, not Doctrine entities. Each resource follows the same pattern — `Event` is the +canonical example: -1. `src/Api/Dto/.php` — `#[ApiResource]` class declaring operations, pagination, and `#[ApiFilter]` attributes. The `$id` property is identifier-only; real data is filled in by the provider. -2. `src/Api/State/RepresentationProvider.php` — implements `ProviderInterface`. Receives the operation + context, asks `AbstractProvider::getFilters()` to compile the declared `#[ApiFilter]` attributes into Elasticsearch query fragments, then calls `IndexInterface::search()` and returns a `SearchResults` (collection) or array (single item). -3. `src/Service/ElasticSearch/ElasticSearchIndex.php` — the only `IndexInterface` implementation. Talks to the Elasticsearch cluster (`INDEX_URL` env var). Paginated results come back via `ElasticSearchPaginator`. +1. `src/Api/Dto/.php` — `#[ApiResource]` class declaring operations, pagination, and `#[ApiFilter]` + attributes. The `$id` property is identifier-only; real data is filled in by the provider. +2. `src/Api/State/RepresentationProvider.php` — implements `ProviderInterface`. Receives the operation + + context, asks `AbstractProvider::getFilters()` to compile the declared `#[ApiFilter]` attributes into + Elasticsearch query fragments, then calls `IndexInterface::search()` and returns a `SearchResults` (collection) or + array (single item). +3. `src/Service/ElasticSearch/ElasticSearchIndex.php` — the only `IndexInterface` implementation. Talks to the + Elasticsearch cluster (`INDEX_URL` env var). Paginated results come back via `ElasticSearchPaginator`. -The seven indices are enumerated in `src/Model/IndexName.php` (`events`, `organizations`, `occurrences`, `daily_occurrences`, `tags`, `vocabularies`, `locations`). Each index has a matching DTO and provider. +The seven indices are enumerated in `src/Model/IndexName.php` (`events`, `organizations`, `occurrences`, +`daily_occurrences`, `tags`, `vocabularies`, `locations`). Each index has a matching DTO and provider. ### Filters -Custom Elasticsearch filters live in `src/Api/Filter/ElasticSearch/` (`MatchFilter`, `IdFilter`, `BooleanFilter`, `DateRangeFilter`, `DateFilter`, `TagFilter`). They implement API Platform's `FilterInterface` but `apply()` returns ES query DSL rather than mutating a Doctrine queryBuilder. `AbstractProvider::getFilters()` separates filter clauses from sort clauses via the `SortFilterInterface` marker. +Custom Elasticsearch filters live in `src/Api/Filter/ElasticSearch/` (`MatchFilter`, `IdFilter`, `BooleanFilter`, +`DateRangeFilter`, `DateFilter`, `TagFilter`). They implement API Platform's `FilterInterface` but `apply()` returns +ES query DSL rather than mutating a Doctrine queryBuilder. `AbstractProvider::getFilters()` separates filter clauses +from sort clauses via the `SortFilterInterface` marker. -When adding a filter to a resource, attach it with `#[ApiFilter(SomeFilter::class, properties: […])]` on the DTO — the provider picks them up automatically through `api_platform.filter_locator` (injected via `config/services.yaml`). +When adding a filter to a resource, attach it with `#[ApiFilter(SomeFilter::class, properties: […])]` on the DTO — +the provider picks them up automatically through `api_platform.filter_locator` (injected via +`config/services.yaml`). ### Auth -Stateless. `src/Security/ApiKeyAuthenticator.php` reads `X-Api-Key`; `ApiUserProvider` validates against the JSON-encoded `APP_API_KEYS` env var. `/api/v2/docs` is public; everything else under `/api` requires a valid key (see `config/packages/security.yaml`). There is no role model — any valid key gets full read access. +Stateless. `src/Security/ApiKeyAuthenticator.php` reads `X-Api-Key`; `ApiUserProvider` validates against the +JSON-encoded `APP_API_KEYS` env var. `/api/v2/docs` is public; everything else under `/api` requires a valid key +(see `config/packages/security.yaml`). There is no role model — any valid key gets full read access. ### What lives where - `src/Api/Dto/` + `src/Api/State/` + `src/Api/Filter/ElasticSearch/` — the API layer (see above). -- `src/Service/ElasticSearch/` — the only data access. If you find yourself adding a new persistence concern, this is where it goes. +- `src/Service/ElasticSearch/` — the only data access. If you find yourself adding a new persistence concern, this + is where it goes. - `src/Command/FixturesLoadCommand.php` — loads JSON dumps into Elasticsearch (used by `task fixtures:load*`). - `src/Model/` — value objects / enums (`IndexName`, `FilterType`, `DateLimit`, `SearchResults`, `DateFilterConfig`). - `config/reference.php` — auto-generated, excluded from PHP-CS-Fixer (see `.php-cs-fixer.dist.php`). -- `tests/resources/*.json` — fixtures used by the test suite; loaded via `--url=file:///app/tests/resources/.json`. +- `tests/resources/*.json` — fixtures used by the test suite; loaded via + `--url=file:///app/tests/resources/.json`. ## Conventions worth knowing -- Don't add Doctrine entities or migrations — domain data is owned by `event-database-imports`. New resources mean new DTOs + providers, not entities. -- `paginationMaximumItemsPerPage` on a resource caps client-requested page size. `AbstractProvider::MAX_PAGE_SIZE_FALLBACK = 20` is the safety net when the resource doesn't declare one. -- `failOnDeprecation`, `failOnNotice`, `failOnWarning` are all `true` in `phpunit.dist.xml` — a deprecation warning will fail the suite. -- PHPStan errors about `App\Api\Dto\*::$id` being unused are intentionally ignored (API Platform writes the property reflectively). +- Don't add Doctrine entities or migrations — domain data is owned by `event-database-imports`. New resources mean + new DTOs + providers, not entities. +- `paginationMaximumItemsPerPage` on a resource caps client-requested page size. + `AbstractProvider::MAX_PAGE_SIZE_FALLBACK = 20` is the safety net when the resource doesn't declare one. +- `failOnDeprecation`, `failOnNotice`, `failOnWarning` are all `true` in `phpunit.dist.xml` — a deprecation warning + will fail the suite. +- PHPStan errors about `App\Api\Dto\*::$id` being unused are intentionally ignored (API Platform writes the property + reflectively). From e7cf4974429bbf903f1954b9bb9891c395aeb62c Mon Sep 17 00:00:00 2001 From: turegjorup Date: Fri, 22 May 2026 10:58:14 +0200 Subject: [PATCH 3/3] docs: add CHANGELOG entry for Claude Code setup Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e3ab8..12f306d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ See [keep a changelog] for information about writing changes to this log. ## [Unreleased] +- [PR-27](https://github.com/itk-dev/event-database-api/pull/27) + Add Claude Code project setup (CLAUDE.md, agents, skills) + ## [1.2.2] - 2026-05-22 - [PR-26](https://github.com/itk-dev/event-database-api/pull/26)