Search by

tietge / silverstripe-shipping-dhl

moritz-sauer-13

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

Statistics

Installs: 0

Dependents: 0

Suggesters: 1

1.0.0 2026-08-06 08:38 UTC

This package is auto-updated.

Last update: 2026-10-06 11:06:48 UTC


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.

VerfahrenProduktcodeProdukt
01V01PAKDHL Paket (national)
53V53WPAKDHL Paket International
54V54EPAKDHL Europaket
62V62KPDHL Kleinpaket
66V66WPIWarenpost 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.

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-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.