Refactoring der ZMS PHP Backends in Spring RefArch
Einführung
Teil der produitorientierten RefArch-Roadmap: das PHP-Admin-Backend wird durch einen Spring-Boot-Service ersetzt, der dasselbe MySQL-Schema wie das künftige Citizen-Backend nutzt.
Status (GH-2604): Admin-API und Datenbankzugriff liegen bereits in PHP
zmsbackend(Slim, vertikale Slices untersrc/Zmsbackend/<Domain>/). Diese Seite beschreibt den nächsten Schritt — dasselbe Domain-Layout auf dem RefArch-Spring-Stack. Der Namezmsbackendwird wiederverwendet; unten bedeutet PHPzmsbackenddas aktuelle Modul, Springzmsbackendden Ziel-Service.
| Ziel-Service | Ersetzt (heute) |
|---|---|
Spring zmsbackend | PHP zmsbackend (Admin-REST-API + DB; zusammengeführt aus zmsapi/zmsdb), serverseitige zmsentities-Nutzung |
Spring zmscitizenbackend | PHP zmscitizenapi |
zmsentities bleibt im Monorepo als gemeinsame Vertragsschicht: JSON-Schemas, Validierungshilfen und typisierte Objekte. Frontend-Module wie zmsadmin und zmsstatistic hängen weiter daran für API-Antwortformen und clientseitige Validierung.
Spring zmsbackend
Ersetzt PHP zmsbackend (und historisch die früher getrennten Pakete zmsdb + zmsapi) durch einen Spring-Boot-Service.
Heute (PHP)
| Paket / Modul | Rolle |
|---|---|
zmsentities | Schema-basierte Domain-Objekte (Department, Scope, …), Validierung, Collections |
zmsbackend | Einheitliche Admin-REST-API + SQL-Schicht — Api/, Service/, Repository/, Exception/ pro Domain (GH-2604) |
Vor GH-2604 lagen dieselben Verantwortlichkeiten in getrennten zmsapi- (HTTP) und zmsdb- (Queries) Composer-Paketen; die Code-Explorer unten zeigen das aktuelle PHP-zmsbackend-Layout.
Ziel (Spring)
Jede Domain erhält einen vertikalen Schnitt: Pakete api/, model/, repository/, service/, view/ und exception/. Services sind nach Operation getrennt (Fetch, Create, Update, Delete) statt einer Klasse pro PHP-Modul.
API-Typen liegen in view/ und bleiben kompatibel zu zmsentities-JSON-Schemas (z. B. department.json) für Frontends — Spring zmsbackend führt aber keine JSON-Schema-Validierung aus. Validierung erfolgt in Java auf view/-Typen (RefArch-Validatoren). JPA-Typen in model/ mappen auf umbenannte DB-Tabellen (siehe Datenbank-Refactor).
Warum Spring zmsbackend?
PHP zmsbackend hat die zmsdb- vs. zmsapi-Trennung bereits entfernt (GH-2604). Vorteile, dieses vereinheitlichte Domain-Modell auf Spring Boot auf dem RefArch-Stack zu bringen:
Validierung an einem Ort, weniger Verwirrung — Heute liegen Regeln verteilt in Controllern, Mellon-Request-Parsing, Opis-JSON-Schema-Dateien,
Entity::testValid()und Ad-hoc-Checks. Schemas sind zu großen, schwer wartbaren JSON-Dateien mitoneOf-Zahl/String-Vereinigungen und$ref-Ketten gewachsen, die kaum noch jemand vollständig durchschaut. Inzmsbackendvalidiert jede Domain invalidation/gegen ihreview/-Typen — ein klarer Ort zum Nachschlagen und Ändern.Ein Dependency-Baum statt drei Composer-Projekte —
zmsdb,zmsapiund gemeinsame PHP-Bibliotheken bedeuten mehrerecomposer.json, einen großen transitiven Abhängigkeitsgraphen und ständige Dependabot/Renovate-PRs über Pakete hinweg.zmsbackendist ein Maven-pom.xml, ein Backend-Artefakt, ein Upgrade-Pfad.Auf RefArch aufbauen statt Eigenbau — refarch-templates für CI/CD, Container-Builds, Keycloak-Anbindung und Gateway-Muster nutzen, statt eigene GitHub Actions zu pflegen, die häufig brechen. Spring-Boot-Images lassen sich typisch deutlich schneller bauen als große PHP-Basis-Images (
zmsbase). In München gibt es viele Java-Entwickler; PHP-Backend-Know-how ist rar und teuer in der Pflege.Ein Service, ein Mentalmodell — Kein „liegt das in
zmsdboderzmsapi?“ mehr, kein Durchreichen von Entities durch drei Pakete für einen HTTP-Call. Fachlogik sitzt im vertikalen Schnitt (api/→service/→repository/→model/).Stärkere Typen, weniger Laufzeitüberraschungen — Java-
model/- undview/-Typen ersetzen schema-basierte PHP-ArrayObject-Entities. Refactorings und API-Änderungen fallen dem Compiler und der IDE auf, nicht erst zur Laufzeit oder in Integrationstests.Standard-Persistenz und Migrationen — JPA-Repositories und RefArch-Standard für DB-Migrationen ersetzen handgeschriebene Query-Klassen und verstreutes SQL. Passt zum Datenbank-Refactor (klare Tabellen- und Spaltennamen im Code).
Betrieb und Sicherheit out of the box — Spring Boot Actuator, Micrometer-Metriken, strukturiertes Logging und RefArch-Sicherheitsmuster (Keycloak, API-Gateway) entsprechen dem Betrieb anderer Münchner IT-Produkte — kein Einzelstück-PHP-Stack.
Schnellere, sicherere Auslieferung — JUnit und Spring Boot Test für Unit-/Integrationstests; ATAF und REST Assured in
zmsautomationbereits im Einsatz. Kleinere, reproduzierbare Container-Images verkürzen Deploy-Zeiten und reduzieren „läuft nur in meinem DDEV“-Drift.Einfachere Wiederverwendung und Einarbeitung — Gleicher Stack wie bürgerorientierte RefArch-Komponenten (
refarch-gateway,zmscitizenview). Neue Teammitglieder und andere Städte folgen dokumentierten RefArch-Konventionen statt ZMS-spezifischer PHP-Paketgrenzen.RefArch-API-Gateway mit Keycloak-SSO für interne Frontends — Das RefArch-Spring-API-Gateway bringt Keycloak-Login mit. Sobald
zmsadminundzmsstatisticauf Vue/RefArch-Frontends umgestellt sind, authentifizieren sie sich über dieses Gateway — wiezmscitizenviewes bereits tut — statt einen eigenenzmsclient-OAuth/Keycloak-Flow und Login-Controller in jedem PHP-Frontend-Modul zu pflegen.Kein
zmsslim-Routing-Framework mehr — Heute bindetzmsapi(und andere PHP-Module) HTTP-Routen überrouting.php, Slim-Middleware (Route,OAuthMiddleware, …) undBaseController-Muster auszmsslim. Spring Boot mappt Endpoints mit@RestController/@RequestMapping(oder RefArch-Route-Registries wieDepartmentRouteRegistry) — Standard-Spring-MVC, IDE-freundlich, kein eigener Slim-Bootstrap mehr zu pflegen.SLF4J/Logback statt zentralisiertem Monolog in
zmsslim— PHP-Backends loggen überApp::$log, einmal verdrahtet in **zmsslim**sBootstrap::configureLogger()(Monolog, JSON nach stdout). Spring Boot nutzt kein Monolog; es bringt SLF4J + Logback und RefArch-Logging-Konfiguration für strukturiertes JSON mit — der gemeinsame Monolog-Bootstrap und PSR-3-Plumbing entfallen im Backend-Stack.Keine riesige
routing.php— Routen sitzen an den Controllern — Heute istzmsapi/routing.php~6.600 Zeilen URL-zu-Controller-Zuordnungen in einer Datei, fern vom Handler-Code. Inzmsbackendsteht jeder Endpoint am@RestController(@GetMapping,@PostMapping, …) direkt neben der Handler-Methode — die IDE springt von der Route zur Implementierung, und Domain-Slices bleiben in sich geschlossen.Keine eigene Swagger-Pipeline mehr pflegen — Heute betreiben
zmsapiundzmscitizenapijeweils eine eigene Doc-Toolchain:@swagger-Blöcke inrouting.php,build_swagger.js,swagger-jsdoc, YAML-Partials unterpublic/doc/, npm-Skripte (npm run doc) und CI-Schritte zum Bündeln vonswagger.jsonund Ausliefern von Swagger-UI-Assets. Das muss pro PHP-API funktionsfähig bleiben.zmsbackendundzmscitizenbackendnutzen Spring OpenAPI (springdoc-openapi im RefArch-Stack): Controller annotieren, lokal starten, Swagger UI unter/swagger-ui.htmlöffnen und Endpoints interaktiv testen — kein separates npm-Build oder handgebauter Doc-Generator pro Service.Domain-Controller statt ~166 Einzelaktions-Klassen —
zmsapiimplementiert fast jede HTTP-Aktion als eigene PHP-Klasse (DepartmentGet,DepartmentList,ProcessFree, …), jeweils mit wiederholtem Mellon-Parsing, Berechtigungsprüfung,Response\Message::create()und Übergabe anzmsdb. Inzmsbackendbündelt ein@RestControllerpro Domain verwandte Endpoints, teilt Security und Exception-Handling und hält Request/Response-Mapping neben der Service-Schicht — weniger Copy-Paste und deutlich weniger Dateien, wenn ein Feature geändert wird.
Beispiel: Department (behoerde → department)
Illustratives Spring-Boot-Layout. Tabellenumbenennung: behoerde → department.
Ordnerstruktur
src/main/java/de/muenchen/zms/department/
├── api/ # heute: zmsbackend Department/Api + routing.php
├── exception/
├── model/ # heute: zmsbackend Repository/ Tabellen-Mappings (behoerde → department)
├── repository/ # heute: zmsbackend Repository/* + readByDepartmentId-Helfer
├── service/ # ein Service pro Operation (nicht eine Klasse pro PHP-Modul)
├── validation/ # RefArch: imperative Validatoren auf view/-Typen
└── view/ # API-Payloads (Form kompatibel zu zmsentities-Schemas)PHP heute → Java Ziel (vollständiger Department-Slice)
PHP (heute — zmsbackend) | Java (Spring-Ziel) |
|---|---|
zmsbackend/routing.php (/department/*, …) | api/DepartmentRouteRegistry + Controller |
Department\Api\DepartmentGet | api/DepartmentController.getDepartment |
Department\Api\DepartmentList | api/DepartmentController.listDepartments |
Department\Api\DepartmentUpdate | api/DepartmentController.updateDepartment |
Department\Api\DepartmentDelete | api/DepartmentController.deleteDepartment |
Department\Api\DepartmentAddScope | api/DepartmentController.addScope |
Department\Api\DepartmentAddCluster | api/DepartmentController.addCluster |
Organisation\Api\OrganisationByDepartment | api/DepartmentController.getOrganisation |
Department\Api\DepartmentWorkstationList | api/DepartmentController.listWorkstations |
Organisation\Api\OrganisationAddDepartment | api/OrganisationDepartmentController.addDepartment |
Department\Api\DepartmentByScopeId | api/ScopeDepartmentController.getDepartmentByScope |
Useraccount\Api\UseraccountListByDepartments | api/DepartmentUseraccountController |
Useraccount\Api\UseraccountListByRoleAndDepartments | api/DepartmentUseraccountController |
Department\Service\Department + Department\Repository\Department | model/, repository/DepartmentRepository |
Link, Dayoff, Scope, Cluster, Workstation, Organisation Domains | passende repository/Department* |
Useraccount\Service\Useraccount (Department-Queries) | repository/DepartmentUseraccountRepository |
zmsentities\Department + department.json | view/DepartmentView |
zmsentities\Schema\Validator + Department::testValid() | validation/ValidateDepartment |
| — | validation/DepartmentValidationService |
Heute — PHP-Stack durchklicken
Den vollständigen Department-Slice im Monorepo: zmsentities-Schema und -Entity, PHP-zmsbackend-Department/- und verwandte Domain-Klassen (Service/, Repository/, Api/) sowie den routing.php-Auszug mit jedem Department-Endpoint.
Ziel — Spring-Boot-Modul (Spring zmsbackend)
Das vollständig übersetzte Modul durchklicken: api/-Controller für jeden Endpoint, repository/-Klassen für jede Query-Schicht, model/-JPA-Entities, view/-API-Typen, service/ (ein Service pro Operation) und validation/-Java-Validatoren (kein JSON Schema auf dem Server).
Explorer-Daten nach PHP- oder Java-Änderungen neu erzeugen: npm run docs:department-explorers in docs/.
Frontends wie zmsadmin und zmsstatistic behandeln API-Payloads weiter als zmsentities-Typen; nur der erzeugende Backend-Service ändert sich.
zmscitizenbackend
Eigenes bürgerorientiertes Backend (heute: PHP-Modul zmscitizenapi).
Heute
| Paket | Rolle |
|---|---|
zmscitizenapi | Controller, Services, Citizen-Modelle (Office, Service, ThinnedScope, …) |
ZmsApiClientService | HTTP-Client zu PHP zmsbackend — lädt volle zmsentities-Graphen (Provider, Scope, Process, …) |
ZmsApiFacadeService | Orchestriert mehrere PHP-zmsbackend-Aufrufe, merged Listen, Second-Level-Cache |
MapperService | Mappt große zmsentities-Payloads auf schlanke Citizen-Modelle |
zmsentities | Teilweise gemeinsame Typen; Citizen-Modelle überwiegend separat |
Kerndaten werden über HTTP-Aufrufe an PHP zmsbackend geladen, nicht über eine eigene Query-Schicht. Typische Flows holen überdimensionierte Admin-Entitäten, dann projiziert MapperService sie in Office, ThinnedScope, Service und ähnliche Citizen-DTOs — oft nach mehreren Roundtrips über ZmsApiFacadeService (~900 Zeilen heute).
Ziel
zmscitizenbackend behält eigene bürgerorientierte Modelle (Office, Service, ThinnedScope, …) in model/ und view/. Es ruft PHP zmsbackend nicht per HTTP auf, sondern nutzt eine eigene Repository-Schicht — SQL- (bzw. JPA-) Queries im Besitz des Citizen-Backends, gegen das gemeinsame MySQL-Schema.
Gleiches Vertical-Slice-Layout wie zmsbackend: api/, model/, repository/, service/, view/, exception/ pro Domain (z. B. office/, thinnedprocess/, availability/).
Citizen-Modelle bleiben schlank und API-spezifisch. Sie müssen zmsentities-Schemas nicht eins zu eins abbilden; zmsbackend liefert bei Bedarf vollständigere, schema-kompatible Payloads.
Warum zmscitizenbackend?
Vorteile, zmscitizenapi und den PHP-zmsbackend-HTTP-Client-Stack durch einen Spring-Boot-Service mit eigener Persistenz zu ersetzen:
Kein Mapping mehr: kleine Citizen-Modelle aus riesigen Admin-Entitäten — Heute durchläuft
MapperService(~640 Zeilen) vollezmsentities-Graphen (Provider,Scope,Process,Request, …) und kopiert Felder manuell inOffice,ThinnedScope,ThinnedProcessusw. Inzmscitizenbackendladen Repositories undview/-Typen nur das, was die Citizen-API exponiert — Mapping ist Query-Design, keine wartungsintensive Übersetzungsschicht.Weg mit dem zweiten API-Hop — Jeder Citizen-Lese-/Schreibzugriff läuft heute
zmscitizenapi→ HTTP → PHPzmsbackend→ MySQL, mit JSON en-/decodieren auf beiden Seiten.zmscitizenbackendspricht direkt mit der Datenbank (service/→repository/→model/). Weniger Netzwerk-Hops, weniger Serialisierung, geringere Latenz auf Buchungs- und Verfügbarkeits-Hotpaths.Maßgeschneiderte Queries statt „alles laden, in PHP filtern“ —
ZmsApiFacadeServiceholt oft komplette Provider- und Scope-Listen von PHPzmsbackend, merged sie im Speicher und cached das Ergebnis. Citizen-Backends können fokussiertes JPA/SQL (Joins, Projektionen, Pagination) für Offices-by-Service, verfügbare Tage und Slots nutzen — weniger Daten, weniger CPU fürs DTO-Formen.Weniger Klebe-Code —
ZmsApiClientService,ZmsApiFacadeServiceundMapperServicehängen eng an PHP-zmsbackend-Routen undzmsentities-Formen. Schema- oder Endpoint-Änderungen in der Admin-API wirken in Citizen-Mapping und Cache-Keys nach. Eigene Repositories entkoppeln den öffentlichen Citizen-Vertrag von der internen Admin-API-Evolution.Einfachere Cache-Strategie — Second-Level-Caches (
processed_offices,processed_scopes, …) amortisieren vor allem HTTP + Mapping. Direkte Reads machen Caching optional und gezielt (z. B. heiße Office-Listen) statt Pflicht für akzeptable Antwortzeiten.Weniger Fehlerquellen für Bürger:innen — Citizen-Buchung hängt nicht mehr davon ab, dass PHP
zmsbackendunter Admin-Last (Reports, Massenbearbeitung, Statistik) schnell und verfügbar bleibt.zmscitizenbackendskaliert und fällt auf seinen Lese-/Schreibpfaden unabhängig aus.Gleicher RefArch-Stack wie
zmsbackendundrefarch-gateway— Ein Maven-Projekt, JUnit/Spring Boot Test, Actuator-Metriken, gemeinsame Münchner CI/Container-Muster — kein separates PHP-Modul plus HTTP-Client-Konfiguration (ZMS_API_URL,zmsclient-artige Plumbing).Klare Verantwortung für die Citizen-Domain — Vertical Slices (
office/,thinnedprocess/,availability/) ersetzen eine monolithische Fassade. Jedes Feature besitzt API, Service, Repository und View statt neue Zweige in gemeinsamen Mapper-/Facade-Klassen.Hotpaths gezielt optimierbar — Verfügbarkeits- und Reservierungsflows können eigene Read-Modelle und Indizes bekommen, ohne neue PHP-
zmsbackend-Endpoints oder aufgeblähte Admin-Entitäten, die Frontends nie sehen.Einfacher testbar — Repository- und Service-Tests gegen das gemeinsame Schema ersetzen aufwändiges Mocken von
ZmsApiClientService-HTTP-Antworten und Mapper-Randfällen; ATAF/REST Assured können einen Citizen-Spring-Service End-to-End ansprechen.
Beispiel: ThinnedProcess (Citizen-Buchungs-Slice)
Illustratives Spring-Boot-Layout für die bürgerorientierte ThinnedProcess-Domain — den öffentlichen API-Typ, den Bürger:innen und zmscitizenview bereits nutzen. Tabelle heute: buerger (künftige Umbenennung: process — siehe Datenbank-Refactor).
Benennung: ThinnedProcess, nicht Appointment
PHP zmscitizenapi mischt Namen: Controller heißen Appointment*Controller, jede Antwort ist aber ein ThinnedProcess. In zmscitizenbackend heißt der Vertical Slice durchgängig thinnedprocess/ — ThinnedProcessController, ThinnedProcessFetchService, ThinnedProcessView — damit Code und Citizen-Vertrag zusammenpassen.
- HTTP-Pfade bleiben
/appointment,/reserve-appointment, … — unverändert fürzmscitizenviewund bestehende Clients. - Alle Slice-Typen nutzen das Präfix
ThinnedProcess— … — keine Verwechslung mit Admin-zmsentities\Processoder PHPzmsbackend\Process. - JPA-Typen bleiben
ThinnedProcessRecordaufbuerger— nur Persistenzschicht; nicht als Admin-zmsentities\Process-Graph exponiert.
Ordnerstruktur
src/main/java/de/muenchen/zms/citizen/thinnedprocess/
├── api/ # heute: zmscitizenapi Appointment*Controller + routing.php
├── exception/
├── model/ # JPA → buerger (ThinnedProcessRecord); nicht das öffentliche ThinnedProcess-DTO
├── repository/ # heute: ZmsApiClientService HTTP → PHP zmsbackend Process/Api/*
├── service/ # heute: Appointment*Service, ZmsApiFacadeService, MapperService
├── validation/
└── view/ # ThinnedProcessView — Citizen-API-PayloadPHP heute → Java Ziel (voller ThinnedProcess-Slice)
| PHP (heute) | Java (Ziel) |
|---|---|
zmscitizenapi/routing.php (/appointment, /reserve-appointment, …) | api/ThinnedProcessController, api/ThinnedProcessListController |
AppointmentByIdController | api/ThinnedProcessController (GET /appointment) |
AppointmentReserveController | api/ThinnedProcessController (POST /reserve-appointment) |
AppointmentUpdateController | api/ThinnedProcessController (POST /update-appointment) |
AppointmentConfirmController | api/ThinnedProcessController (POST /confirm-appointment) |
AppointmentPreconfirmController | api/ThinnedProcessController (POST /preconfirm-appointment) |
AppointmentCancelController | api/ThinnedProcessController (POST /cancel-appointment) |
MyAppointmentsController | api/ThinnedProcessListController (GET /my-appointments) |
AppointmentByIdService | service/ThinnedProcessFetchService |
AppointmentReserveService | service/ThinnedProcessReserveService |
AppointmentUpdateService | service/ThinnedProcessUpdateService |
AppointmentConfirmService | service/ThinnedProcessConfirmService |
AppointmentPreconfirmService | service/ThinnedProcessPreconfirmService |
AppointmentCancelService | service/ThinnedProcessCancelService |
MyAppointmentsService | service/ThinnedProcessListService |
ZmsApiFacadeService::getThinnedProcessById | service/ThinnedProcessFetchService + service/ThinnedProcessAccessService |
ZmsApiClientService::getProcessById (+ authentifizierte Variante) | repository/ThinnedProcessRepository, repository/ThinnedProcessQueryRepository |
ZmsApiClientService::reserveTimeslot, submitClientData, Status-POSTs | service/ThinnedProcessReserveService, ThinnedProcessWriteSupport, … |
MapperService::processToThinnedProcess | service/ThinnedProcessAssembler |
MapperService::thinnedProcessToProcess | service/ThinnedProcessWriteSupport (nur Schreibpfad) |
ValidationService (Process-ID, Auth-Key, not found) | validation/ThinnedProcessValidationService, ValidateThinnedProcessAccess |
ThinnedProcess + citizenapi/thinnedProcess.json | view/ThinnedProcessView (Java-Validierung; Schema bleibt für Frontends) |
ThinnedScope | view/ThinnedScopeView |
| — | model/ThinnedProcessRecord (JPA → buerger) |
| — | repository/ThinnedProcessProjection (SQL-Join → nur Citizen-Felder) |
Heute — PHP-Stack durchklicken
Durchsuchen Sie den vollen Appointment- / ThinnedProcess-Slice über zmscitizenapi (Citizen-Schemas, ThinnedProcess-Modell, Appointment*-Controller/Services, MapperService / Facade / Client) und PHP zmsbackend (Process/Api/*-Controller und routing.php-Auszug — die HTTP-Schicht, die ZmsApiClientService aufruft, plus Process/Service und Process/Repository auf buerger). Zeigt den zmscitizenapi → PHP zmsbackend-Hop, bevor riesige Process-Entitäten in ThinnedProcess gemappt werden.
Ziel — Spring-Boot-Modul in zmscitizenbackend
Durchsuchen Sie das vollständig übersetzte thinnedprocess/-Modul: ThinnedProcessController (URL-Pfade unverändert), repository/ mit Join-Projection, ThinnedProcessView, ThinnedProcess*Service pro Operation und ThinnedProcessAssembler statt MapperService.
Explorer-Daten nach PHP- oder Java-Änderungen neu erzeugen: npm run docs:thinned-process-explorers in docs/.
Citizen-Frontends (zmscitizenview) konsumieren weiter ThinnedProcess-JSON; das citizenapi/thinnedProcess.json-Schema in zmsentities bleibt der Vertrag — nur das produzierende Backend ändert sich.
Migrationshinweise
- Domain für Domain — zuerst
Department*-Endpoints auf Springzmsbackendmigrieren; PHP-zmsbackend-Controller bis Parity-Tests behalten. - Eine Datenbank — beide Backends lesen dasselbe Schema; Tabellenumbenennungen laufen über Migrationen (siehe Datenbank-Refactor-Dokument).
zmsentities-Schemas für Frontends behalten —zmsbackendvalidiert mit Java aufview/-Typen, kein JSON Schema in Spring.- Paket-Layout — pro Domain auf beiden Backends:
api/→service/→repository/→model/, API-Typen inview/.
Verwandt: ZMS-Architektur modernisieren (3–5-Jahresplan) · Datenbanktabellen- und Feldbenennung standardisieren