Projektpartner-Profil-Datenmodell: drupal/profile statt User-Base-Fields
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) + transitivdrupal/entity(1.6.0). Einzige neue Modul-Abhängigkeit des Vorhabens. - Neues Custom-Modul (Arbeitsname
solcom_projektpartner) kapselt Profile-Typ-Config, Rolleprojektpartner, 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_downloadmit 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
policyoption=1) und aktiviertem Account. Das finale Kriterium muss Datenschutz/Vertrieb absegnen — nicht allein eine Architektur-Entscheidung.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.