tcgunel / omniship-common
Multi-carrier shipping abstraction for PHP
Requires
- php: ^8.2
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- nyholm/psr7: ^1.8
- pestphp/pest: ^3.0
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.0
This package is auto-updated.
Last update: 2026-07-30 09:38:05 UTC
README
Multi-carrier shipping abstraction library for PHP. Like Omnipay, but for shipping.
Requirements
- PHP 8.2+
- PSR-18 HTTP client implementation
- PSR-17 HTTP factory implementation
Installation
composer require tcgunel/omniship-common
Architecture
AbstractCarrier
├── AbstractHttpCarrier → REST/JSON carriers (UPS, FedEx, DHL, HepsiJet, Aras, MNG, KolayGelsin, Horoz)
└── AbstractSoapCarrier → SOAP/XML carriers (Yurtiçi, PTT, Sürat)
Each carrier is a separate package that extends the appropriate base class.
The two base classes take different constructor arguments — an HTTP carrier
accepts a PSR-18 client, a SOAP carrier accepts a SoapClient. Ask before you
instantiate, rather than assuming:
Omniship::isSoap('Yurtici'); // true Omniship::isSoap('MNG'); // false $carrier = Omniship::isSoap($name) ? Omniship::create($name) : Omniship::create($name, $yourPsr18Client);
Available Carriers
| Package | Carrier | Type | Auth |
|---|---|---|---|
tcgunel/omniship-yurtici |
Yurtiçi Kargo | SOAP | Username/Password |
tcgunel/omniship-aras |
Aras Kargo | HTTP/XML | Username/Password |
tcgunel/omniship-kolaygelsin |
KolayGelsin (Sendeo) | HTTP/JSON | API Token |
Quick Start
use Omniship\Omniship; // Create a carrier instance $carrier = Omniship::create('Yurtici'); $carrier->initialize([ 'username' => 'your-username', 'password' => 'your-password', 'testMode' => true, ]); // Create a shipment $response = $carrier->createShipment([ 'cargoKey' => 'ORDER-001', 'invoiceKey' => 'INV-001', 'shipTo' => new \Omniship\Common\Address( name: 'Mehmet Demir', street1: 'Kızılay Mah. 123. Sok. No:5', city: 'Ankara', district: 'Çankaya', phone: '05559876543', ), 'packages' => [ new \Omniship\Common\Package(weight: 2.5, desi: 3), ], ])->send(); if ($response->isSuccessful()) { echo $response->getTrackingNumber(); echo $response->getShipmentId(); }
Common Operations
Every carrier supports these operations:
Create Shipment
$response = $carrier->createShipment([...])->send(); $response->isSuccessful(); $response->getTrackingNumber(); $response->getShipmentId(); $response->getBarcode();
Track Shipment
$response = $carrier->getTrackingStatus([ 'trackingNumber' => '330012345678', ])->send(); $info = $response->getTrackingInfo(); $info->status; // ShipmentStatus enum $info->trackingNumber; $info->events; // TrackingEvent[]
Cancel Shipment
$response = $carrier->cancelShipment([ 'trackingNumber' => 'ORDER-001', ])->send(); $response->isSuccessful(); $response->isCancelled();
Logging SOAP Traffic
SOAP carriers build their own SoapClient from the WSDL, so there is no PSR-18
client to wrap. Supply a factory to hand them a client of your own — typically a
subclass that overrides __doRequest() to record every exchange:
$carrier->setSoapClientFactory( fn (string $wsdl, array $options) => new LoggingSoapClient($wsdl, $options), );
The factory receives the carrier's WSDL URL and SOAP options, and is called once,
lazily, on the first request. Passing a ready-made client to the constructor (or
setSoapClient()) still works when you do not need the WSDL.
Parameter Handling
Parameters usually arrive from HTTP input, where everything is a string, while
setters are typed for the carrier's API. initialize() bridges that:
- null means "not configured" and is skipped, so an unconfigured optional
credential no longer throws a
TypeErrorout of astring-typed setter. Setters declared?stringstill receive it, so a carrier can tell "cleared" from "never set". - scalars are coerced to the declared type when nothing is lost.
'1'reaches anintsetter as1,1reaches astringsetter as'1'. This matters because carriers disagree: Yurtiçi typescodCollectionTypeasint, Aras asstring, and one payload has to drive both. A lossy value ('abc'or'1.5'into anint) is passed through untouched so a real mistake still fails loudly.
Domain Models
Address
new Address( name: 'Alıcı Adı', company: 'Firma', street1: 'Adres satırı 1', street2: 'Adres satırı 2', city: 'İstanbul', // İl district: 'Kadıköy', // İlçe postalCode: '34700', country: 'TR', phone: '05551234567', email: 'alici@example.com', taxId: '1234567890', // Vergi numarası );
Package
new Package( weight: 2.5, // KG (metric default) length: 30, // CM width: 20, // CM height: 15, // CM desi: 3, // Volumetric weight (L*W*H/3000) quantity: 1, description: 'Elektronik ürün', );
ShipmentStatus Enum
PRE_TRANSIT → Kayıt alındı
PICKED_UP → Kabul edildi
IN_TRANSIT → Aktarmada / Şubede
OUT_FOR_DELIVERY → Dağıtımda
DELIVERED → Teslim edildi
CANCELLED → İptal edildi
RETURNED → İade
UNKNOWN → Bilinmiyor
Key Concepts for Turkish Carriers
- Desi: Volumetric weight = (L x W x H) / 3000. First-class field on Package.
- District (İlçe): Required by all Turkish carriers. Maps to
Address::$district. - Barcode: Turkish carriers return a barcode string on shipment creation.
- PaymentType: Sender/receiver pays. Standard in Turkish e-commerce.
- COD (Kapıda Ödeme): Cash on delivery with
cashOnDeliveryflag +codAmount.
Testing
# Run tests vendor/bin/pest # Static analysis vendor/bin/phpstan analyse
License
MIT