Verbesserungen & Roadmap¶
Systematische Code-Analyse mit konkreten Verbesserungsvorschlägen. Jeder Eintrag enthält einen Prompt, der direkt in Claude Code eingefügt werden kann.
Priorität: Hoch¶
[Sicherheit] CORS-Konfiguration – Origin-Validierung fehlt¶
Problem: Der ResponseEmitter (src/Application/ResponseEmitter/ResponseEmitter.php) spiegelt den HTTP_ORIGIN-Header ohne Validierung zurück und setzt gleichzeitig Access-Control-Allow-Credentials: true. Jede beliebige Origin kann dadurch authentifizierte Requests stellen.
Impact: 🔴 Hoch
Aufwand: S
Betroffene Dateien: src/Application/ResponseEmitter/ResponseEmitter.php
Umsetzungs-Prompt¶
Öffne
src/Application/ResponseEmitter/ResponseEmitter.php. Aktuell wird$_SERVER['HTTP_ORIGIN']ungefiltert in denAccess-Control-Allow-Origin-Header geschrieben, währendAccess-Control-Allow-Credentials: truegesetzt ist. Das ist eine CORS-Sicherheitslücke.Erstelle eine Whitelist erlaubter Origins. Lese die erlaubten Domains aus der Umgebungsvariable
CORS_ALLOWED_ORIGINS(kommasepariert). Falls die Variable nicht gesetzt ist, erlaube nur dieDOMAINundMAIN_DOMAINaus der.env. Prüfe den eingehenden Origin gegen diese Whitelist, bevor du ihn zurückspiegelst. Ist der Origin nicht erlaubt, setze keinenAccess-Control-Allow-Origin-Header.Teste danach: Sende einen Request mit
Origin: https://evil.example.com– der Response darf keinenAccess-Control-Allow-Origin-Header enthalten.
[Sicherheit] OAuth-Scopes für Tickets und Customer fehlen¶
Problem: NamespaceTicketController definiert SCOPE_TICKET_READ und SCOPE_TICKET_WRITE, aber diese Scopes fehlen in der erlaubten Scope-Liste des OAuthController (src/Controller/Api/OAuthController.php). Gleiches gilt für customer:read/customer:write aus CustomerController.
Impact: 🔴 Hoch
Aufwand: S
Betroffene Dateien: src/Controller/Api/OAuthController.php, src/Controller/Api/V1/NamespaceTicketController.php, src/Controller/Api/V1/CustomerController.php
Umsetzungs-Prompt¶
Öffne
src/Controller/Api/OAuthController.php. Suche die Liste der erlaubten OAuth-Scopes (circa Zeile 19-28). Dort fehlen die Scopesticket:read,ticket:write,customer:readundcustomer:write. Ergänze diese vier Scopes in der erlaubten Liste, analog zu den bestehenden Einträgen (z.B.cms:read,news:write).Prüfe danach, dass
NamespaceTicketController::SCOPE_TICKET_READundSCOPE_TICKET_WRITEmit den neuen Einträgen übereinstimmen. Aktualisiere auch die MCP-Connector-Dokudocs/mcp-connector-setup.md, falls die Scopes dort noch nicht gelistet sind.Teste: Ein OAuth-Token-Request mit
scope=ticket:read ticket:writedarf keinen Fehler mehr werfen.
[Performance] Fehlende Pagination in API- und MCP-List-Endpoints¶
Problem: Alle List-Endpoints (API v1 und MCP-Tools) liefern alle Datensätze ohne Limit zurück. Bei großen Namespaces mit vielen Seiten/Tickets/News führt das zu hohem Speicherverbrauch und langen Antwortzeiten.
Impact: 🔴 Hoch
Aufwand: M
Betroffene Dateien: src/Controller/Api/V1/NamespacePageController.php, src/Controller/Api/V1/NamespaceTicketController.php, src/Controller/Api/V1/NamespaceNewsController.php, src/Controller/Api/V1/NamespaceQuizController.php, src/Service/Mcp/PageTools.php, src/Service/Mcp/TicketTools.php, src/Service/Mcp/NewsTools.php
Umsetzungs-Prompt¶
Implementiere Pagination für alle API-v1-List-Endpoints und MCP-List-Tools. Beginne mit
src/Controller/Api/V1/NamespacePageController.phpMethodelist().
- Akzeptiere Query-Parameter
offset(int, default 0) undlimit(int, default 50, max 200).- Reiche diese an
PageService::getAllForNamespace()weiter – ergänze die Methode insrc/Service/PageService.phpumLIMITundOFFSETin der SQL-Query.- Gib im Response zusätzlich
"pagination": { "offset": 0, "limit": 50, "total": 123 }zurück.- Wende dasselbe Muster auf
NamespaceTicketController::list(),NamespaceNewsController::list()undNamespaceQuizController::listEvents()an.- Ergänze in den MCP-Tools (
PageTools,TicketTools,NewsTools) die Parameteroffsetundlimitin dendefinitions()und leite sie an die Services weiter.Teste:
GET /api/v1/namespaces/test/pages?limit=2&offset=0liefert maximal 2 Seiten und einpagination-Objekt.
[MCP-Härtung] Fehlende Try-Catch-Blöcke in MCP-Tools¶
Problem: MCP-Tools wie NewsTools und TicketTools werfen Exceptions direkt (InvalidArgumentException, RuntimeException), statt sie in strukturierte Fehlerantworten zu wrappen. Die API-Controller machen es richtig (try-catch mit JSON-Error-Response), die MCP-Tools nicht.
Impact: 🔴 Hoch
Aufwand: M
Betroffene Dateien: src/Service/Mcp/NewsTools.php, src/Service/Mcp/TicketTools.php, src/Service/Mcp/QuizTools.php, src/Service/Mcp/WikiTools.php, src/Service/Mcp/McpToolRegistry.php
Umsetzungs-Prompt¶
Prüfe
src/Service/Mcp/McpToolRegistry.phpMethodecallTool(). Stelle sicher, dass dort ein globaler try-catch existiert, der alle Exceptions fängt und als strukturierte MCP-Fehlerantwort zurückgibt (Format:{ "isError": true, "content": [{ "type": "text", "text": "Fehlermeldung" }] }).Prüfe dann in
src/Service/Mcp/NewsTools.phpdie MethodecreateNews()(ca. Zeile 145-166): Dort wirdnew DateTimeImmutable($args['publishedAt'])ohne try-catch aufgerufen. Ungültige Datumsstrings werfen eine Exception. Wrappe den Aufruf in try-catch und gib eine verständliche Fehlermeldung zurück.Wende dasselbe Muster auf alle Handler-Methoden in
TicketTools.php,QuizTools.phpundWikiTools.phpan, die Dates parsen oder externe Services aufrufen.Teste: Rufe
create_newsmitpublishedAt: "kein-datum"auf – die Antwort muss ein strukturierter Fehler sein, kein Stack-Trace.
[MCP-Härtung] Duplizierter resolveNamespace()-Code in allen Tool-Klassen¶
Problem: Alle 9 MCP-Tool-Klassen implementieren identischen resolveNamespace()-Code und identische Input-Validierungspatterns. Das sind ~45 Stellen mit duplizierter Logik.
Impact: 🟡 Mittel
Aufwand: S
Betroffene Dateien: src/Service/Mcp/PageTools.php, src/Service/Mcp/MenuTools.php, src/Service/Mcp/NewsTools.php, src/Service/Mcp/FooterTools.php, src/Service/Mcp/QuizTools.php, src/Service/Mcp/StylesheetTools.php, src/Service/Mcp/WikiTools.php, src/Service/Mcp/TicketTools.php, src/Service/Mcp/BackupTools.php
Umsetzungs-Prompt¶
Erstelle einen neuen Trait
src/Service/Mcp/McpToolTrait.phpmit folgenden Methoden:
resolveNamespace(array $args): string– die gemeinsame Namespace-AuflösungrequireString(array $args, string $key): string– wirft Exception wenn leerrequireInt(array $args, string $key): int– castet und prüft > 0optionalString(array $args, string $key, string $default = ''): stringoptionalInt(array $args, string $key, int $default = 0): intoptionalBool(array $args, string $key, ?bool $default = null): ?boolNutze den Trait in allen 9 Tool-Klassen und ersetze die duplizierten Methoden. Die
$defaultNamespace-Property bleibt als Constructor-Parameter.Teste: Alle bestehenden MCP-Tool-Aufrufe müssen weiterhin funktionieren. PHPStan darf keine neuen Fehler melden.
Priorität: Mittel¶
[Performance] Fehlende Datenbank-Indizes auf Namespace-Spalten¶
Problem: Die pages- und events-Tabellen haben zwar Unique-Constraints auf (namespace, slug), aber keinen einzelnen Index auf namespace für List-Queries. PageService::getAllForNamespace() und EventService::getAll() filtern nach namespace und profitieren von einem dedizierten Index.
Impact: 🟡 Mittel
Aufwand: S
Betroffene Dateien: migrations/ (neue Migration)
Umsetzungs-Prompt¶
Erstelle eine neue Migration
migrations/YYYYMMDD_add_namespace_indexes.sql(mit aktuellem Datum). Füge folgende Indizes hinzu:CREATE INDEX IF NOT EXISTS idx_pages_namespace ON pages (namespace); CREATE INDEX IF NOT EXISTS idx_events_namespace ON events (namespace); CREATE INDEX IF NOT EXISTS idx_landing_news_namespace ON landing_news (namespace); CREATE INDEX IF NOT EXISTS idx_tickets_namespace ON tickets (namespace); CREATE INDEX IF NOT EXISTS idx_cms_footer_blocks_namespace ON cms_footer_blocks (namespace); CREATE INDEX IF NOT EXISTS idx_cms_menus_namespace ON cms_menus (namespace);Teste:
php scripts/run_migrations.phpmuss fehlerfrei durchlaufen. Prüfe mitEXPLAIN ANALYZE SELECT * FROM pages WHERE namespace = 'test'dass der Index verwendet wird.
[API-Design] Inkonsistente Fehler-Responses über API-Controller hinweg¶
Problem: API-Controller verwenden unterschiedliche Error-Formate. Manche geben { "error": "code" } zurück, andere { "error": "code", "message": "..." }, wieder andere { "error": "code", "details": [...] }. Es fehlt ein standardisiertes Error-Format.
Impact: 🟡 Mittel
Aufwand: M
Betroffene Dateien: src/Controller/Api/V1/NamespacePageController.php, src/Controller/Api/V1/NamespaceTicketController.php, src/Controller/Api/V1/NamespaceNewsController.php, src/Controller/Api/V1/NamespaceQuizController.php
Umsetzungs-Prompt¶
Erstelle einen Trait oder eine Methode in einem gemeinsamen Base-Controller
src/Controller/Api/V1/AbstractApiController.phpmit der MethodeerrorResponse(Response $response, int $status, string $code, string $message = '', array $details = []): Response.Das einheitliche Format soll sein:
Ersetze alle individuellen Error-Response-Patterns in den 4 API-v1-Controllern durch Aufrufe dieser Methode. Achte darauf, dass bestehende Error-Codes (
invalid_json,missing_scope,not_found, etc.) beibehalten werden.Teste: Alle bestehenden API-Tests müssen weiterhin bestehen. Error-Responses müssen konsistent die drei Felder enthalten.
[Performance] Fehlender Transaction-Support bei Page-Upsert¶
Problem: NamespacePageController::upsert() führt mehrere separate DB-Operationen durch (Seite speichern, SEO-Config speichern, Menu-Assignments erstellen) ohne Transaktions-Klammer. Schlägt Schritt 3 fehl, ist die Seite bereits gespeichert.
Impact: 🟡 Mittel
Aufwand: S
Betroffene Dateien: src/Controller/Api/V1/NamespacePageController.php
Umsetzungs-Prompt¶
Öffne
src/Controller/Api/V1/NamespacePageController.php, Methodeupsert(). Wrappe die gesamte Speicherlogik (Page upsert + SEO config + Menu assignments) in eine DB-Transaktion:$pdo->beginTransaction(); try { // ... bestehende Logik ... $pdo->commit(); } catch (\Throwable $e) { $pdo->rollBack(); throw $e; }Die PDO-Instanz sollte über den Constructor oder Container verfügbar sein. Falls nicht, injiziere sie.
Teste: Erstelle eine Seite mit ungültigem
menuAssignments-Payload. Die Seite darf NICHT gespeichert werden, wenn die Assignments fehlschlagen.
[MCP-Härtung] Fehlende Filter- und Sortieroptionen in MCP-List-Tools¶
Problem: MCP-List-Tools wie list_pages und list_news unterstützen keine Filterung (z.B. nach Status, Sprache) und keine Sortierung. list_tickets hat Filter, aber list_pages nicht.
Impact: 🟡 Mittel
Aufwand: M
Betroffene Dateien: src/Service/Mcp/PageTools.php, src/Service/Mcp/NewsTools.php
Umsetzungs-Prompt¶
Erweitere
src/Service/Mcp/PageTools.phpToollist_pages:
- Füge die optionalen Parameter
status(draft/published),language,typeundparentIdzurinputSchemahinzu.- Leite die Filter an
PageService::getAllForNamespace()weiter – ergänze die Service-Methode um optionale WHERE-Klauseln.- Füge einen
sortBy-Parameter hinzu (Werte:title,slug,updatedAt; Default:title).Wende dasselbe Muster auf
NewsTools::listNews()an (Filter:isPublished, Sort:publishedAt,title).Teste:
list_pages({ status: "published" })liefert nur veröffentlichte Seiten.
[Code-Qualität] Services mit direkten PDO-Queries statt Repository-Pattern¶
Problem: 37 Service-Klassen enthalten direkte SQL-Queries via PDO, während nur 5 Repository-Klassen existieren. Das Repository-Pattern wurde begonnen, aber nicht durchgezogen (Verhältnis 37:5).
Impact: 🟡 Mittel
Aufwand: XL
Betroffene Dateien: src/Service/PageService.php, src/Service/LandingNewsService.php, src/Service/CmsMenuDefinitionService.php, src/Service/TicketService.php u.v.m.
Umsetzungs-Prompt¶
Dies ist ein großes Refactoring. Beginne mit den am häufigsten genutzten Services:
- Erstelle
src/Repository/PageRepository.phpund extrahiere alle SQL-Queries aussrc/Service/PageService.phpdorthin. Der Service ruft dann nur noch Repository-Methoden auf.- Erstelle
src/Repository/TicketRepository.phpanalog fürsrc/Service/TicketService.php.- Erstelle
src/Repository/NewsRepository.phpanalog fürsrc/Service/LandingNewsService.php.Muster: Jede Repository-Methode nimmt primitive Parameter entgegen und gibt Arrays oder Domain-Objekte zurück. Kein Business-Logic im Repository.
Teste: Alle bestehenden Tests müssen bestehen. PHPStan Level 4 darf keine neuen Fehler zeigen.
[DX] Fehlende PHPStan-Konfiguration und CS-Fixer¶
Problem: PHPStan läuft auf Level 4. Es fehlt ein automatischer Code-Style-Fixer (php-cs-fixer) und PHPStan könnte auf Level 5+ angehoben werden.
Impact: 🟡 Mittel
Aufwand: M
Betroffene Dateien: phpstan.neon.dist, composer.json, .php-cs-fixer.dist.php (neu)
Umsetzungs-Prompt¶
- Erstelle
.php-cs-fixer.dist.phpmit einer PSR-12-Konfiguration für dassrc/-Verzeichnis.- Füge
friendsofphp/php-cs-fixerals Dev-Dependency zucomposer.jsonhinzu.- Ergänze in
composer.jsonunterscripts:"cs-fix": "php-cs-fixer fix"und"cs-check": "php-cs-fixer fix --dry-run --diff".- Hebe in
phpstan.neon.distdas Level von 4 auf 5 an. Behebe die neu auftretenden Fehler oder füge sie alsignoreErrorshinzu.- Ergänze den
cs-check-Schritt im GitHub Actions Workflowtests.yml.Teste:
composer cs-checkmeldet Style-Verstöße.composer cs-fixbehebt sie.vendor/bin/phpstan analyseauf Level 5 zeigt keine unbehandelten Fehler.
Priorität: Niedrig¶
[Modernisierung] Veraltetes POSTGRES_PASS-Alias¶
Problem: Das Entrypoint-Skript unterstützt noch POSTGRES_PASS als Alias für POSTGRES_PASSWORD mit Deprecation-Warnung. Dieses Legacy-Mapping sollte entfernt werden.
Impact: 🟢 Niedrig
Aufwand: S
Betroffene Dateien: docker-entrypoint.sh, sample.env
Umsetzungs-Prompt¶
Öffne
docker-entrypoint.sh. Suche nachPOSTGRES_PASSund entferne das Mapping aufPOSTGRES_PASSWORD. Entferne auch eventuelle Kommentare dazu. Stelle sicher, dasssample.envnurPOSTGRES_PASSWORDdokumentiert und keinPOSTGRES_PASSmehr enthält.Teste: Docker-Container startet fehlerfrei mit
POSTGRES_PASSWORD. MitPOSTGRES_PASSsoll der Container einen klaren Fehler werfen.
[Fehlende Features] REST-API für Footer-Blöcke fehlt¶
Problem: Footer-Blöcke können nur über MCP-Tools und die Admin-UI verwaltet werden. Es gibt keinen REST-API-v1-Controller für Footer-Operationen, obwohl die Scopes footer:read/footer:write im OAuth definiert sind.
Impact: 🟢 Niedrig
Aufwand: M
Betroffene Dateien: src/Controller/Api/V1/ (neue Datei), src/Routes/api_v1.php
Umsetzungs-Prompt¶
Erstelle
src/Controller/Api/V1/NamespaceFooterController.phpmit folgenden Endpoints:
GET /api/v1/namespaces/{ns}/footer/blocks– Blöcke auflisten (Scope:footer:read)POST /api/v1/namespaces/{ns}/footer/blocks– Block erstellen (Scope:footer:write)PATCH /api/v1/namespaces/{ns}/footer/blocks/{id}– Block aktualisieren (Scope:footer:write)DELETE /api/v1/namespaces/{ns}/footer/blocks/{id}– Block löschen (Scope:footer:write)GET /api/v1/namespaces/{ns}/footer/layout– Layout abrufen (Scope:footer:read)PUT /api/v1/namespaces/{ns}/footer/layout– Layout aktualisieren (Scope:footer:write)Nutze den bestehenden
CmsFooterBlockService. Registriere die Routes insrc/Routes/api_v1.phpanalog zu den bestehenden Endpoints.Teste:
GET /api/v1/namespaces/test/footer/blocksmit gültigem Token undfooter:read-Scope liefert die Footer-Blöcke.
[Fehlende Features] Batch-Operationen in MCP-Tools¶
Problem: Alle MCP-Tools arbeiten auf Einzelobjekten. Für Workflows wie „erstelle 5 Seiten" oder „lösche alle Entwürfe" sind 5+ separate Tool-Calls nötig.
Impact: 🟢 Niedrig
Aufwand: L
Betroffene Dateien: src/Service/Mcp/PageTools.php, src/Service/Mcp/TicketTools.php
Umsetzungs-Prompt¶
Erweitere
src/Service/Mcp/PageTools.phpum ein neues Toolbatch_upsert_pages:
- Input:
{ "namespace": "...", "pages": [{ "slug": "...", "blocks": [...], ... }] }- Verarbeite jede Seite in einer Transaktion. Bei Fehler: Rollback und Fehlermeldung für die betroffene Seite.
- Rückgabe:
{ "results": [{ "slug": "...", "status": "ok" }, { "slug": "...", "status": "error", "message": "..." }] }Ergänze analog
batch_transition_ticketsinTicketTools.php(mehrere Tickets gleichzeitig den Status ändern).Registriere die neuen Tools in der jeweiligen
definitions()-Methode. Teste:batch_upsert_pagesmit 3 Seiten erstellt alle 3 in einer Transaktion.
[Quick Win] Fehlende CHANGELOG.md-Verlinkung in der Doku¶
Problem: Das Changelog wird automatisch via git-cliff generiert, ist aber nicht in der MkDocs-Navigation verlinkt.
Impact: 🟢 Niedrig
Aufwand: S
Betroffene Dateien: mkdocs.yml, CHANGELOG.md
Umsetzungs-Prompt¶
Prüfe ob
CHANGELOG.mdim Root existiert. Falls ja, kopiere es nachdocs/changelog.md(oder erstelle einen Symlink) und füge es inmkdocs.ymlunter „Entwicklung" ein:Falls
CHANGELOG.mdnicht existiert, erstelle einen initialen Eintrag mit dem aktuellen Doku-Update.
Zusammenfassung¶
| Kategorie | Anzahl | Priorität |
|---|---|---|
| Sicherheit | 2 | 🔴 Hoch |
| Performance | 3 | 🔴/🟡 Hoch/Mittel |
| MCP-Härtung | 3 | 🔴/🟡 Hoch/Mittel |
| API-Design | 1 | 🟡 Mittel |
| Code-Qualität | 1 | 🟡 Mittel |
| DX | 1 | 🟡 Mittel |
| Modernisierung | 1 | 🟢 Niedrig |
| Fehlende Features | 3 | 🟢 Niedrig |