1
0
Files
breadcrumb-the-shire/CLAUDE.md
fs f793f5d493 chore(drawer): close consolidation gaps after three-consumer rollout
Loose ends surfaced by comparing the three drawer consumers side by side:

- Core i18n was missing `Open full page`, `Loading`, `Failed to load` —
  admin/users showed English strings in the German UI because those labels
  resolve from core, not module catalogs.
- Helpdesk tickets page config did not pass drawer labels; the JS fell back
  to its hard-coded English defaults. Labels now wired via `t(...)`.
- CLAUDE.md notes the `fetchUrl` single-expression convention (required so
  the architecture test can statically locate the fragment path) and
  documents the `onContentLoaded` hook for module-specific runtime
  (helpdesk's async communication feed uses it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:25:07 +02:00

18 KiB

CLAUDE.md — CoreCore

Multi-tenant admin application built on MintyPHP. Manages tenants, users, departments, roles, permissions, address books, mail, CSV imports, scheduled jobs, and Microsoft Entra ID SSO.

Mandatory Workflow

All guard IDs (GR-*) and gate IDs (QG-*) from .agents/checks/ are binding — not advisory.

  • Full workflow trigger is risk-based and defined in .agents/checks/enforcement-policy.json (full_workflow_required_when). Matching changes MUST follow .agents/workflow.md (Analyst → Planner → Executor → Reviewers → Finalizer). Read .agents/README.md and the relevant role prompt in .agents/prompts/ before starting.
  • Quick fixes may skip full workflow only if they match .agents/checks/enforcement-policy.json (quick_fix_allowlist) and still pass required gates.
  • New modules MUST conform to .agents/contracts/module-manifest.schema.json and pass all 10 security guards (GR-SEC-001 through GR-SEC-010).

Full workflow definition: .agents/workflow.md | Guard catalog: .agents/checks/guard-catalog.json | Enforcement policy: .agents/checks/enforcement-policy.json | Guard enforcement map: .agents/checks/guard-enforcement-map.json | Skills: .agents/skills/

Tech Stack

  • Runtime: PHP 8.5 (FPM) on MintyPHP framework (mintyphp/core)
  • Web server: Nginx 1.25 (Alpine)
  • Database: MariaDB 11.4
  • Cache: Memcached 1.6
  • Frontend: Vanilla JS (ES2022 modules), vanilla CSS with @layer system — no build step
  • Key PHP libs: PHPMailer 7, Dompdf 3, endroid/qr-code 6, league/commonmark 2
  • Containerized: Docker Compose (dev + prod variants)

Quick Commands

# Start dev environment
cp config/config.php.example config/config.php
docker compose up --build -d
# App: http://localhost:8080 | phpMyAdmin: http://localhost:8081

# Run PHPUnit tests (phpunit.xml configures bootstrap + testsuite)
docker compose exec php vendor/bin/phpunit

# Run PHPStan (level 5)
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress

# CLI commands (single entry point)
docker compose exec php php bin/console list
docker compose exec php php bin/console module:sync              # full module runtime sync
docker compose exec php php bin/console module:migrate           # apply module SQL migrations
docker compose exec php php bin/console module:permissions-sync  # sync + deactivate orphaned
docker compose exec php php bin/console module:build             # build runtime page root
docker compose exec php php bin/console module:assets-sync       # publish module web assets
docker compose exec php php bin/console module:deactivate <id> --confirm
docker compose exec php php bin/console scheduler:run
docker compose exec php php bin/console doctor

# Check unused Composer packages
docker compose exec php vendor/bin/composer-unused

Project Structure

core/                    # All backend PHP (namespace MintyPHP\)
  App/                  # DI container + bootstrap (AppContainer, Container/Registrars/)
    Module/             # Module platform (Registry, Manifest, Autoloader, Builder)
  Repository/           # SQL queries, prepared statements, filters/paging
  Service/              # Business logic, validation, orchestration
  Http/                 # Auth guards, API auth, request helpers
    Input/              # Request input handling (RequestInput, FormErrors)
  Console/              # CLI command framework (ConsoleApplication, Command base class)
    Commands/           # Command classes (Module/, Scheduler/, Tools/)
  Support/              # Crypto, flash, guard, search, helpers
modules/                # Self-contained feature modules (namespace MintyPHP\Module\<Name>\)
  <id>/module.php       # Module manifest (routes, slots, providers, permissions)
  <id>/lib/              # Module PHP classes (Service, Repository, Providers)
  <id>/pages/           # Module actions + views (symlinked into runtime)
  <id>/templates/       # Module-specific templates
  <id>/web/             # Module CSS/JS (published to web/modules/<id>/)
  <id>/tests/           # Module PHPUnit tests
pages/                  # Core actions (controllers) — file-based routing
  pages/**/*.php        # Action files (read input, check permissions, call services)
  pages/**/*.phtml      # View files (rendering only, no DB calls)
templates/              # Core layouts and partials
  partials/             # Reusable UI components
  emails/               # Email HTML templates
  pdfs/                 # PDF templates (Dompdf)
config/                 # App config (routes, assets, themes, modules)
web/                    # Document root (entry point, CSS, JS, static assets)
  js/core/              # DOM utilities, Grid.js factory, component runtime
  js/components/        # Reusable UI components
  css/                  # Layered CSS (base → components → layout → pages)
  modules/              # Published module assets (symlinks to modules/<id>/web/)
storage/                # Uploaded files + runtime state
  runtime/pages/        # Symlinked page tree (core + modules, built by module-build)
  runtime/settings.php  # Hot-path settings cache file (generated by SettingCacheService)
db/init/init.sql        # Full schema + seed data (no migration framework)
db/updates/             # Idempotent SQL update scripts for existing installs
tests/                  # PHPUnit tests (namespace MintyPHP\Tests\)
docs/                   # German-language documentation (Markdown)
i18n/                   # Translation files (de, en)
bin/                    # CLI entry points (bin/console, bin/dev) + shell tools
docker/                 # Dockerfiles, Nginx configs, PHP configs
.agents/                # Agent workflow system (contracts, prompts, checks, skills, runs)

Architecture Rules

Strict Layering (enforce in all changes)

  1. App (core/App/): DI container bootstrap. Registers all service/repository factories. No business logic.
  2. Repository (core/Repository/): All SQL lives here. No HTTP, session, or rendering.
  3. Service (core/Service/): Business rules and validation. No HTML. Four internal sub-types:
    • Service (*Service.php): Business logic + orchestration. Coordinates repositories and gateways.
    • Gateway (*Gateway.php): Adapter for a single external dependency (repository, settings, HTTP API, scope). No orchestration flow.
    • Factory (*Factory.php): Only place inside core/Service/ that may call new on services/gateways/repositories. Registers via DI container.
    • Policy (*Policy.php in core/Service/Access/): Authorization decisions per resource type. Returns AuthorizationDecision. No HTTP, no business logic.
  4. Action (pages/**/*.php): Reads input, checks permissions + tenant scope, calls services, sets view vars.
  5. View (pages/**/*.phtml): Pure rendering. No DB calls. HTML escaping via e(...).
  6. Template (templates/): Layouts and partials.

Modules

  • Modules are self-contained feature packages under modules/<id>/
  • Namespace: MintyPHP\Module\<Name>\* — never use core namespaces (MintyPHP\Service\*, MintyPHP\Repository\*)
  • Manifest: module.php declares routes, UI slots, providers, permissions, scheduler jobs
  • UI integration: Only via platform slots (aside.tab_panel, topbar.right_item, layout.head_style, runtime.component, etc.) — no core template hardcoding
  • Session keys: Prefixed with module.<id>.*
  • Activation: config/modules.php or APP_ENABLED_MODULES env override
  • Runtime sync: php bin/console module:sync after any module/manifest change (builds page symlinks, syncs permissions, publishes assets)
  • Fingerprint guard: web/index.php validates runtime state matches active modules; fails fast if stale
  • API isolation: API requests (api/ prefix) skip session/auth/cookie flows entirely; modules using API endpoints rely on ApiBootstrap.php
  • Cross-module coupling: Modules may depend on other modules but MUST declare requires: [<module-id>] in their manifest. Undeclared cross-module imports are forbidden. Prefer loose coupling via events and providers over direct service injection
  • Module DB changes: Via module:migrate, not db/updates/ (which is for core schema only)
  • Module permissions: Registered in manifest, synced via module:permissions-sync — never hardcoded
  • Manifest contract: Must conform to .agents/contracts/module-manifest.schema.json

Routing

  • File-based: pages/admin/users/edit($id).php → URL parameter id
  • Module pages: modules/<id>/pages/ symlinked into storage/runtime/pages/ at build time
  • ...(none).phtml → no template layout
  • data().php suffix → data/AJAX endpoints
  • Named aliases in config/routes.php with public flag for auth guard
  • Module routes declared in manifest, merged at boot (fail-fast on collision)

PHP Conventions

  • PSR-4 autoload: MintyPHP\core/, MintyPHP\Module\*modules/*/lib/, MintyPHP\Tests\tests/
  • All user-facing text uses t('key') translation helper — keys must exist in all language files under i18n/
  • Request input via requestInput() — never raw $_GET/$_POST/$_SESSION (GR-CORE-003)
  • Permissions + tenant scope enforced in actions/services, never just in UI
  • Security/integration settings stay in DB, not in storage/runtime/settings.php file cache (GR-CORE-011)
  • POST-Redirect-GET (PRG) pattern on form submissions; flash before redirect (GR-CORE-012)
  • Constructor injection for dependencies; no hidden global state (GR-TEST-002)
  • New/changed Service or Gateway must have PHPUnit tests — happy path + edge case (GR-TEST-001)

Frontend Conventions

  • Vanilla ES2022 modules, no bundler
  • CSS classes prefixed with app-
  • Defensive DOM checks (elements may not exist on every page)
  • No business logic in frontend
  • Assets served raw with assetVersion() cache-busting (filemtime-based)

UI Patterns

  • Edit pages: tabs → Master data | Visibility | optional Danger zone
  • Tab panel width: data-tab-panel defaults to max-width: 70ch (optimal for form readability). Add data-tab-panel-wide for panels that need full width (e.g. split layouts, side-by-side editors, preview panels, tables). Use 70ch for standard form fields (text inputs, selects, textareas); use wide for multi-column layouts or content that benefits from horizontal space.
  • Titlebar partial for primary actions (Save, Save & close)
  • Secondary actions in Aside block via app-details-aside-actions.phtml
  • Lists use Grid.js via initStandardListPage() (GR-UI-LIST)
  • Detail pages use shared partials and layout contracts (GR-UI-DETAIL)
  • New views must handle loading, empty, error, and success states (GR-UI-014)
  • All visible text wrapped in t('...')
  • Check existing JS/CSS components before adding new ones (GR-UI-REUSE)
  • Buttons: Always use core classes (secondary outline small, danger outline small) — custom CSS only for shape/position. Icon-only buttons must have data-tooltip + aria-label.
  • Components inside forms: Reset core margin-bottom on buttons/inputs/selects/labels at component scope. Checkboxes in flex need explicit 1.25em sizing + flex-shrink: 0.
  • Sticky elements: Always use top: calc(var(--app-topbar-height) + ...) — never top: 0. Multi-column grids must have a responsive single-column @media fallback.
  • File uploads use app-file-upload.phtml partial (deliberate exception to plain-HTML inputs — the markup is too complex for inline use). See templates/partials/app-file-upload.phtml for the $fileUpload array contract. Never write file-upload markup inline; always use the partial.
  • List exports: every Grid.js list that offers a download MUST go through the core primitive — no inline fputcsv, no hand-rolled headers, no bespoke dropdown. (GR-UI-EXPORT)
    • Server: endpoint calls exportRequireGetRequest() + exportCapLimit() + exportSendCsv() from core/Support/helpers/export.php, builds columns as ExportColumn[] (core/Service/Export/CsvExportService.php), and reuses the SAME filter-schema.php via gridParseFiltersFromSchemaFile() as the data endpoint. Extract a shared service or presenter if row-formatting is non-trivial (see DomainListService / SystemAuditRowPresenter).
    • View: require templatePath('partials/app-list-export-dropdown.phtml'); inside $listTitleActionsHtml. Pass the URL via 'exportUrl' => endpointUrl('<route-path>') in the page config — always endpointUrl(), never plain lurl(). The helper resolves module routes to their target path so query strings survive.
    • Client: initListExport({ gridConfig, exportUrl }) from /js/core/app-list-export.js. No hand-rolled click listener, no inline URL construction.
  • Detail drawer: Row-level detail views that slide in from the right use the core primitive — no per-module drawer implementation.
    • Server: Action <route>/<name>-fragment($id).php + view <route>/<name>-fragment(none).phtml. The (none) template renders without layout/topbar/breadcrumbs. Reuse the same partial that the full-page view uses (extract a modules/<id>/templates/<name>-profile.phtml if needed). Must enforce auth + scope exactly like the full-page view.
    • Route: Declare ['path' => '<route>/<name>-fragment', 'target' => '<route>/<name>-fragment'] in the module manifest.
    • Client: initDetailDrawer({ gridConfig, fetchUrl: (uuid) => new URL(/-fragment/${uuid}, appBase).toString(), fullUrl, hashPrefix, rowProvider?, onContentLoaded? }) from /js/components/app-detail-drawer.js. The drawer handles focus-trap, body-scroll-lock, session-expiry reload, and auto-initializes tabs/tooltips/fslightbox in the fragment via initFragmentContent(). Consumers do NOT need to call initTabs, refreshFsLightbox, etc. manually. For module-specific runtime (e.g. async communication feeds) use onContentLoaded(contentEl, uuid).
    • fetchUrl signature: write it as a single-expression arrow (not a block body) so the architecture test can statically locate the fragment path. Pattern: (uuid) => new URL(`path/${uuid}...`, appBase).toString().
    • Architecture test DetailDrawerFragmentContractTest enforces that every initDetailDrawer({ fetchUrl }) has matching *-fragment($id).php + *-fragment(none).phtml.
    • Filename rule: URL parameters go only into the .php action filename (*-fragment($id).php). The (none).phtml view filename must NOT contain ($id) — MintyPHP's router parses paren groups and breaks silently on nested ones.

Never Do This

  • Direct $_GET/$_POST/$_SESSION access — use requestInput() (GR-CORE-003)
  • SQL outside Repository classes (GR-SEC-003)
  • new Service() or new Gateway() outside Factory classes (GR-TEST-002)
  • Hardcoding module UI into core templates — use platform slots
  • Core schema changes without idempotent db/updates/ script (GR-CORE-010)
  • Settings in config files instead of DB settings table (GR-CORE-011)
  • Skipping PRG pattern on form submissions (GR-CORE-012)
  • Loading assets from external CDNs (GR-SEC-004)
  • Undeclared cross-module imports (must use requires in manifest)
  • Inline SQL or string-concatenated queries (GR-SEC-003)
  • Raw echo in views without // raw-html-ok marker — use e() (GR-SEC-010)
  • OpenAPI changes without updating the spec (GR-CORE-007)
  • Hand-rolled list exports (inline fputcsv, own header setup, bespoke click-to-download JS) — always use core/Service/Export/* + exportSendCsv() + app-list-export-dropdown.phtml + initListExport() (GR-UI-EXPORT)
  • Building an export/data endpoint URL with lurl() when it will carry query params — use endpointUrl() so module routes where path ≠ target still resolve (GR-UI-EXPORT)

Database

  • Schema defined in db/init/init.sql (applied once on container init)
  • No migration framework — core schema changes for existing installs are idempotent SQL scripts in db/updates/
  • Module schema changes via module:migrate (separate from core)
  • Settings stored in settings DB table (single source of truth); storage/runtime/settings.php is a partial file cache for hot-path UI reads only

Security (non-negotiable)

These rules are enforced by guards GR-SEC-001 through GR-SEC-010 in .agents/checks/guard-catalog.json. Any violation is a blocking finding.

  • CSRF token verified on every POST (GR-SEC-001)
  • No PII or secrets in logs or audit metadata (GR-SEC-002)
  • SQL only via Repository prepared statements — no inline queries (GR-SEC-003)
  • No external CDN or remote asset loading (GR-SEC-004)
  • Encryption only via core/Support/Crypto.php AES-256-GCM (GR-SEC-005)
  • File uploads: storage/ only, authz-gated serving, MIME-validated (GR-SEC-006)
  • API requests must not start sessions (GR-SEC-007)
  • Authorization enforced server-side, not just in UI (GR-SEC-008)
  • Tenant scope (tenant_id) enforced on all queries — no unscoped data access (GR-SEC-009)
  • Views use e() for all output — raw echo only with // raw-html-ok: reason marker (GR-SEC-010)

Quality Gates

9 gates defined in .agents/checks/quality-gates.json. Required/optional enforcement is defined in .agents/checks/enforcement-policy.json.

ID Gate Command
QG-001 PHPUnit vendor/bin/phpunit
QG-002 PHPStan level 5 vendor/bin/phpstan analyse -c phpstan.neon
QG-003 Architecture Contract vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php
QG-006 PHP Style vendor/bin/php-cs-fixer fix --config=tools/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --diff --verbose
QG-008 Docs link + drift integrity bin/docs-link-check.sh (plus bin/docs-drift-check.sh via bin/qa-required.sh)
QG-009 Codex skills sync bin/codex-skills-sync.sh --check

Additional non-blocking gates: QG-004 structural checks, QG-005 JS smoke, QG-007 composer-unused. See gate catalog + enforcement policy for details.

Environment

  • Config via .env (gitignored) — copy from .env.example for dev, .env.prod.example for prod
  • config/config.php (gitignored) — copy from config/config.php.example
  • APP_CRYPTO_KEY (64-char hex) needed for AES-256-GCM encryption of SSO secrets