Architecture Decision Record · Projektpartnerportal

Projektpartner-Profil-Datenmodell: drupal/profile statt User-Base-Fields

accepted 2026-06-15
TL;DR: Das Self-Service-Profil eines Projektpartners liegt als separate drupal/profile-Entity (Typ projektpartner) neben der User-Entity, nicht als Base-Fields am User. Die User-Entity bleibt reiner Login-Anker und trägt nur field_ppp_id (Bestands-/Salesforce-Referenz). Die Rollen-Trennung zu Redakteuren fällt dadurch natürlich raus (eigene Profile-Edit-Route statt hook_form_alter). D11-Kompatibilität ist empirisch bestätigt (Profile 1.14.0 + Entity 1.6.0 lösen gegen Core ^11.3.5 auf).

Kontext

Die Story P2503-226 baut die authentifizierte Self-Service-Schicht des Projektpartnerportals (Epic P2503-107). Ein registrierter Projektpartner pflegt selbst ein umfangreiches Fachprofil: Stammdaten (Anrede, Titel, Vor-/Nachname, Geburtsdatum, Firma), Kontakt (Telefon, Mobil, Website, Adresse), Verfügbarkeit (Einsatz-Regionen DE/AT/CH/international, Stundensatz, Verfügbarkeits-Status, Netzwerk-IDs für GULP, freelance.de, Xing, freelancermap), Qualifikation (Sprachen, Anschreiben, fachliche Schwerpunkte), DSGVO-Consent mit Timestamp und Version, sowie private, owner-only Dokument-Uploads.

Rund 37.700 Bestandspartner werden aus einem Neos-CMS/Flow-Altsystem migriert (Quelle: MySQL-Dump, Tabelle contenance_solcom_domain_model_user). Bestehende Redakteure und Admins (Rollen content_editor, content_manager) müssen ihr unverändertes Standard-Drupal-Benutzerformular behalten — das Partner-Profil darf deren Account-Verwaltung nicht berühren.

Drei Datenmodell-Topologien stehen zur Wahl: (A) eine separate drupal/profile-Entity, (B) alle Felder als Base-Fields direkt auf der User-Entity, (C) eine eigene Custom-Content-Entity. Die Entscheidung ist schwer reversibel — sie bestimmt das Schema, das alle Folge-Sub-Tasks (Registrierung, Self-Service-Edit, Account-Löschung, Migration) befüllen. Daher wurde sie vor jeglicher Implementierung über den customer-lens-reviewer geprüft.

Bis zu dieser Entscheidung war drupal/profile nicht im Stack (nicht in composer.json), und es existierte kein field_ppp_id und keine Rolle projektpartner in config/sync — das Datenmodell startet auf der grünen Wiese.

Entscheidung

1. Separate Profile-Entity vom Typ projektpartner

Das Fachprofil wird als drupal/profile-Bundle projektpartner modelliert, an die User-Entity gekoppelt (ein Profil pro User). Die Profile-Entity ist die single source of truth für die Profildaten — Migrationsziel, Views-Quelle, Permission-Anker. Die User-Entity bleibt reiner Login-Träger.

2. field_ppp_id bleibt auf der User-Entity

Die PPP-ID (numerische Bestands-Referenz, zugleich Salesforce-Verknüpfungsanker) liegt als field_ppp_id auf der User-Entity — nicht am Profil. Begründung: Sie ist ein Identitäts-Feld, das von Migration und Salesforce-Linking am Account erwartet wird (konsistent mit ADR-0048). Für Neuregistrierer ist das Feld zunächst leer (nullable), bis eine RMS-Übernahme erfolgt. Die bestehende Bewerbungs-Webform liest Profilfelder folglich über Profile::loadByUser(), nicht über User-Felder.

3. Rollen-Trennung über getrennte Routen, nicht über Form-Alter

Projektpartner erhalten die native Profile-Edit-Route (/user/{uid}/projektpartner); Redakteure und Admins behalten unverändert /user/{uid}/edit. Die Trennung ist damit eine strukturelle Invariante (zwei Entitäten, zwei Routen, zwei Permission-Sets) statt einer fragilen hook_form_FORM_ID_alter-Sichtbarkeitssteuerung. Damit ist Sub-Task P2503-234 weitgehend ohne Custom-Code erfüllt.

4. Erstanlage über Webform-Wizard, laufende Pflege über native Profile-Form

Die mehrstufige Selbst-Registrierung läuft als Webform-Wizard (Repo-konsistent mit solcom_vacancy_application); ein Final-Submit-Handler legt User + projektpartner-Profil + Rolle atomar an und hängt die in der Draft-Submission gesammelten Dateien an das frische Profil um. Die laufende Pflege nutzt dagegen die native Profile-Edit-Form, sodass die Feldliste genau einmal (als Profile-Fields) definiert ist und die Webform nur die Erstanlage-Teilmenge spiegelt.

5. Migration legt User und Profil koordiniert an

Die Migrate-API (SqlBase direkt gegen den Neos-Dump) fährt zwei verbundene Migrations: zuerst die User-Entity (inkl. field_ppp_id), dann — via migration_dependencies und migration_lookup — die projektpartner-Profile. Die Reihenfolge (User-first → Profile-second) ist verpflichtend.

Komponenten-Versionen (empirisch bestätigt)

drupal/profile  1.14.0   ← löst gegen drupal/core ^11.3.5 auf
drupal/entity   1.6.0    ← transitive Dependency von profile

Verifiziert per ddev composer require drupal/profile --dry-run am 2026-06-15. Damit ist die Vorbedingung des customer-lens-reviewer erfüllt (Option A wäre bei fehlender D11-Kompatibilität gekippt).

Abgelehnte Alternativen

Option B — Base-Fields direkt auf der User-Entity

Alle Felder via hook_entity_base_field_info am User, Rollen-Trennung via hook_form_FORM_ID_alter. Vorteil: kein neues Modul, einfachere Views auf 37.700 Datensätze. Abgelehnt, weil die Rollen-Trennung dann rein hook-basiert und fragil ist (jede Core-Änderung am User-Formular oder ein neues Field-Group-Contrib kann sie lautlos brechen), der Rollencheck an mehreren Stellen dupliziert werden muss (Form-Alter, Access-Callback, Migration), und Auth-Identität mit Fachdomäne auf einer Entity vermischt wird. Owner-only-Dokumente und DSGVO-Consent auf der Core-User-Entity erzwingen zudem über viele Hooks verstreute Custom-Access-Callbacks ohne zentrale Entity-Grenze als Sicherheitsanker.

Option C — eigene Custom-Content-Entity

Maximale Kontrolle, aber alle Core-Services (Permissions, Entity-Access, Caching, Views-Integration, Admin-Listing) müssten selbst gebaut werden — unverhältnismäßig zum Nutzen gegenüber Option A, das diese Services fertig mitbringt. Ein plausibler Anwendungsfall (mehrere Profile-Typen pro User mit komplexer Beziehung) liegt hier nicht vor. Abgelehnt.

Konsequenzen

  • Neue Composer-Dependencies: drupal/profile (1.14.0) + transitiv drupal/entity (1.6.0). Einzige neue Modul-Abhängigkeit des Vorhabens.
  • Neues Custom-Modul (Arbeitsname solcom_projektpartner) kapselt Profile-Typ-Config, Rolle projektpartner, Webform-Handler, Datei-Access-Logik und Migration.
  • field_ppp_id (Integer, unique, nullable) auf der User-Entity — konsistent mit ADR-0048, dort als „separater, späterer Task" vorgemerkt; dieser ADR realisiert die Verortung.
  • Migration koordiniert: User-Migration vor Profile-Migration, verkettet über migration_dependencies + migration_lookup. Ein Migrate-Fehler isoliert sich auf die Profile-Entity, statt die User-Entity zu korrumpieren.
  • DSGVO-Cascade-Delete (Pflicht-Test): Bei Account-Selbst-Löschung (Sub-Task P2503-233) muss verifiziert und per Test belegt sein, dass die projektpartner-Profile-Entity und alle privaten Dokument-Uploads (File-Entities + physische Dateien) mit-gelöscht werden. Profile-Default-Cascade darf nicht blind vertraut werden.
  • Owner-only-Datei-Access wird über einen Profile-Access-Handler bzw. hook_file_download mit Profil-Ownership-Check gelöst, nicht über User-Permissions allein (Sub-Task P2503-229).
  • Webform-Profile-Handler-Integration für D11 ist vor dem Wizard-Slice zu verifizieren (eigenständiger Profile-Webform-Handler vs. Custom-Submit-Handler).
  • Performance der Admin-Übersicht auf ~37.700 Profile-Entities (separate Tabelle, Views-Integration) ist ein eigenes Performance-Review wert (n+1-Risiko).

Offene Punkte

Migrations-Umfang (DSGVO / Fachbereich): Welche der ~37.700 Bestandspartner werden migriert? Technische Default-Linie: nur Datensätze mit erteiltem Datenschutz-Consent (policyoption=1) und aktiviertem Account. Das finale Kriterium muss Datenschutz/Vertrieb absegnen — nicht allein eine Architektur-Entscheidung.
Skill-Vokabular: Ob field_skills Freitext bleibt (wie im Neos-Altsystem, 1000 Zeichen) oder auf eine Taxonomie/Tags umgestellt wird, entscheidet das PRD. Betrifft die Migration der Suchagent-Kriterien und das spätere Projekt-Matching.
Admin-Theme-Integration: Ob die native Profile-Edit-Form sauber ins Admin-Theme (Gin?) und ggf. ins Front-End-Theme des Self-Service-Bereichs integriert, ist vor dem Edit-Slice zu prüfen.