No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Moritz Sauer 0a8456dc91 HTTP-Aufrufe mit Zeitlimits
DhlConnection und AccessTokenProvider holen den Client aus der
HttpClientFactory des Basismoduls statt ueber die Discovery. Vorher gab es
weder Verbindungs- noch Antwortlimit: ein haengender DHL-Aufruf hielt den
CMS-Request bis zum Abbruch durch den Webserver fest.
2026-09-17 10:03:19 +02:00
_config Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
lang Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
src HTTP-Aufrufe mit Zeitlimits 2026-09-17 10:03:19 +02:00
tests/php Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
.gitattributes Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
.gitignore Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
composer.json Composer: Guzzle als echte Abhaengigkeit 2026-09-17 10:02:55 +02:00
LICENSE DHL Paket ueber die Parcel DE Shipping API v2 2026-08-05 16:19:01 +02:00
phpcs.xml.dist Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
phpstan.neon.dist Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
phpunit.xml.dist Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00
README.md Befunde aus dem Code-Review behoben, Testsuite ergaenzt 2026-08-06 10:38:08 +02:00

tietge/silverstripe-shipping-dhl

DHL Paket für tietge/silverstripe-shipping — Etiketten über die Parcel DE Shipping API v2.

Baut auf dem offiziellen SDK dhl/sdk-api-parcel-de (Netresearch) auf, mit zwei Umgehungen, die weiter unten begründet sind.

Installation

composer require tietge/silverstripe-shipping-dhl
vendor/bin/sake db:build --flush

Braucht einen PSR-18-Client — php-http/guzzle7-adapter genügt.

Zugangsdaten

Nach app/_config/shipping.yml (gitignored, Vorlage in shipping.yml.dist):

Tietge\Shipping\Dhl\DhlCarrier:
  sandbox: true
  api_key: ''            # aus der App im Developer Portal
  api_secret: ''         # dito
  gkp_user: ''           # Benutzer des Geschäftskundenportals
  gkp_password: ''
  ekp: ''                # 10-stellige Kundennummer
  billing_numbers:
    V01PAK: '0101'
    V53WPAK: '5301'

Sandbox-Testzugang (öffentlich dokumentiert): user-valid / SandboxPasswort2023!, EKP 3333333333. Ersetzt nicht den API-Key — der stammt aus der eigenen App und wird auch in der Sandbox gebraucht. Ohne ihn: „Invalid client identifier".

Wer die Geheimnisse auch aus der (gitignoreten) YAML heraushalten will, nutzt Silverstripes Backtick-Syntax und legt sie in die .env:

  api_secret: '`SS_DHL_API_SECRET`'
  gkp_password: '`SS_DHL_GKP_PASSWORD`'

Das Entwicklerkonto auf developer.dhl.com ist Selbstbedienung. Der Produktivzugang ist ein separater Antrag; erst dafür braucht es GKP-Benutzer und echte Abrechnungsnummern.

Die Abrechnungsnummer ist die häufigste Fehlerquelle

Sie hat 14 Stellen: EKP (10) + Verfahren (2) + Teilnahme (2). Es gibt eine Nummer je Produkt, und die API prüft beides gegeneinander — V01PAK mit einer 53er-Nummer wird abgewiesen.

Das Verfahren sind immer zwei Ziffern, die Teilnahme darf Buchstaben tragen: DHLs Schema gibt \w{10}\d{2}\w{2} vor und führt 333333333362aa ausdrücklich als gültiges Beispiel. Selten, aber es kommt vor.

Verfahren Produktcode Produkt
01 V01PAK DHL Paket (national)
53 V53WPAK DHL Paket International
54 V54EPAK DHL Europaket
62 V62KP DHL Kleinpaket
66 V66WPI Warenpost International

Das Modul prüft die Zuordnung selbst, bevor ein Aufruf hinausgeht — eine deutsche Meldung statt einer Ablehnung, die wie ein Zugangsproblem aussieht.

Nicht V62WP verwenden. Warenpost national wurde zum 01.01.2025 durch DHL Kleinpaket ersetzt; die stillschweigende Übersetzung alter Anfragen endete am 31.05.2026. Produktcodes sind bei DHL keine Konstanten, sondern vertraglich terminierte Werte.

Diese Liste bestimmt zugleich, welche Produkte im CMS zur Auswahl stehen — kein zweiter Schalter, keine Möglichkeit, ein Produkt anzubieten, für das kein Vertrag existiert.

Weitere Schalter

  profile: 'STANDARD_GRUPPENPROFIL'  # DHLs Berechtigungskonstrukt, jede Anfrage braucht es
  auth_mode: 'ropc'                  # oder 'basic' — ein dritter Wert wird abgewiesen
  must_encode: false                 # true = kein Etikett bei irgendeiner Beanstandung
  notify_recipient: false            # true = E-Mail der Empfängerin an DHL übergeben

Das Etikettenformat steht im CMS unter Website-Einstellungen → Shop → Versand → DHL Paket.

notify_recipient ist voreingestellt aus

DHL nutzt die E-Mail-Adresse der Empfängerin für die Paketankündigung, verschickt darüber also selbst eine Nachricht. Die Adresse dafür weiterzugeben ist eine Weitergabe an einen Dritten zu dessen eigenem Versand und setzt eine Einwilligung voraus — die kann nur der Shop einholen, nicht dieses Modul. Wer sie hat, schaltet ein.

Die Absenderadresse geht unabhängig davon immer mit: sie ist die eigene, und DHL meldet darüber Probleme mit der Sendung.

auth_mode wird geprüft

Weder getMissingSettings() noch die Verbindung akzeptieren einen dritten Wert. Das ist kein Formalismus: Ein Tippfehler würde sonst lautlos auf den abgekündigten Basic-Auth-Weg führen. Es liefe alles, das CMS meldete „eingerichtet", und der Ausfall käme an dem Tag, an dem DHL abschaltet.

Protokollierung ist aus — mit Absicht

Das SDK hängt an den übergebenen Logger einen LoggerPlugin mit FullHttpMessageFormatter: vollständige Anfrage und Antwort, alle Kopfzeilen, Körper ohne Längenbegrenzung, auf Stufe info. In der Anfrage steht an dieser Stelle noch die Basic-Auth-Kopfzeile des SDK — der BearerAuthClient ersetzt sie erst darunter. Der allgemeine Projektlogger würde hier also bei jedem Etikett das GKP-Passwort und ein mehrere Megabyte großes Base64-PDF in die Logdatei schreiben.

Deshalb bekommt das SDK einen eigenen Dienst, voreingestellt als NullLogger. Zum Mitschneiden bei der Fehlersuche in app/_config/:

SilverStripe\Core\Injector\Injector:
  Psr\Log\LoggerInterface.dhlShipping:
    class: Monolog\Logger
    constructor: [ 'dhl-shipping' ]
    calls:
      - [ pushHandler, [ '%$Monolog\Handler\StreamHandler' ] ]

Und danach wieder herausnehmen. Das Protokoll enthält Zugangsdaten.

Zwei Umgehungen am SDK — und warum

1. Das SDK verwirft die Beanstandungen. CreateShipmentResponseMapper überträgt nur Sendungsnummer und Etikett; items[].validationMessages hat im öffentlichen ShipmentInterface kein Gegenstück. Ohne Gegenmaßnahme bliebe LabelResult::getWarnings() bei DHL dauerhaft leer — und damit ausgerechnet beim einzigen Dienst, der solche Hinweise liefert, die Unterscheidung zwischen „glatter Erfolg" und „gedruckt, aber nicht zustellbar" ausgeschaltet. ResponseRecorder schneidet die Rohantwort mit; das ist der einzige Weg ohne Fork.

2. Das SDK kennt nur Basic Auth. HttpServiceFactory verdrahtet sie fest. DHL hat Basic Auth abgekündigt („no longer offered in future API versions") und bietet seit API 2.1.11 OAuth2-ROPC. BearerAuthClient legt sich als PSR-18-Mantel um den Client und ersetzt die Kopfzeile — kein Fork, keine Kopie.

Dabei ist zu beachten: Bearer und API-Key gemeinsam weist DHL zurück („Use EITHER Bearer Token or (Apikey and Basic Auth)"). Die Antwort ist ein 401, der wie ein falsches Passwort aussieht, obwohl die Anmeldung sauber lief. Der Mantel entfernt dhl-api-key deshalb wieder.

Verbindung testen

Website-Einstellungen → Shop → Versand → DHL Paket → „Verbindung testen".

Der Test ruft validateShipments() je Produkt auf — damit fällt auch eine Abrechnungsnummer auf, deren Verfahren nicht zum Produkt passt. Ein reiner Anmeldetest würde genau den häufigsten Einrichtungsfehler durchlassen.

Zwei Feinheiten, die dabei nötig waren:

  • Jedes Produkt wird gegen ein passendes Ziel geprüft. Ein internationales Produkt an eine Inlandsadresse lehnt DHL hart ab („The product entered is not available for this country") — das hat nichts mit dem Vertrag zu tun. Nationale Produkte werden gegen die eigene Anschrift geprüft, internationale gegen eine EU-Adresse (Österreich; außerhalb der EU verlangt DHL eine Zollinhaltserklärung, die das Modul nicht mitschickt).
  • Weiche Beanstandungen sind kein Ausschluss. validateShipments() wirft auch bei „weak validation errors", die den Druck gar nicht verhindern — der Test ist damit strenger als der echte Etikettendruck und muss die Fälle trennen.

In der Sandbox dauert der Test spürbar länger: Das SDK schaltet dort seine Schemaprüfung ein, und die baut je POST-Anfrage einen OpenAPI-Validator aus einer rund 2 MB großen YAML-Datei auf. Bei fünf Produkten sind das fünf vollständige Durchläufe. In der Produktion entfällt das.

Tests

composer install
composer test      # PHPUnit
composer lint      # PHP_CodeSniffer (PSR-12)
composer analyse   # PHPStan, Stufe 5

Geprüft wird alles, was vor der ersten Anfrage an DHL passiert: die Zusammensetzung der Abrechnungsnummer, das Auffüllen der Referenz, die Umrechnung von Millimetern und Ländercodes, die Übersetzung der SDK-Ausnahmen — und der Austausch der Anmeldekopfzeile samt Wiederholungsversuch, mit einem PSR-18-Doppelgänger statt eines echten Aufrufs. Kein Test verlässt den Rechner.

silverstripe/standards ist bewusst nicht eingebunden: Es setzt customRulesetUsed: true und schaltet damit die Level-Regeln von PHPStan vollständig ab. Übrig bliebe eine Analyse, die nur so aussieht.

Eigenheiten

  • ISO-3-Ländercodes. DHL will DEU, SilverShop speichert DE. Umgerechnet über symfony/intl.
  • Maße in Zentimetern. Das Basismodul führt Millimeter.
  • Referenz mindestens 8 Zeichen (Schemavorgabe). Kürzere werden links mit Nullen aufgefüllt.
  • Nach dem Tagesabschluss ist das Etikett bei DHL nicht mehr abrufbar. Dass das Basismodul das PDF selbst ablegt, ist deshalb keine Bequemlichkeit.
  • Storno über DELETE /orders, bis zum Tagesabschluss.