Global Logistics Aggregation Plugin

Global Logistics Aggregation (global-logistics)

Global Logistics Aggregation is a composer package with a unified facade for domestic express and international logistics tracking queries (PHP 8.2+, PSR-4, framework-agnostic).

Project introduction

global-logistics collapses tracking queries for 209 express/postal carriers worldwide into a single facade: your application just passes in a tracking number, and the package works out the domestic/international channel and the carrier on its own — you never have to care about the differences between carriers (signing, OAuth2, XML/JSON, status mapping).

| Metric | Value |
|—-|—-|
| Carriers integrated | 209 (45 domestic + 164 international) |
| Tracking-number detection rules | 187 (order-sensitive, first match wins) |
| International coverage | The big four (DHL / FedEx / UPS / USPS) + national postal S10 systems (Europe, Latin America and the Caribbean, Africa and the Middle East, Asia-Pacific) |
| Unified status semantics | 7 TrackStatus values (including exception / returned) |
| Tests | 1663 cases / 6662 assertions, all green |
| Environment | PHP 8.2+, PSR-4 / PSR-18, no framework binding; works out of the box with Laravel / ThinkPHP / Hyperf / Webman / Yii 2 |

Project description

Built for e-commerce, warehousing, ERP and similar business systems, it converges the official APIs of “domestic express + international logistics” into a single facade:

  • One entry point: Logistics::track($trackingNo) automatically identifies the domestic/international channel and the carrier — no need to know who owns the tracking number
  • One data model: every carrier returns the same Tracking / TrackingEvent structure, so the business layer only talks to one shape
  • One status semantic: the wildly varying raw statuses of carriers are mapped onto a unified TrackStatus enum (7 values)
  • Global coverage: 164 international carriers, including DHL / FedEx / UPS / USPS and the national postal S10 systems (Europe, Latin America and the Caribbean, Africa and the Middle East, Asia-Pacific)
  • Zero hardcoded credentials: every carrier’s credentials are injected through configuration, keeping code and secrets completely separate

Features

Integrated carriers (209)

Domestic: SF Express, ZTO Express, YTO Express, J&T Express, Yunda Express, STO Express, JD Logistics, EMS, BEST Express, Deppon Logistics, Kuayue Express, ANE Logistics, Cainiao Express, China Post, Suning Logistics, UC Express, Yimidida, ZJS Express, TTK Express, ZTO Freight, Daniao (Cainiao Direct Delivery), China Railway Express, Shunxin Jetta, SURE Express, Xinfeng Logistics, Lianhaotong, RRS Logistics, Fengwang Express, BEST Freight, Yunda Freight, YTO Freight, Zengyi Express, China Civil Aviation Express, Hoau, Jiaji Express, Longbang Express, A Plus Express, Suteng Logistics, China Railway Logistics, China Post Logistics, Zengyi Express, Quanfeng Express, Guotong Express, Yuancheng Express, Xinbang Logistics

International: DHL, FedEx, UPS, USPS, Royal Mail, Canada Post, Australia Post, Japan Post, Aramex, GLS, DPD, PostNL, Cainiao International, Correios (Brazil), Evri, 4PX, Hongkong Post, Kerry Express, Korea Post, La Poste (France), NZ Post, Poste Italiane, Russian Post, Singapore Post, Swiss Post, Yodel, Yuntu Logistics, Yanwen Logistics, SF International, TNT, ONTRAQ, Purolator, bpost (Belgian Post), Correos (Spanish Post), Delhivery (India), InPost (Poland parcel lockers), Omniva (Estonia), Posti (Finland), Bring (Norway), Austrian Post, Thailand Post, Chunghwa Post (Taiwan), PostNord (Sweden/Denmark), CTT (Portugal), An Post (Ireland), Poczta Polska (Poland), India Post, Pos Malaysia (Malaysia), Emirates Post (UAE), Magyar Posta (Hungary), Česká pošta (Czech Republic), ELTA (Greece), Viettel Post (Vietnam), ZTO International, YTO International, J&T International, Winit, Ukrposhta (Ukraine), Turkey PTT (Turkey), Israel Post (Israel), Egypt Post (Egypt), Saudi Post (Saudi Arabia), South African Post (South Africa), Correos de México (Mexico), Correo Argentino (Argentina), Correos de Chile (Chile), Pos Indonesia (Indonesia), PHLPost (Philippines), Pakistan Post (Pakistan), Kazpost (Kazakhstan), Poșta Română (Romania), Hrvatska pošta (Croatia), Slovak Post (Slovakia), Pošta Slovenije (Slovenia), Pošta Srbije (Serbia), Bulgarian Posts (Bulgaria), Lietuvos paštas (Lithuania), Latvijas Pasts (Latvia), Íslandspóstur (Iceland), MaltaPost (Malta), POST Luxembourg (Luxembourg), Cyprus Post (Cyprus), Poșta Moldovei (Moldova), Posta Shqiptare (Albania), Belpochta (Belarus), Makedonska Pošta (North Macedonia), BH Pošta (Bosnia and Herzegovina), Deutsche Post (Germany), Montenegro Post (Montenegro), Andorra Post (Andorra), La Poste Monaco (Monaco), Liechtenstein Post (Liechtenstein), Poste San Marino (San Marino), Poste Vaticane (Vatican), Royal Gibraltar Post (Gibraltar), Jersey Post (Jersey), Guernsey Post (Guernsey), Isle of Man Post (Isle of Man), Posta Faroe Islands (Faroe Islands), Post Greenland (Greenland), Post Åland (Åland Islands), 4-72 (Colombia), Serpost (Peru), Correo Uruguayo (Uruguay), Correo Paraguayo (Paraguay), Correos de Bolivia (Bolivia), Correos del Ecuador (Ecuador), Ipostel (Venezuela), Correos de Costa Rica (Costa Rica), Correos de Panamá (Panama), INPOSDOM (Dominican Republic), Correo de Guatemala (Guatemala), HonduCorreo (Honduras), Correos de El Salvador (El Salvador), Correos de Nicaragua (Nicaragua), Correos de Cuba (Cuba), Jamaica Post (Jamaica), TTPOST (Trinidad and Tobago), Barbados Post (Barbados), Bahamas Post (Bahamas), Suriname Post (Suriname), Guyana Post (Guyana), Barid Al-Maghrib (Morocco), Algérie Poste (Algeria), La Poste Tunisienne (Tunisia), Posta Kenya (Kenya), NIPOST (Nigeria), Ethiopia Post (Ethiopia), Ghana Post (Ghana), Tanzania Post (Tanzania), Uganda Post (Uganda), Rwanda Post (Rwanda), Zampost (Zambia), Zimpost (Zimbabwe), Mozambique Post (Mozambique), Correios de Angola (Angola), La Poste Sénégalaise (Senegal), La Poste de Côte d’Ivoire (Côte d’Ivoire), Cameroon Post (Cameroon), Mauritius Post (Mauritius), Qatar Post (Qatar), Kuwait Post (Kuwait), Bahrain Post (Bahrain), Bangladesh Post (Bangladesh), Nepal Post (Nepal), Sri Lanka Post (Sri Lanka), Myanmar Post (Myanmar), Cambodia Post (Cambodia), Laos Post (Laos), Mongolia Post (Mongolia), Georgian Post (Georgia), Azərpoçt (Azerbaijan), HayPost (Armenia), Uzbekistan Post (Uzbekistan), Kyrgyz Post (Kyrgyzstan), Tajikistan Post (Tajikistan), Turkmenistan Post (Turkmenistan), Afghanistan Post (Afghanistan), Bhutan Post (Bhutan), Maldives Post (Maldives), Brunei Post (Brunei), Papua New Guinea Post (Papua New Guinea), Fiji Post (Fiji), Samoa Post (Samoa)

Unified status enum (GlobalLogistics\Support\TrackStatus)

PENDING (awaiting pickup) → IN_TRANSIT (in transit) → OUT_FOR_DELIVERY (out for delivery) → DELIVERED (signed for); exceptions map to EXCEPTION, returns to RETURNED, and anything unrecognizable to UNKNOWN.

Core capabilities

  • Automatic tracking-number detection (187 regex rules, order-sensitive, domestic rules take priority)
  • Unified tracking queries (Logistics::track()) plus explicit channel calls (domestic() / international())
  • A unified exception hierarchy (authentication failure / tracking number not found / network error / carrier not registered / API error)
  • HTTP infrastructure: PSR-18 client, automatic OAuth2 token acquisition and caching, automatic retry on failure
  • Framework auto-discovery: works out of the box with Laravel / ThinkPHP 8 / Hyperf / Webman / Yii 2
  • Callback signature verification (SF Express example: verifyCallbackSignature())

Usage

Installation

1
composer require erikwang2013/global-logistics

Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

use GlobalLogistics\Logistics;

Logistics::configure([
// Domestic
'sf' => ['partner_id' => '...', 'checkword' => '...'],
'zto' => ['company_id' => '...', 'secret' => '...'],
'yto' => ['app_key' => '...', 'app_secret' => '...'],
'jt' => ['api_key' => '...', 'secret' => '...'],
'yd' => ['app_key' => '...', 'app_secret' => '...'],
'sto' => [],
'jd' => [],
'ems' => ['app_id' => '...'],
'ht' => ['partner_id' => '...', 'token' => '...'],
'debon' => ['app_key' => '...', 'app_secret' => '...'],
'ky' => ['app_key' => '...', 'app_secret' => '...'],
'ane' => ['app_key' => '...'],
// International
'dhl' => ['client_id' => '...', 'client_secret' => '...'],
'fedex' => ['client_id' => '...', 'client_secret' => '...'],
'ups' => ['client_id' => '...', 'client_secret' => '...'],
'usps' => ['user_id' => '...'],
'royal-mail' => ['client_id' => '...', 'client_secret' => '...'],
'canada-post' => ['customer_number' => '...', 'api_key' => '...'],
'australia-post' => ['api_key' => '...'],
'japan-post' => [],
'aramex' => ['user_name' => '...', 'password' => '...', 'account_number' => '...'],
'gls' => ['api_key' => '...'],
'dpd' => ['user_name' => '...', 'password' => '...'],
'postnl' => ['api_key' => '...'],

// Optional: custom PSR-18 HTTP client (Guzzle is built automatically by default)
'http_client' => null,
// Optional: number of retries on failure (default 2)
'max_retries' => 2,
]);

Framework projects (Laravel, etc.) can use the config/logistics.php template directly — see “Framework integration”.

If configure() is never called, the facade initializes itself with an empty configuration (only carriers that need no credentials are supported).

Configuration reference

Top-level options

Key Type Default Description
http_client a PSR-18 client or null null Custom HTTP client; null builds Guzzle automatically
max_retries int 2 Number of retries after a single failed request
registry array built-in registry of 209 carriers Custom “channel → code → adapter class” registry (advanced)
detector_rules array built-in 187 rules Custom tracking-number detection rules (advanced)

Credential field cheat sheet

Each carrier authenticates differently, so field names differ too. The common mappings are:

Field Meaning Typical carriers
partner_id + checkword SF-style signed account and check code sf, sf-international
company_id + secret ZTO-style company account and secret zto, zto-freight, zto-intl
app_key + app_secret Generic signing key pair yto, yd, jt, debon, ky, etc.
ebusiness_id + app_key Kdniao API (electronic waybills) lht, rrs, sure, xf and the various freight carriers
client_id + client_secret OAuth2 client credentials (token acquired and refreshed automatically) dhl, fedex, ups, royal-mail, swiss-post, yodel
user_id / api_key / key An account or key issued by the carrier usps, postnl, gls, saudi-post, etc.
endpoint Custom API endpoint (optional; leave it as '' to use the built-in official address) Supported by all carriers

Which fields a given carrier needs is determined by the keys of its entry in config/logistics.php; carriers that need no credentials can simply use an empty array.

Obtaining credentials

  • Domestic express: contact the carrier’s sales or integration contact to apply for a “tracking query / electronic waybill API” account (SF Fengqiao, ZTO Open Platform, etc.)
  • International express: DHL / FedEx / UPS and others let you register a developer account on their developer portal to obtain OAuth2 credentials; USPS, PostNL and others issue API Keys on their websites
  • National postal operators: most offer a public tracking API that needs no credentials; where credentials are required, apply at the postal operator’s developer center

Carriers that need no credentials

The following carriers work as-is — just leave their configuration as an empty array: domestic sto (STO Express), jd (JD Logistics); international japan-post (Japan Post) and most postal operators (turkey-post, israel-post, egypt-post, south-african-post, phl-post, pakistan-post, kazpost, etc.). See config/logistics.php for the full list.

Credential security

Inject credentials through environment variables or your framework’s .env rather than hardcoding them into the repository:

1
2
3
4
5
6
Logistics::configure([
'sf' => [
'partner_id' => getenv('SF_PARTNER_ID'),
'checkword' => getenv('SF_CHECKWORD'),
],
]);

Querying tracking information

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Automatically identifies the channel (domestic/international) and the carrier
$tracking = Logistics::track('SF1234567890');

// Specify explicitly (when the tracking-number rules cannot cover it)
$tracking = Logistics::domestic('sf')->queryTrack('SF1234567890');
$tracking = Logistics::international('dhl')->queryTrack('DHL1234567890');

echo $tracking->status->name; // DELIVERED
echo $tracking->latestDescription; // shipment signed for
echo $tracking->carrierCode; // sf
echo $tracking->deliveredAt?->format('Y-m-d H:i:s'); // delivery time (when signed for)

foreach ($tracking->events as $event) {
echo $event->occurredAt?->format('Y-m-d H:i:s'), ' ', $event->location, ' ', $event->description, PHP_EOL;
}

Error handling

All exceptions extend GlobalLogistics\Exceptions\LogisticsException, so they can be caught in one place:

Exception Scenario
CarrierNotFoundException The carrier cannot be identified from the tracking number
TrackingNotFoundException The tracking number is valid but the carrier has no tracking record for it
AuthException Authentication failure (bad credentials, etc.)
NetworkException HTTP network error (implements the PSR-18 NetworkExceptionInterface)
LogisticsException Other API/parsing errors
1
2
3
4
5
6
7
use GlobalLogistics\Exceptions\LogisticsException;

try {
$tracking = Logistics::track('SF1234567890');
} catch (LogisticsException $e) {
// Log $e->getMessage() — it carries the carrier code and the original error code, e.g. "[SF A1001] 必传参数不可为空" (required parameter must not be empty)
}

Callback signature verification (subscription push)

Once the carrier has enabled its subscription API, here is how to handle the callback (SF Express as the example):

1
2
3
4
5
6
7
8
use GlobalLogistics\Logistics;

$carrier = Logistics::domestic('sf');
if (!$carrier->verifyCallbackSignature((string) file_get_contents('php://input'), (string) $_SERVER['HTTP_DIGEST'])) {
http_response_code(401);
exit('signature mismatch');
}
// Signature verified — handle the tracking push ...

Architecture

global-logistics architecture diagram

global-logistics design sequence diagram

Directory structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
global-logistics/
├── src/
│ ├── Carriers/
│ │ ├── Domestic/ # 45 domestic adapters (SF Express, ZTO, YTO, ...)
│ │ └── International/ # 164 international adapters (DHL, FedEx, UPS, national postal S10, ...)
│ ├── Exceptions/ # Exception hierarchy (LogisticsException + 4 scenario-specific exceptions)
│ ├── Framework/ # Framework auto-discovery (Laravel / ThinkPHP / Hyperf / Webman / Yii 2)
│ ├── Http/ # PSR-18: OAuthTokenClient, RetryingClient, HttpClientFactory
│ ├── Models/ # Tracking / TrackingEvent / Order / OrderRequest / Label
│ ├── Resources/ # carrier-registry.php (209-carrier registry), detector-rules.php (187 rules)
│ ├── Support/ # support classes such as TrackStatus
│ ├── CarrierFactory.php # registry → adapter instantiation
│ ├── CarrierInterface.php # the unified adapter contract
│ ├── Channel.php # domestic / international channel enum
│ ├── Config.php # dot-notation configuration reader
│ ├── Detection.php # tracking-number detection result
│ ├── Detector.php # tracking-number rule detection
│ ├── Install.php # installation bootstrap
│ └── Logistics.php # the static facade
├── config/
│ └── logistics.php # configuration template (credential placeholders for all 209)
├── docs/
│ ├── images/ # architecture diagram / design sequence diagram
│ └── superpowers/ # design specs and implementation plans
├── tests/
│ ├── Carriers/ # 7 cases per carrier (539 in total)
│ ├── Unit/ # detector, registry smoke tests, and so on
│ └── fixtures/ # track / empty / error fixtures for every carrier
├── composer.json
└── README.md

Responsibilities of each layer

  • Logistics (src/Logistics.php): the static facade; it holds the global configuration, detector and factory, and initializes itself with an empty configuration when nothing has been configured
  • Detector (src/Detector.php + src/Resources/detector-rules.php): a regex rule table where the first match in order wins, returning a Detection (channel + carrier code); rule order is sensitive (for example, STO numbers starting with 77 must be matched before the plain 13-digit rule)
  • CarrierFactory (src/CarrierFactory.php + src/Resources/carrier-registry.php): instantiates adapters from the “channel → code → adapter class” registry, injecting Config and the HTTP client in a uniform way
  • Carrier adapters (src/Carriers/): implement CarrierInterface and absorb each carrier’s protocol differences (signing, OAuth2, XML/JSON, status mapping); they all follow one template structure (ENDPOINT constant + STATUS_MAP + mapEvent()), which makes adding carriers straightforward
  • HTTP layer (src/Http/): OAuthTokenClient is a PSR-18 decorator that fetches the token lazily, caches it for the process (expiring 60s early) and refreshes plus retries once on a 401; RetryingClient retries failed requests according to max_retries
  • Model layer (src/Models/): Tracking / TrackingEvent are immutable objects; Order / OrderRequest / Label are reserved for ordering and waybill capabilities
  • Config (src/Config.php): dot-notation value lookup ($config->get('dhl.client_id'))
  • Exception hierarchy (src/Exceptions/): LogisticsException is the base class, with 4 scenario-specific subclasses

Adding a new carrier

  1. Create the adapter class (following the src/Carriers/Domestic/Yto.php template): implement CarrierInterface and do the status mapping in mapEvent()
  2. Registry: add the “channel → code → class” entry to src/Resources/carrier-registry.php
  3. Tracking-number rules: add a regex to src/Resources/detector-rules.php (mind the ordering — domestic rules come first)
  4. Add a fixture and an adapter test (mock HTTP, no real credentials needed)

Framework integration

After composer require erikwang2013/global-logistics the package is auto-discovered per framework, with no manual registration; the configuration template is consistently an array keyed by carrier code at the top level (see config/logistics.php; the structure matches the argument to Logistics::configure()).

Laravel

  • Auto-registration: composer package discovery via extra.laravel.providers, nothing to configure
  • Publish the config: php artisan vendor:publish --tag=global-logistics (generates config/logistics.php)
  • Usage: \GlobalLogistics\Logistics::track('SF1234567890')

ThinkPHP 8

  • Auto-registration: composer extra.think.services (generates vendor/services.php at install time; you can also regenerate it with php think service:discover)
  • Configuration: have config/logistics.php in your application return an array of the same structure (it overrides the package defaults)
  • Usage: \GlobalLogistics\Logistics::track('SF1234567890')

Hyperf

  • Auto-discovery: composer extra.hyperf.config points at the ConfigProvider
  • Publish the config: php bin/hyperf.php vendor:publish (publishes to config/autoload/logistics.php)
  • Usage: \GlobalLogistics\Logistics::track('SF1234567890')

Webman

  • Auto-install: the webman project template ships composer hooks such as post-package-install, so on install/update the configuration is copied to config/plugin/erikwang2013/global-logistics/ and on uninstall it is removed automatically
  • Reading the config: config('plugin.erikwang2013.global-logistics.app.sf.partner_id')
  • Usage: \GlobalLogistics\Logistics::track('SF1234567890')

Yii 2

  • Auto-registration: the package type yii2-extension plus composer extra.bootstrap, executed on every application bootstrap
  • Configuration: add 'logistics' => [...array of the same structure...] to your application configuration’s params
  • Usage: \GlobalLogistics\Logistics::track('SF1234567890')
  • If this package’s entry does not appear in vendor/yiisoft/extensions.php (rare), run composer dump-autoload to rebuild it

Development

1
2
composer install
composer test

The full test suite runs without real credentials (adapter tests go through mock HTTP + fixtures; framework integration tests use the real framework classes).