alexanderpoellmann / shipping-contracts
Small carrier-neutral shipping contracts and value objects for PHP applications.
Package info
github.com/AlexanderPoellmann/shipping-contracts
pkg:composer/alexanderpoellmann/shipping-contracts
Requires
- php: ^8.4
Requires (Dev)
- laravel/pint: ^1.29
- pestphp/pest: ^4.0
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-25 17:49:14 UTC
README
Small, framework-agnostic contracts and DTOs for applications that ship through more than one carrier.
Requires PHP 8.4 or newer.
composer require alexanderpoellmann/shipping-contracts
The package deliberately models only concepts that survive a carrier boundary. DPD Product1/additional-product slots, Austrian Post product codes, printer settings, features and similar carrier configuration stay in their carrier packages/adapters.
Capabilities
Providers implement only what they actually support:
use AlexanderPoellmann\Shipping\Contracts\Carrier; use AlexanderPoellmann\Shipping\Contracts\CreatesShipments; use AlexanderPoellmann\Shipping\Contracts\DownloadsLabels; use AlexanderPoellmann\Shipping\Contracts\CancelsShipments; final class SomeCarrierAdapter implements Carrier, CreatesShipments, DownloadsLabels, CancelsShipments { // ... }
There is intentionally no large ShippingProvider interface.
The first integrations use:
Carrier— stable adapter identity for discovery/selection.CreatesShipments— create a shipment from the neutralShipmentDTO and return neutral tracking numbers/labels.DownloadsLabels— resolve a remote label into label contents. DPD supports this; Austrian Post already returns label data when the shipment is created.CancelsShipments— cancel by neutralTrackingNumber.
TracksShipments is intentionally not defined yet: DPD's current GetStatus operation reports WEB.Service availability rather than parcel tracking, and the current Austrian Post PLC package exposes no parcel-tracking operation. CreatesLabels is also not separated from CreatesShipments because both existing APIs generate labels as part of shipment creation. SchedulesPickups is deferred because the two native APIs do not currently share honest semantics: DPD schedules against the configured account while PLC requires an explicit pickup address, time window, handover/location mode and terms acceptance. Add these capabilities only when their neutral request/response semantics are clear rather than introducing carrier-option escape hatches.
Neutral DTOs
use AlexanderPoellmann\Shipping\Data\Address; use AlexanderPoellmann\Shipping\Data\Parcel; use AlexanderPoellmann\Shipping\Data\Shipment; $shipment = new Shipment( sender: new Address('Sender GmbH', 'Main Street', '1010', 'Vienna', 'AT'), recipient: new Address('Recipient GmbH', 'Other Street', '4020', 'Linz', 'AT'), parcels: [new Parcel(weightInGrams: 1200)], reference: 'ORDER-42', );
A Shipment contains no product/service code and no carrierOptions escape hatch. Carrier-specific selections belong to the adapter.
DTOs are final, readonly objects. Invalid input throws InvalidArgumentException:
- Required address fields must contain non-whitespace characters. Country codes must be two ASCII letters; assigned ISO codes are not checked and case is preserved.
- Tracking numbers must contain non-whitespace characters and retain their original value, including leading zeroes, when converted to strings or JSON.
- Parcel weights and dimensions must be positive when supplied. Dimensions must be supplied together; measurements may otherwise be omitted.
- Shipments require at least one
Parcel. Parcel collections and shipment result collections are reindexed as lists and checked for the expected object types. - Labels require a URL or document contents. Supplied URLs must not be blank and supplied contents must not be empty. Binary contents are preserved.
withContents()returns a new label and preserves existing metadata unless replacements are supplied.
Address and tracking values are validated without trimming or normalization. Carrier-specific validation belongs in the adapter. shippingDate accepts DateTimeInterface; use DateTimeImmutable to prevent later changes to a shared date instance.
Laravel integration
This package does not depend on Laravel and defines no service-container tags or manager abstraction. The Laravel carrier packages register their concrete adapters and tag them as shipping.adapters.
Applications can resolve a carrier directly:
$dpd = app(\AlexanderPoellmann\LaravelDpd\Shipping\DpdShippingAdapter::class); $post = app(\AlexanderPoellmann\LaravelPostPlc\Shipping\PostPlcShippingAdapter::class);
Or discover installed adapters without a competing global interface binding:
$adapters = collect(app()->tagged('shipping.adapters')) ->keyBy(fn (\AlexanderPoellmann\Shipping\Contracts\Carrier $carrier) => $carrier->carrier()); $dpd = $adapters->get('dpd'); $post = $adapters->get('post-plc');
Carrier product/service selection remains native. For example, the DPD adapter accepts DPD Products, while the Austrian Post adapter accepts PostProductCodes|ProductCode|string through carrier-specific methods.
Development
composer install composer check
composer check validates the package metadata, checks formatting with Laravel Pint, runs PHPStan at its maximum level on src, and runs the Pest suite. Individual commands are also available:
composer test
composer analyse
composer format
composer format:test
composer test:coverage
Coverage requires Xdebug with XDEBUG_MODE=coverage, or PCOV, and enforces 100% source line coverage. CI runs the checks and coverage on PHP 8.4 and 8.5. As a library, this repository excludes composer.lock so CI resolves dependencies for each supported PHP version.