Creating Your First Shipping Method
This page builds the Webkul\CustomExpressShipping package: a carrier that charges a configurable rate per order or per item, with its settings in the admin.
Package Generator
The Package Generator can scaffold a shipping method package for you. This page builds it by hand, so you see every file and how it's registered.
What You'll Build
packages/Webkul/CustomExpressShipping
└── src
├── Carriers
│ └── CustomExpressShipping.php
├── Config
│ ├── carriers.php
│ └── system.php
└── Providers
└── CustomExpressShippingServiceProvider.phpStep 1: Create the Package Structure
From the Bagisto root, create the package's folders:
mkdir -p packages/Webkul/CustomExpressShipping/src/{Carriers,Config,Providers}Step 2: Add the Carrier Configuration
This file registers the carrier and holds its default settings.
File: packages/Webkul/CustomExpressShipping/src/Config/carriers.php
<?php
return [
'custom_express_shipping' => [
'code' => 'custom_express_shipping',
'title' => 'Express Delivery (1-2 Days)',
'description' => 'Premium express shipping with tracking and insurance',
'active' => true,
'default_rate' => '19.99',
'type' => 'per_order',
'class' => 'Webkul\CustomExpressShipping\Carriers\CustomExpressShipping',
],
];Core reads only class from this file when it collects rates. The other keys are defaults that the carrier's getConfigData() returns until an admin saves a value, which is why the method is offered as soon as the package is registered.
Step 3: Create the Carrier Class
calculate() returns the rate for the current cart, or false when the method doesn't apply.
File: packages/Webkul/CustomExpressShipping/src/Carriers/CustomExpressShipping.php
<?php
namespace Webkul\CustomExpressShipping\Carriers;
use Webkul\Checkout\Facades\Cart;
use Webkul\Checkout\Models\CartShippingRate;
use Webkul\Shipping\Carriers\AbstractShipping;
class CustomExpressShipping extends AbstractShipping
{
/**
* Shipping method carrier code, must match the carriers.php key.
*
* @var string
*/
protected $code = 'custom_express_shipping';
/**
* Shipping method code, the value the checkout posts back.
*
* @var string
*/
protected $method = 'custom_express_shipping_custom_express_shipping';
/**
* Calculate the rate for the current cart.
*
* @return CartShippingRate|false
*/
public function calculate()
{
if (! $this->isAvailable()) {
return false;
}
$cart = Cart::getCart();
$cartShippingRate = new CartShippingRate;
$cartShippingRate->carrier = $this->getCode();
$cartShippingRate->carrier_title = $this->getConfigData('title');
$cartShippingRate->method = $this->getMethod();
$cartShippingRate->method_title = $this->getConfigData('title');
$cartShippingRate->method_description = $this->getConfigData('description');
$baseRate = $this->getConfigData('default_rate');
$finalRate = $baseRate;
if ($this->getConfigData('type') === 'per_unit') {
$totalItems = 0;
foreach ($cart->items as $item) {
if ($item->getTypeInstance()->isStockable()) {
$totalItems += $item->quantity;
}
}
$finalRate = $baseRate * $totalItems;
}
$cartShippingRate->price = core()->convertPrice($finalRate);
$cartShippingRate->base_price = $finalRate;
return $cartShippingRate;
}
}The class has the shape of core's Webkul\Shipping\Carriers\FlatRate. base_price is in the base currency and price is converted to the channel currency with core()->convertPrice(). Per-item pricing counts only the items that ship, asking each cart item's own type instance, which is right for a configurable variant.
Step 4: Add the System Configuration
This section gives the carrier its settings under Configure → Sales → Shipping Methods.
File: packages/Webkul/CustomExpressShipping/src/Config/system.php
<?php
return [
[
'key' => 'sales.carriers.custom_express_shipping',
'name' => 'Custom Express Shipping',
'info' => 'Configure the Custom Express Shipping method settings.',
'sort' => 3,
'fields' => [
[
'name' => 'active',
'title' => 'Enabled',
'type' => 'boolean',
'channel_based' => true,
'locale_based' => false,
],
[
'name' => 'title',
'title' => 'Method Title',
'type' => 'text',
'depends' => 'active:1',
'validation' => 'required_if:active,1',
'channel_based' => true,
'locale_based' => true,
],
[
'name' => 'description',
'title' => 'Description',
'type' => 'textarea',
'depends' => 'active:1',
'channel_based' => true,
'locale_based' => true,
],
[
'name' => 'default_rate',
'title' => 'Base Rate',
'type' => 'text',
'depends' => 'active:1',
'validation' => 'required_if:active,1|numeric|min:0',
'channel_based' => true,
'locale_based' => false,
],
[
'name' => 'type',
'title' => 'Pricing Type',
'type' => 'select',
'depends' => 'active:1',
'options' => [
[
'title' => 'Per Order (Flat Rate)',
'value' => 'per_order',
],
[
'title' => 'Per Item',
'value' => 'per_unit',
],
],
'channel_based' => true,
'locale_based' => false,
],
],
],
];The key must be sales.carriers. followed by the carrier code, and info is required. Keep the boolean active field: saving the Shipping Methods page reads it from every carrier and refuses to save when none is on. Understanding Carrier Configuration explains sort, depends and the validation rules.
Step 5: Create the Service Provider
The provider merges both configuration files.
File: packages/Webkul/CustomExpressShipping/src/Providers/CustomExpressShippingServiceProvider.php
<?php
namespace Webkul\CustomExpressShipping\Providers;
use Illuminate\Support\ServiceProvider;
class CustomExpressShippingServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->mergeConfigFrom(
dirname(__DIR__).'/Config/carriers.php',
'carriers'
);
$this->mergeConfigFrom(
dirname(__DIR__).'/Config/system.php',
'core'
);
}
/**
* Bootstrap services.
*/
public function boot(): void {}
}carriers.php joins config('carriers'), which the checkout reads; system.php joins config('core'), which the admin configuration reads.
Step 6: Register the Package
Add the namespace to the root composer.json and the provider to bootstrap/providers.php. Package Development explains both files.
File: composer.json
{
"autoload": {
"psr-4": {
"Webkul\\CustomExpressShipping\\": "packages/Webkul/CustomExpressShipping/src"
}
}
}The block shows only the entry to add; keep the others.
File: bootstrap/providers.php
<?php
use App\Providers\AppServiceProvider;
use Webkul\CustomExpressShipping\Providers\CustomExpressShippingServiceProvider;
return [
AppServiceProvider::class,
// ...
CustomExpressShippingServiceProvider::class,
];Then regenerate the autoloader and clear the cached configuration:
composer dump-autoload
php artisan optimize:clearTest It
- In the admin, open Configure → Sales → Shipping Methods. A Custom Express Shipping section follows Free Shipping and Flat Rate.
- Add a simple product to the cart, go to checkout and enter a shipping address. Express Delivery (1-2 Days) is offered at the base rate of 19.99, from the defaults in
carriers.php. - Set Pricing Type to Per Item and save. Back on the shipping step, the rate is the base rate times the quantity of items that ship.
- Check out a cart that holds only a virtual or downloadable product. The checkout skips the shipping step, because it collects rates only for a cart with stockable items.
- Cover
calculate()with a Pest test: core'spackages/Webkul/Shipping/tests/Unit/CarriersTest.phpsaves carrier settings withsetConfig()and callscalculate(); see Testing with Pest.
Things to Watch
- The checkout asks for rates more than once, and the cart page's shipping estimator asks with only a country, state and postcode. Keep
calculate()cheap and free of side effects; see Understanding the Carrier Class. - A carrier is created with
new, not through the container, so constructor injection fails. Resolve services withapp()insidecalculate(). - Use a code of your own.
mergeConfigFrom()keeps keys that are already set, so reusingflatrateorfreeleaves core's carrier in place. To change a core carrier instead, see Understanding Carrier Configuration. - A saved setting applies to one channel, and a translated field such as the title to one locale. Saving the section replaces the
carriers.phpdefaults only for the channel and locale selected in the switchers.
Next Step
The carrier works. Next, see every key carriers.php and system.php accept and how a setting is resolved.
Continue to: Understanding Carrier Configuration
