Most German docs were written with `ue`/`ae`/`oe`/`ss` ASCII fallbacks (`fuer`, `aenderung`, `koennen`, `gemaess`) — relic from older toolchain constraints. Replaced 237 occurrences across 38 .md files with proper umlauts and ß so the docs read naturally. Substitutions are constrained to plain prose: fenced code blocks (```...```), inline code spans (`...`), markdown link targets `[..](..)`, and HTML comments are preserved verbatim. Path references like `docs/howto-erste-aenderung.md` keep their original (umlaut-replaced) filenames — only the surrounding prose is normalised. Tool: bin/fix-md-umlauts.py (idempotent — no-op on already-clean files). Re-runnable if ASCII fallbacks creep back in via copy-paste. Doc-link-check + doc-drift-check stay green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
153 lines
5.8 KiB
Markdown
153 lines
5.8 KiB
Markdown
# Architektur-Kompass
|
|
|
|
Letzte Aktualisierung: 2026-03-25
|
|
|
|
## Ziel
|
|
|
|
Verbindliche Kurzregeln für Architekturentscheidungen.
|
|
|
|
## Systembild
|
|
|
|
- Server-gerenderte Admin-App + REST-API unter `/api/v1`.
|
|
- Multi-Tenant-Isolation ist Standard.
|
|
- Autorisierung läuft zentral über Policies.
|
|
- Externe API-Verträge sind UUID-first.
|
|
|
|
## Schichtenübersicht
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
Browser([Browser / API-Client])
|
|
|
|
subgraph APP["core/App/ — Composition Root"]
|
|
DI["AppContainer + Registrars"]
|
|
end
|
|
|
|
subgraph SVC["core/Service/"]
|
|
direction LR
|
|
FAC["Factory"]
|
|
SERVICE["Service"]
|
|
GW["Gateway"]
|
|
POL["Policy"]
|
|
end
|
|
|
|
subgraph REPO["core/Repository/"]
|
|
REP["Repository"]
|
|
end
|
|
|
|
subgraph pages["pages/**"]
|
|
ACT["Action (*.php)"]
|
|
VIEW["View (*.phtml)"]
|
|
end
|
|
|
|
subgraph TPL["templates/"]
|
|
LAY["Layouts & Partials"]
|
|
end
|
|
|
|
DB[(MariaDB)]
|
|
|
|
DI -->|"ruft zur Laufzeit"| FAC
|
|
FAC -->|"erzeugt"| SERVICE
|
|
FAC -->|"erzeugt"| GW
|
|
|
|
Browser --> ACT
|
|
ACT -->|"Input · AuthZ · Response"| SERVICE
|
|
ACT --> POL
|
|
ACT --> VIEW
|
|
VIEW --> LAY
|
|
SERVICE --> GW
|
|
GW --> REP
|
|
SERVICE --> REP
|
|
REP --> DB
|
|
```
|
|
|
|
## Request-Flow
|
|
|
|
1. `web/index.php` bootstrappt Routing/Konfiguration und initialisiert den `AppContainer`.
|
|
2. Guard/Firewall/Session/Locale greifen.
|
|
3. Action in `pages/**` nimmt Input an.
|
|
4. Action delegiert an Service.
|
|
5. Service nutzt Repository für Datenzugriff.
|
|
6. Response wird als HTML/JSON/Datei ausgegeben.
|
|
|
|
## Schichtregeln (MUSS)
|
|
|
|
- `pages/**`: Input, Auth/AuthZ, Orchestrierung, Response.
|
|
- `core/Service/**`: Fachlogik und Anwendungsfluss.
|
|
- `core/Repository/**`: SQL, Filter, Paging, Mapping.
|
|
- `templates/**`: Rendering ohne Business-Logik.
|
|
- Templates konsumieren nur vom Action-Layer vorbereitete View-Daten (`$viewAuth`, Form/ViewModel), keine direkten Permission-Helper.
|
|
- Service-Auflösung in Actions/Helpers nur über `app(Foo::class)`.
|
|
- In Actions bevorzugt direkte Klassen-Bindings (`app(Service::class)`) statt Factory-Ketten.
|
|
- In `pages/admin/**` keine `Factory::class`-Referenzen.
|
|
- Request-Input in Actions nur über `requestInput()` lesen.
|
|
- Validierungsfehler über `FormErrors` führen und für APIs mit `ApiResponse::validationFromFormErrors(...)` ausgeben.
|
|
- In `pages/api/v1/**` kein direktes JSON-Body-Lesen über `ApiResponse::readJsonBody(...)`.
|
|
- `Flash` nur für Redirect-/Outcome-Meldungen verwenden, nicht als primäre Validation-Struktur.
|
|
- Listen-Endpoints in `pages/**/data().php` sind GET-only (`gridRequireGetRequest()`).
|
|
- Query-Filter in `data().php` über `gridParseFiltersFromSchemaFile(...)` + `filter-schema.php` normalisieren (enthält `limit`, `offset`, `order`, `dir` im Ergebnis).
|
|
|
|
## Instanziierungsregeln (MUSS)
|
|
|
|
- Composition Root: `web/index.php` + `core/App/registerContainer.php`
|
|
(→ Details: `/docs/explanation-di-container.md`).
|
|
- `new ...Factory()` nur im Composition Root und in `*Factory.php`.
|
|
- In `core/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Factory()`.
|
|
- In `core/Service/**` außerhalb von `*Factory.php` keine direkten `new ...Service()`, `new ...Gateway()` oder `new ...Repository()`.
|
|
- Legacy-Helper (`accessServicesFactory()`, `userServicesFactory()` usw.) sind entfernt und werden nicht mehr genutzt.
|
|
|
|
## API- und Security-Regeln (MUSS)
|
|
|
|
- Tenant-Scope und Permission nur serverseitig erzwingen.
|
|
- Denials je Endpunkt als `403` oder `404` modellieren.
|
|
- API-Fehlerformat konsistent halten (`error`, optional `errors`).
|
|
- OpenAPI bei API-Änderungen im selben Merge aktualisieren.
|
|
|
|
## RBAC-UI-Standard (MUSS)
|
|
|
|
- Ability-Entscheidungen laufen serverseitig über `AuthorizationService`.
|
|
- Für Layout/Partials werden globale Capabilities zentral über `UiAccessService::layoutCapabilities()` + `UiCapabilityMap::LAYOUT` bereitgestellt.
|
|
- Für einzelne Views setzen Actions explizit `$viewAuth['page']` (bevorzugt über `UiCapabilityMap` + `UiAccessService::pageCapabilities()`).
|
|
- In `templates/**` keine direkten `can('...')`- oder Permission-Key-Checks.
|
|
|
|
## Frontend-Page-Module (MUSS)
|
|
|
|
- In `pages/**` und `templates/**` sind inline `<script type="module">...</script>` ohne `src` verboten.
|
|
- Seiteninitialisierung liegt in dedizierten ESM-Page-Modulen unter `web/js/pages/*`.
|
|
- Pro Seite wird Konfiguration per `<script type="application/json" id="page-config-<page-key>">...</script>` bereitgestellt und im Modul über `readPageConfig(...)` gelesen.
|
|
- In `pages/**` und `templates/**` müssen alle JS-`src` (eigene und Vendor) über `assetVersion('...js...')` laufen.
|
|
|
|
## Mindestchecks vor Merge
|
|
|
|
```bash
|
|
docker compose exec php vendor/bin/phpunit
|
|
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
|
|
```
|
|
|
|
Bei JS-Änderungen zusätzlich Browser-Smoke mit DevTools-Console (keine JS-Errors).
|
|
|
|
Struktur-Gates zusätzlich:
|
|
|
|
```bash
|
|
(rg 'new\s+[^\s(]+Factory\(' core/Service || true) | wc -l
|
|
(rg --glob '!**/*Factory.php' 'new\s+[^\s(]+(Service|Gateway|Repository)\(' core/Service || true) | wc -l
|
|
(rg "\b(accessServicesFactory|directoryServicesFactory|userServicesFactory|authServicesFactory|tenantServicesFactory|settingServicesFactory|userRepositoryFactory|authRepositoryFactory|authGatewayFactory)\(" pages core templates web || true) | wc -l
|
|
(rg -n 'new\s+[^\s(]+(Service|Gateway|Repository)\(' pages -g '*.php' || true) | wc -l
|
|
(rg -n '\bDB::' pages -g '*.php' || true) | wc -l
|
|
(rg -n 'app\([^\n]*Factory::class\)->create' pages/admin -g '*.php' || true) | wc -l
|
|
(rg -n 'Factory::class' pages/admin -g '*.php' || true) | wc -l
|
|
(rg -n 'ApiResponse::readJsonBody\(' pages/api/v1 -g '*.php' || true) | wc -l
|
|
```
|
|
|
|
Architektur-Contract:
|
|
|
|
```bash
|
|
docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php
|
|
```
|
|
|
|
## Vertiefung
|
|
|
|
- `/docs/reference-core-standards.md`
|
|
- `/docs/explanation-sicherheitsmodell.md`
|
|
- `/docs/howto-rbac-permissions-playbook.md`
|