solcom_sf_publication als schlankes Custom-Modul statt Drupal Salesforce Suite
web/modules/custom/solcom_sf_publication mit ~300 LoC: Guzzle-basierter Service-OAuth-Client (refresh_token-Flow), SOQL-Delta-Puller, Field-Mapper inkl. HTML-Sanitize, QueueWorker für Pull, Reconciliation und Outbound-URL-Push, plus Drush-Commands. Die Drupal Salesforce Suite (drupal/salesforce + ~15 Sub-Module) wird nicht installiert.
Kontext
Story F-290 verlangt einen unidirektionalen Pull von Projekt-Ausschreibungen (TR1__Job__c mit RecordType Publication) aus der FCSB-Sandbox in die SOLCOM-Website. Zusätzlich schreibt Drupal ein einziges Feld zurück: hohr__Job_Application_URL__c, gesetzt bei Publish und gelöscht bei Unpublish (siehe Context-0008). User-Authentifizierung über SF, User-Linking, Profile-Pull oder Merkliste sind nicht in dieser Story — Drupal-Login bleibt nativ (F-400, separate Story).
Damit ist der Scope für einen SF-Connector sehr eng:
- Ein SF-Objekt (
TR1__Job__c), ein RecordType (Publication), ein Drupal-Content-Type (project). - Service-OAuth via refresh_token (Long-lived, kein User-Login-Flow).
- Inbound: SOQL-Delta via
LastModifiedDate+ nightly Reconciliation-Sweep (siehe Context-0008). - Outbound: ein Feld, Push asynchron via QueueWorker.
Der etablierte Drupal-Weg für SF-Integrationen ist das Contrib-Modul drupal/salesforce („Salesforce Suite") mit ~15 Sub-Modulen: salesforce_mapping, salesforce_push, salesforce_pull, salesforce_logger, salesforce_jwt, salesforce_oauth … Es bietet eine umfangreiche Mapping-UI, Queues für Push/Pull, Webhook-Endpoints, JWT-Auth und sieht für jeden Drupal-Field-Typ einen Plugin-Slot vor.
Das Scope-zu-Suite-Verhältnis ist asymmetrisch: F-290 nutzt davon ~5% der Surface (genau einen Pull, einen Push, ein Mapping). Der Rest ist Footprint, der über die nächsten Jahre gepflegt, geupdatet und verstanden werden muss. Karpathy-Guideline: Minimum code that solves the problem. Nothing speculative.
Entscheidung
Wir bauen solcom_sf_publication als schlankes Custom-Modul. Die Drupal Salesforce Suite wird nicht installiert. Konkrete Modul-Struktur (folgt drupal-module-development-Konventionen):
web/modules/custom/solcom_sf_publication/
├─ solcom_sf_publication.info.yml
├─ solcom_sf_publication.module (hook_node_update → Queue-Enqueue)
├─ solcom_sf_publication.install (field_sf_id unique-index, content-type-Trigger)
├─ solcom_sf_publication.services.yml
├─ solcom_sf_publication.routing.yml (Admin-Status-Page)
├─ solcom_sf_publication.permissions.yml
├─ config/install/ (Default-Settings, Cron-Period)
├─ config/schema/
└─ src/
├─ Service/
│ ├─ SfClient.php (Guzzle, OAuth refresh, 429-Retry)
│ ├─ PublicationPuller.php (SOQL, Pagination via nextRecordsUrl)
│ ├─ PublicationMapper.php (SF-Felder → Drupal-Felder, Industry-Lookup)
│ ├─ RichTextSanitizer.php (Whitelist, force.com-Filter)
│ └─ ReconciliationService.php (Full-Id-Diff)
├─ Plugin/QueueWorker/
│ ├─ PullDeltaWorker.php (#[QueueWorker(cron: ['time'=>120])])
│ ├─ ProcessPublicationWorker.php (Item-für-Item Upsert)
│ ├─ ReconcileWorker.php
│ └─ PushUrlWorker.php
├─ EventSubscriber/ (optional: Logging, Health)
├─ Commands/
│ └─ SolcomSfCommands.php (drush solcom-sf:bootstrap|reconcile|push-urls)
└─ Controller/
└─ StatusController.php (/admin/reports/solcom-sf)
Operative Anker:
- Credential-Storage:
composer require drupal/key. Key-Provider Environment Variable. Env-VarsSF_FCSB_CLIENT_ID,SF_FCSB_CLIENT_SECRET,SF_FCSB_REFRESH_TOKEN. Lokal in.ddev/config.local.yaml(gitignored), produktiv in Mittwald-mStudio. Konsistent mit der Mittwald-Deployment-Linie aus ADR-0019. - Cron-Rhythmus: Delta-Pull 15 Min (QueueWorker mit
cron['time'=>120]), Reconciliation nachts 03:00. Bootstrap initial viadrush solcom-sf:bootstrap --dry-run, danach scharf. - Token-Cache: Access-Token wird im Drupal-State (oder Cache mit kurzer TTL) gehalten, on-401 verworfen und per refresh_token erneuert. Refresh-Token bleibt langlebig im Env.
- API-Version-Pin:
v66.0(siehe Context-0008), insolcom_sf_publication.settingseditierbar für Upgrades. - Retry/Backoff: 429-Response wird ausgewertet (
Retry-After-Header), 5xx mit exponentiellem Backoff bis 3 Versuche. - Logging: Eigener Logger-Channel
solcom_sf. Sync-Run-Metadaten instate.solcom_sf.last_pull_atundstate.solcom_sf.last_pull_count. - Sicherheit: Pflicht-Pattern aus
drupal-module-development(DI statt Service-Locator,accessCheck(TRUE)in Queries, RichTextSanitizer für SF-Rich-Text-Felder).
Rejected alternatives
Drupal Salesforce Suite (drupal/salesforce) komplett rejected
Bewährt, breite Surface (Mapping-UI, Push-/Pull-Queues, JWT, OAuth, Webhook-Endpunkte). Verworfen, weil das Scope-zu-Footprint-Verhältnis nicht passt: F-290 nutzt ~5% der Module. Maintenance- und Update-Last des großen Sub-Modul-Sets übersteigt den Mehrwert; Mapping-UI ersetzt nicht den Source-of-Truth-Vorteil von versioniertem PHP-Mapping; Plugin-Hooks würden für jede Anpassung (RichText-Sanitize, Lifecycle-Window-Logik, Window+Status-driven Publish-Status) durchgriffen werden müssen.
Reaktivierung der Entscheidung sinnvoll, wenn (a) ≥3 SF-Objekte synchronisiert werden, (b) Site-Builder das Mapping in der UI pflegen sollen, oder (c) bi-direktionale Sync für ≥2 Felder gewünscht ist.
Hybrid — Auth aus der Suite, Mapping/Push/Pull custom rejected
Idee: nur salesforce-Core inkl. salesforce_oauth für Token-Handling installieren, Mapping/Push/Pull custom bauen. Verworfen, weil die Suite ein integriertes Modul-Set ist — das Auth-Modul allein zieht die RestClient-Plumbing, Config-Entities, ein eigenes Logger-Modul und ein Set von Service-Definitionen mit. Die geretteten ~30 LoC OAuth-Refresh aus dem Custom-Service stehen einer halben Welt aus Suite-Configs/Services gegenüber.
Bestehende Drupal-Suite-Fork mit nur den nötigen Sub-Modulen vendoren rejected
Schneller Start, aber Patch-Stream gegen Upstream wird zur permanenten Last. Custom-Modul ist schlanker und nicht von Upstream-Releases abhängig.
Boomi-Webhook-Endpoint in Drupal statt Pull rejected
Excel-Header („Boomi Canonical Data Model") und Sample-Payload legen einen Pfad SF → Boomi → Drupal-Webhook nahe. Verworfen für Phase 1, weil (a) Kunde Drupal direkte FCSB-OAuth-Creds gegeben hat (Indikator: Pull-Wunsch), (b) Boomi-Verfügbarkeit/-Verantwortung nicht in SOLCOMs Scope. Detail-Begründung siehe ADR-0027.
Consequences
- OAuth-Refresh-Flow ist SOLCOM-Verantwortung. Kein Suite-Polster — Token-Lifecycle, 401-Behandlung und Refresh-Logik leben in
SfClient. Dafür: voll testbar (Guzzle-Mocks), keine versteckte Magic. - Mapping ist Code, kein UI-Konstrukt. SF-Felder ↔ Drupal-Felder ist im
PublicationMapperversioniert. Pull-Request-Reviews erfassen Mapping-Änderungen direkt. Site-Builder können das Mapping nicht in der Drupal-UI ändern (Tradeoff — aber gewollt, weil Mapping eine Spezifikation des Kunden ist, kein Site-Builder-Knob). - Kein Out-of-the-box Multi-Object-Sync. Wenn F-330 (Minggo) oder ein zweites SF-Objekt anfällt, wird der Connector erweitert — vermutlich mit Refactor in Richtung „SfClient + Object-spezifische Puller". Diese Refactor-Schwelle ist akzeptiert.
- Drupal/Key wird eingeführt. Composer-Dependency und Config-Sync-Schema für
key.key.*. Konsistent mit der Mittwald-Env-Var-Discipline. Andere zukünftige Integrationen können dasselbe Pattern nutzen. - Tests sind Pflicht. Unit (Mapper, Sanitizer, Date-Parsing), Kernel (Service-Wiring, Drush-Command), Functional (hook-trigger → Queue → assert). „No tests = PR-rejection" gilt (CLAUDE.md).
- Doku-Anker entstehen automatisch. Modul-README, ADR-0026 (dieses), ADR-0027, Context-0008. Kein zusätzlicher Skill nötig — das Wissen ist anker-bar im Modul-Kontext.
- Bei Drift wird konsolidiert. Sollte ein zweiter SF-Konsument entstehen (Marketing-Cloud, Lead-Webhook), wird dieser ADR revisited und entweder (a) der zweite Konsument ebenfalls custom mit eigenem Auth gebaut, oder (b) ein gemeinsamer
solcom_sf-Auth-Service extrahiert. Entscheidung fällt zu dem Zeitpunkt, nicht spekulativ jetzt.