# CLAUDE.md: Drupal 11 site

## Commands
- `composer install` · `drush cr` (rebuild caches) · `drush updb -y` · `drush cim -y` / `drush cex -y`
- Code standards: `vendor/bin/phpcs --standard=Drupal,DrupalPractice web/modules/custom`
- Static analysis: `vendor/bin/phpstan analyse web/modules/custom` (with `mglaman/phpstan-drupal`)
- Tests: `vendor/bin/phpunit -c web/core web/modules/custom/<module>`
- Local env is DDEV if `.ddev/` exists: prefix commands with `ddev` (e.g. `ddev drush cr`).

## Code map
- Custom code only in `web/modules/custom/` and `web/themes/custom/`. Never edit `web/core/` or contrib modules; use patches via `cweagans/composer-patches` in `composer.json`.
- Config lives in `config/sync/`; changes made in the UI must be exported with `drush cex` and committed.

## Conventions
- Modules: `<module>.info.yml` with `core_version_requirement: ^11`, services in `<module>.services.yml`, routes in `<module>.routing.yml` with explicit `_permission` or `_custom_access`.
- Use dependency injection: services via constructor injection; controllers and forms implement `create(ContainerInterface $container)`. No `\Drupal::service()` in classes (OK in `.module` hooks only when unavoidable).
- Prefer OOP hooks with `#[Hook('hook_name')]` attributes in `src/Hook/` classes over procedural `.module` functions where the core version supports them.
- Plugins (blocks, field formatters, queue workers) use PHP attributes, not annotations, for new code.
- Entity queries call `->accessCheck(TRUE)` (or deliberately FALSE with a comment).
- Render arrays with `#cache` metadata (tags, contexts, max-age). Don't disable caching to "fix" stale output.
- Output escaping is automatic in Twig; never use `|raw` on user input. Translate strings with `$this->t()` / `{{ 'Text'|t }}`.
- Schema changes go in `hook_update_N()` or post-update functions, never manual SQL.

## Done
phpcs and phpstan are clean, tests pass, `drush cr` runs, and `drush cex` shows no unexpected config diff.
