tietge / silverstripe-shipping-dhl
DHL-Paketdienst für tietge/silverstripe-shipping — Etiketten über die Parcel DE Shipping API v2.
Package info
git.innomedia.de/Tietge/silverstripe-shipping-dhl
Type:silverstripe-vendormodule
pkg:composer/tietge/silverstripe-shipping-dhl
Requires
- php: ^8.3
- dhl/sdk-api-parcel-de: ^1.4
- php-http/discovery: ^1.17
- psr/http-client: ^1
- psr/http-factory: ^1
- psr/log: ^2 || ^3
- psr/simple-cache: ^2 || ^3
- silverstripe/framework: ^6
- symfony/intl: ^7.0
- tietge/silverstripe-shipping: ^1.0
Requires (Dev)
- nyholm/psr7: ^1.8
- php-http/guzzle7-adapter: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.3
- squizlabs/php_codesniffer: ^3.7
Suggests
- php-http/guzzle7-adapter: PSR-18-Client für die API-Aufrufe. Im Projekt bereits vorhanden.
Provides
None
Conflicts
None
Replaces
None
README
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
V62WPverwenden. 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-keydeshalb 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 speichertDE. Umgerechnet übersymfony/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.