This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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. 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.
All commands are wrapped in Taskfile.yml (run via Task). Most are just
docker compose exec phpfpm … underneath — useful to know when task --dry <name> to see the actual command.
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 ElasticsearchThe 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).
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 / patternTests 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.
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 6CI (GitHub Actions pr.yaml) runs all of these — run them locally before opening a PR.
public/spec.yaml is the committed OpenAPI export and is checked in CI. Regenerate after changing any API resource:
task api:spec:exportAPI Platform resources are DTOs, not Doctrine entities. Each resource follows the same pattern — Event is the
canonical example:
src/Api/Dto/<Resource>.php—#[ApiResource]class declaring operations, pagination, and#[ApiFilter]attributes. The$idproperty is identifier-only; real data is filled in by the provider.src/Api/State/<Resource>RepresentationProvider.php— implementsProviderInterface. Receives the operation + context, asksAbstractProvider::getFilters()to compile the declared#[ApiFilter]attributes into Elasticsearch query fragments, then callsIndexInterface::search()and returns aSearchResults(collection) or array (single item).src/Service/ElasticSearch/ElasticSearchIndex.php— the onlyIndexInterfaceimplementation. Talks to the Elasticsearch cluster (INDEX_URLenv var). Paginated results come back viaElasticSearchPaginator.
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.
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).
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.
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 bytask 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/<index>.json.
- Don't add Doctrine entities or migrations — domain data is owned by
event-database-imports. New resources mean new DTOs + providers, not entities. paginationMaximumItemsPerPageon a resource caps client-requested page size.AbstractProvider::MAX_PAGE_SIZE_FALLBACK = 20is the safety net when the resource doesn't declare one.failOnDeprecation,failOnNotice,failOnWarningare alltrueinphpunit.dist.xml— a deprecation warning will fail the suite.- PHPStan errors about
App\Api\Dto\*::$idbeing unused are intentionally ignored (API Platform writes the property reflectively).