Magento 2 Create Shipping Method

Magento 2 is a rich eCommerce platform and it also supports few shipping methods in theΒ checkout process. However, they are not enough to make you comfortable. In order to be proportional with your development in the future, the customization of shipping methods is really crucial. Therefore, Magento 2 Create Shipping Method is built to make all easier.

With the simple explanation, it is accessible to follow step-by-step and complete the creation of new shipping methods.

All generated shipping methods are stored in Magento Admin Panel.

Please go toΒ Stores > Settings > Configuration > Sales > Delivery MethodsΒ to find and enable it on the storefront. But hold on, access the fileΒ /Model/Carries/Generatedshippingmethod.phpΒ in which you can set the specific shipping cost for eachΒ shipping method.

Namely to create the shipping method, please keep tracking on the following steps.

To add a new shipping carrier to the Magento checkout:

  1. Create a new module
  2. Add the carrier configuration
  3. Create the carrier model
  4. Enable the module

Step 1: Create a new module

The example module for use here isΒ Thecoachsmb_CustomShipping.

1.1 Register Module

Source code of app/code/Thecoachsmb/CustomShipping/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Thecoachsmb_CustomShipping',
    __DIR__
);

1.2 Create composer.json

Source code ofΒ app/code/Thecoachsmb/CustomShipping/composer.json

{
    "name": "thecoachsmb/custom-shipping",
    "description": "Custom shipping module",
    "require": {
        "php": "~7.2.0||~7.3.0",
        "magento/framework": "102.0.*",
        "magento/module-backend": "101.0.*",
        "magento/module-catalog": "103.0.*",
        "magento/module-config": "101.1.*",
        "magento/module-directory": "100.3.*",
        "magento/module-quote": "101.1.*",
        "magento/module-sales": "102.0.*",
        "magento/module-sales-rule": "101.1.*",
        "magento/module-shipping": "100.3.*",
        "magento/module-store": "101.0.*"
    },
    "type": "magento2-module",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Thecoachsmb\\CustomShipping\\": ""
        }
    },
    "version": "1.0.0"
}

1.3 Declare the Module

Source code of app/code/Thecoachsmb/CustomShipping/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Thecoachsmb_CustomShipping" >
        <sequence>
            <module name="Magento_Store"/>
            <module name="Magento_Sales"/>
            <module name="Magento_Quote"/>
            <module name="Magento_SalesRule"/>
        </sequence>
    </module>
</config>

Step 2: Add the module configuration

To add a module configuration use the following source code snippets.

2.1 Show Configuration in Stores Configuration

Source codeΒ  of app/code/Thecoachsmb/CustomShipping/etc/adminhtml/system.xml

TheΒ system.xmlΒ source code declares custom shipping module options:

  • Enabled
  • Title
  • Method Name
  • Shipping Cost
  • Ship to Applicable Countries
  • Ship to Specific Countries
  • Show Method if Not Applicable
  • Sort Order
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="carriers" translate="label" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1">
            <group id="customshipping" translate="label" type="text" sortOrder="0" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Thecoachsmb Custom Shipping Method</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="name" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Method Name</label>
                </field>
                <field id="price" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
                    <label>Price</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="handling_type" translate="label" type="select" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
                    <label>Calculate Handling Fee</label>
                    <source_model>Magento\Shipping\Model\Source\HandlingType</source_model>
                </field>
                <field id="handling_fee" translate="label" type="text" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Handling Fee</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Title</label>
                </field>
                <field id="sallowspecific" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
                    <label>Ship to Applicable Countries</label>
                    <frontend_class>shipping-applicable-country</frontend_class>
                    <source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="91" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Ship to Specific Countries</label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                    <can_be_empty>1</can_be_empty>
                </field>
                <field id="showmethod" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Method if Not Applicable</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                    <frontend_class>shipping-skip-hide</frontend_class>
                </field>
                <field id="specificerrmsg" translate="label" type="textarea" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Displayed Error Message</label>
                </field>
            </group>
        </section>
    </system>
</config>

Source code of app/code/Thecoachsmb/CustomShipping/etc/config.xml

First of all, shipping method should be defined in fileΒ config.xml, like in a below. Without it, it can’t work. The main node in xml is β€œdefault” and child of node β€œcarriers” should have the same name as property $_code in shipping class Thecoachsmb\CustomShipping\Model\Carrier\Customshipping.

TheΒ config.xml file specifies default values for custom shipping module options and the shipping module model, Thecoachsmb\CustomShipping\Model\Carrier\Customshipping:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <carriers>
            <customshipping>
                <active>1</active>
                <sallowspecific>0</sallowspecific>
                <model>Thecoachsmb\CustomShipping\Model\Carrier\Customshipping</model>
                <name>Thecoachsmb Custom Shipping Method</name>
                <price>10.00</price>
                <title>Thecoachsmb Custom Shipping Method</title>
                <specificerrmsg>This shipping method is not available. To use this shipping method, please contact us.</specificerrmsg>
                <handling_type>F</handling_type>
            </customshipping>
        </carriers>
    </default>
</config>
In our config.xml you will notice XML node β€œmodel” which define php class β€œThecoachsmb\CustomShipping\Model\Carrier\Customshippingβ€œ. This model class is charged for shipping method. In this class should be implemented all logic for shipping calculation.

Step 3: Create the carrier model

In this example, the Thecoachsmb\CustomShipping\Model\Carrier\CustomshippingΒ class is a skeleton of a carrier model. You can extend it to fit your needs.

The carrier class implements theΒ CarrierInterfaceΒ interface and retrieves all available shipping methods in theΒ getAllowedMethodsΒ function.

TheΒ collectRatesΒ function returns theΒ \Magento\Shipping\Model\Rate\ResultΒ object if the carrier method is available on checkout. Otherwise, it returnsΒ falseβ€”the carrier method is not applicable to the shopping cart.

Source code ofΒ app/code/Thecoachsmb/CustomShipping/Model/Carrier/Customshipping.php

<?php
namespace Thecoachsmb\CustomShipping\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Rate\Result;

class Customshipping extends \Magento\Shipping\Model\Carrier\AbstractCarrier implements
    \Magento\Shipping\Model\Carrier\CarrierInterface
{
    /**
     * @var string
     */
    protected $_code = 'customshipping';

    /**
     * @var \Magento\Shipping\Model\Rate\ResultFactory
     */
    protected $_rateResultFactory;

    /**
     * @var \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
     */
    protected $_rateMethodFactory;

    /**
     * Shipping constructor.
     *
     * @param \Magento\Framework\App\Config\ScopeConfigInterface          $scopeConfig
     * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory  $rateErrorFactory
     * @param \Psr\Log\LoggerInterface                                    $logger
     * @param \Magento\Shipping\Model\Rate\ResultFactory                  $rateResultFactory
     * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory
     * @param array                                                       $data
     */
    public function __construct(
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
        \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
        array $data = []
    ) {
        $this->_rateResultFactory = $rateResultFactory;
        $this->_rateMethodFactory = $rateMethodFactory;
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
    }

    /**
     * get allowed methods
     * @return array
     */
    public function getAllowedMethods()
    {
        return [$this->_code => $this->getConfigData('name')];
    }

    /**
     * @return float
     */
    private function getShippingPrice()
    {
        $configPrice = $this->getConfigData('price');

        $shippingPrice = $this->getFinalPriceWithHandlingFee($configPrice);

        return $shippingPrice;
    }

    /**
     * @param RateRequest $request
     * @return bool|Result
     */
    public function collectRates(RateRequest $request)
    {
        if (!$this->getConfigFlag('active')) {
            return false;
        }

        /** @var \Magento\Shipping\Model\Rate\Result $result */
        $result = $this->_rateResultFactory->create();

        /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
        $method = $this->_rateMethodFactory->create();

        $method->setCarrier($this->_code);
        $method->setCarrierTitle($this->getConfigData('title'));

        $method->setMethod($this->_code);
        $method->setMethodTitle($this->getConfigData('name'));

        $amount = $this->getShippingPrice();

        $method->setPrice($amount);
        $method->setCost($amount);

        $result->append($method);

        return $result;
    }
}

In order to properly write php class for shipping method, you should respect some Magento 2 rules. Every Magento 2 shipping class should extend β€œ\Magento\Shipping\Model\Carrier\AbstractCarrier” and implement β€œ\Magento\Shipping\Model\Carrier\CarrierInterfaceβ€œ.

In shipping model you need to create at least two php methods: β€œgetAllowedMethods” and β€œcollectRatesβ€œ. This methods are required by abstract class and interface. Also, you should define propertyΒ $_code with value. In our case, that is β€œcustomshippingβ€œ. It’s related to config.xml and node structure.

Php method β€œcollectRates” accepts parameter β€œ$request” which is instance of class β€œMagento\Quote\Model\Quote\Address\RateRequestβ€œ. This class contains all information about items in cart/quote, weight, shipping address and so on. In this method you can implement all logic for shipping calculation. From this method you can call other services for shipping price calculation but it depends about your integration.

Step 4: Enable the module

Run the commands below to register Thecoachsmb_CustomShipping module:

php bin/magento module:enable Thecoachsmb_CustomShipping
php bin/magento setup:upgrade && php bin/magento se:s:d -f && php bin/magento c:f

After successful running the commands, you will see the output as below:


Conclusion

In this article, we are learningΒ  the process of creating custom shipping method. Follow the article, step by step. Please feel free to comment below for any queries or concerns.