ghaninia/shipping
ghaninia/shipping is a Laravel package for flexible shipping management. Define shipping methods and rates, calculate delivery costs at checkout, and keep shipping logic clean and configurable through a simple, developer-friendly API.
Install via Composer and publish the config:
composer require ghaninia/shipping
php artisan vendor:publish --provider="Ghaninia\Shipping\ShippingServiceProvider"
Open the published config/shipping.php first — this is where driver selection, credentials, and defaults live. The package is driver-based, exposing two built-in postage calculators: sefarshi (registered/ordinary mail) and pisthtaz (express mail). You pick the active driver either in config or per-call.
Your first working call is a rate lookup by weight and destination. Consult the README for the exact facade/method signature, but the shape is typically:
use Ghaninia\Shipping\Facades\Shipping;
$cost = Shipping::driver('pisthtaz')->calculate([
'weight' => 500, // grams
'destination' => 'tehran',
]);
Sanity-check the returned value against the official Iran Post tariff tables before trusting it in production — the published numbers can lag behind real rates, and this package had its last release in October 2022.
A good first use case: add a shipping-method selector to your checkout that fetches both sefarshi and pisthtaz prices for the cart's total weight and lets the customer choose.
Wrap the driver in your own service. Don't scatter Shipping::driver(...) calls through controllers. Create a thin application-level class so you can swap the library, add caching, and mock it in tests:
class PostageCalculator
{
public function options(int $weightGrams, string $destination): array
{
return collect(['sefarshi', 'pisthtaz'])
->mapWithKeys(fn ($driver) => [
$driver => Shipping::driver($driver)->calculate([
'weight' => $weightGrams,
'destination' => $destination,
]),
])
->all();
}
}
Cache aggressively. Tariffs change slowly but the call may hit a remote service. Cache per (driver, weight-bucket, destination) for a day:
$cacheKey = "postage:{$driver}:{$destination}:{$weightGrams}";
return Cache::remember($cacheKey, now()->addDay(), fn () => Shipping::driver($driver)->calculate([...]));
Round weight into tariff brackets before caching. Postal pricing is banded (e.g. up to 500 g, up to 1 kg). Bucketing keeps your cache-key space small and matches how the carrier actually prices:
$bucketed = (int) (ceil($weightGrams / 500) * 500);
Persist the chosen method on the order, not just the price. Store shipping_driver, shipping_cost, and the weight snapshot at purchase time so historical orders remain accurate when tariffs change later.
Graceful degradation in checkout. Fetch quotes behind a try/catch; if a driver fails, drop that option rather than breaking the whole page. If all drivers fail, fall back to a flat rate or "contact us" option.
Testing. Because this hits an external service, inject your PostageCalculator via the container and bind a fake in tests returning fixed quotes. Never let unit tests make real HTTP calls.
weight is grams or kilograms. Iranian post tariffs are usually quoted per-gram bands, but a silent 1000× error is the classic bug here. Add a docblock and an assertion in your wrapper.composer require complains, check the illuminate/support constraint and consider a fork with a widened constraint.'sefarshi', 'pisthtaz') — a Shipping::driver('Pishtaz') typo can throw an unhelpful "driver not found" exception. Validate driver values against an allowlist in your form requests.config/shipping.php, run php artisan config:clear (or config:cache in deploy). Stale cached config is the #1 "my credentials aren't being read" cause.PostageUnavailableException so your checkout logic isn't coupled to the vendor's namespace — makes swapping the library later trivial.src/Drivers/ — implementing the same interface and registering it in config is the intended extension path, rather than forking.0 < cost < someCeiling) so a bad response doesn't silently discount shipping to zero.How can I help you explore Laravel packages today?