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
Bringt guzzlehttp/guzzle als PSR-18-Client mit. Die Aufrufe laufen mit Zeitlimits aus
Tietge\Shipping\Http\HttpClientFactory des Basismoduls (connect_timeout 5 s, timeout 30 s).
Im Projekt muss php-http/discovery als Composer-Plugin erlaubt sein
(config.allow-plugins."php-http/discovery": true) — das SDK setzt es voraus.
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 aus der YAML heraushalten will, legt sie in die .env und verweist darauf —
wahlweise mit Backticks oder einem _env-Schlüssel:
api_key: '`SS_DHL_API_KEY`'
api_secret: '`SS_DHL_API_SECRET`'
gkp_user_env: 'SS_DHL_GKP_USER'
gkp_password_env: 'SS_DHL_GKP_PASSWORD'
ekp: '`SS_DHL_EKP`'
Das gilt für api_key, api_secret, gkp_user, gkp_password und ekp. Aufgelöst wird im
Modul über Environment::getEnv() — Silverstripe selbst ersetzt Backticks nur in
Injector-Definitionen, nicht in der Config-API (bis 1.0.0 ging deshalb die Zeichenkette samt
Backticks als Passwort hinaus). Ist die Variable nicht gesetzt, gilt die Einstellung als fehlend.
„Eingerichtet" heißt: vollständig und brauchbar. Eine EKP, die nicht aus zehn Ziffern
besteht, ein unbekannter auth_mode oder eine Abrechnungsnummer, deren Verfahren nicht zum
Produkt passt, blenden DHL im Backend aus; die Gründe stehen im Unterreiter „DHL Paket".
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.
Produkt nach Zielland
Ist an der Bestellung kein Produkt gewählt, entscheidet das Zielland: für national_countries
(Vorgabe DE) das nationale Produkt, sonst das internationale. Ohne Angabe jeweils das erste
passende aus billing_numbers.
national_product: 'V01PAK'
international_product: 'V53WPAK' # oder V54EPAK (Europaket)
national_countries: ['DE']
Ein nationales Produkt ins Ausland (oder umgekehrt) wird vor dem Aufruf mit einer deutschen
Meldung abgewiesen. Projektweit lässt sich die Wahl auch im Basismodul festlegen
(ShipmentService.default_products), das hat Vorrang.
Feldlängen
Grenzen laut Schema werden vor dem Aufruf eingehalten: Name 50 (Überlauf in name3), Firma 50,
Straße 50, Hausnummer 10 (zu lang → ganze Zeile als Straße), Zusatz 2 × 60, Ort 40. E-Mail über 80
und Telefon über 20 Zeichen werden weggelassen statt gekürzt; eine Postleitzahl über 10 Zeichen
ist ein Fehler.
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.