Creating a Shipping Method in Magento 2

There are existing extensions available for many of the shipping carriers that you may choose to utilize on your Magento 2 site, but what about a shipping method of your own? Adding a custom shipping method like this is a straightforward approach that requires only a handful of files in order to implement – five, to be exact.

Let’s create a ‘Customer Pickup’ shipping method that is used to provide customers who are local to our warehouse the option of picking up their order as opposed to paying for shipping.

Our new shipping method will get its own extension, and as with other custom extensions, it will reside within a vendor folder in our site’s appcode folder. We will be using the following path for our new extension:appcodeClassyLlamaCustomerPickup For the remainder of this article, all file and path references will be in relation to the CustomerPickup folder.

We start by creating the two files that are required for all custom extensions: registration.php and module.xml. I will assume you are familiar enough with Magento 2 development to know what these two files are and why they are required, so I won’t go into depth about that here. If you are new to creating extensions in Magento 2, the DevDocs’ PHP Developer Guide is a must-read.

Configuration files

First, we have to tell Magento about our new shipping method’s existence; this is accomplished with the system.xml and config.xml files. The system.xml file will define the configuration settings available for our shipping method in the Magento 2 admin (in STORES > Configuration > SALES > Shipping Methods) and the config.xml file will set default values for those settings.

There are certain configuration settings for shipping carriers that are expected by Magento and thus should be included in all shipping methods:

  • active – This boolean value indicates whether this shipping method is active or not.
  • model – The path to the shipping method’s model.
  • title – The name of the shipping “carrier” that will be displayed in the frontend.
  • sallowspecific – This boolean value indicates whether this shipping method should be available for all countries or only those specified.
  • sort_order – This parameter tells Magento wherein the list of available shipping methods this method will display.

There are some additional settings we are including as well:

  • price – This shipping method will have a price of $0.00.
  • name – The name of this shipping method to be displayed to the customer.
  • showmethod – Whether or not to show this shipping method even when not applicable.

Our extension’s system.xml file has a default structure that should be followed:

  • Configuration elements must be created using the following node structure:
    <system>
    <section>
    <group>
  • The <section> node must have an id value of "carriers".
  • The <group> node must have an id value equal to our new shipping code, in this case customerpickup.

By building the XML file in this manner, Magento knows where to place the configuration settings in the backend (in this case with the other shipping methods) and how to retrieve them when called.

etcadminhtmlsystem.xml

<?xml version="1.0"?>
<!--
/**
 * @category    ClassyLlama
 * @copyright   Copyright (c) 2017 Classy Llama Studios, LLC
 */
 -->
<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="customerpickup" translate="label" type="text" sortOrder="0" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Customer Pickup</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model>MagentoConfigModelConfigSourceYesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="name" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Method Name</label>
                </field>
                <field id="price" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Price</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="showmethod" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Method if Not Applicable</label>
                    <source_model>MagentoConfigModelConfigSourceYesno</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

Our extension’s config.xml (the file that defines the default values for the settings defined in system.xml) also has layout rules that must be followed:

  • Configuration elements must be created under the main node of <default> with a child node of <carriers> in a child node named with the shipping method code we’ve created, in this case <customerpickup>.

etcconfig.xml

<?xml version="1.0"?>
<!--
/**
 * @category    ClassyLlama
 * @copyright   Copyright (c) 2017 Classy Llama Studios, LLC
 */
 -->
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <carriers>
            <customerpickup>
                <active>1</active>
                <model>ClassyLlamaCustomerPickupModelCarrierCustomerPickup</model>
                <name>Customer Pickup</name>
                <price>0.00</price>
                <title>Free Shipping</title>
                <sallowspecific>0</sallowspecific>
                <sort_order>100</sort_order>
            </customerpickup>
        </carriers>
    </default>
</config>

The Shipping Method Class

At this point, we’ve created the extension, but it doesn’t yet do anything for us. To get the extension working for us, we need to create the model at appcodeClassyLlamaCustomerPickupModelCarrierCustomerPickup.php.

There are some specific items worth noting when creating our custom shipping method’s class:

  • Every shipping method’s class in Magento 2 must extend MagentoShippingModelCarrierAbstractCarrier and implement MagentoShippingModelCarrierCarrierInterface.
  • Every shipping method’s class must include a minimum of two methods: getAllowedMethods and collectRates.
    • The method getAllowedMethods:
      • Must return an array of the available shipping options for our shipping method (e.g. ‘Standard Delivery”, ‘Expedited Delivery’).
      • Can include as few or as many shipping options as are applicable to the method. In our example case, there is only the one – ‘Customer Pickup’.
    • The method collectRates returns either:
      • A boolean false, which effectively disables the shipping method, excluding it from a list of shipping options.
      • An instance of MagentoShippingModelRateResult instantiated via a Magento factory.
        • If returning an instance of Result, it can have as many, or as few, methods appended to it as is applicable; each appended method is a factory-generated instance of MagentoQuoteModelQuoteAddressRateResultMethod. This model represents a shipping method and has its details set within it (e.g. Carrier code and title, method code and title, Price). In our example below, we are only attaching a single method, but the process can be repeated as many times as necessary.
  • The $_code variable in the class must be set to the unique shipping code chosen for our custom shipping method, in this case customerpickup.
  • The method $this->getConfigData is used to retrieve the configuration settings that are defined in the extension’s system.xml file (e.g. $this->getConfigData(‘name’) will return the value set for the method’s name).

app/code/ClassyLlama/CustomerPickup/Model/Carrier/CustomerPickup.php

<?php
/**
 * @category    ClassyLlama
 * @copyright   Copyright (c) 2017 Classy Llama Studios, LLC
 */

namespace ClassyLlamaCustomerPickupModelCarrier;

use MagentoQuoteModelQuoteAddressRateRequest;
use MagentoShippingModelRateResult;

class CustomerPickup
    extends MagentoShippingModelCarrierAbstractCarrier
    implements MagentoShippingModelCarrierCarrierInterface
{
    /**
     * Constant defining shipping code for method
     */
    const SHIPPING_CODE = 'customerpickup';

    /**
     * @var string
     */
    protected $_code = self::SHIPPING_CODE;

    /**
     * @var MagentoShippingModelRateResultFactory
     */
    protected $rateResultFactory;

    /**
     * @var MagentoQuoteModelQuoteAddressRateResultMethodFactory
     */
    protected $rateMethodFactory;

    /**
     * @param MagentoFrameworkAppConfigScopeConfigInterface $scopeConfig
     * @param MagentoQuoteModelQuoteAddressRateResultErrorFactory $rateErrorFactory
     * @param PsrLogLoggerInterface $logger
     * @param MagentoShippingModelRateResultFactory $rateResultFactory
     * @param MagentoQuoteModelQuoteAddressRateResultMethodFactory $rateMethodFactory
     * @param array $data
     */
    public function __construct(
        MagentoFrameworkAppConfigScopeConfigInterface $scopeConfig,
        MagentoQuoteModelQuoteAddressRateResultErrorFactory $rateErrorFactory,
        PsrLogLoggerInterface $logger,
        MagentoShippingModelRateResultFactory $rateResultFactory,
        MagentoQuoteModelQuoteAddressRateResultMethodFactory $rateMethodFactory,
        array $data = []
    ) {
        $this->rateResultFactory = $rateResultFactory;
        $this->rateMethodFactory = $rateMethodFactory;
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
    }

    /**
     * @return array
     */
    public function getAllowedMethods()
    {
        return [self::SHIPPING_CODE => $this->getConfigData('name')];
    }

    /**
     * @param RateRequest $request
     * @return bool|Result
     */
    public function collectRates(RateRequest $request)
    {
        if (!$this->getActiveFlag()) {
            // This shipping method is disabled in the configuration
            return false;
        }
        
        // Check whether order is eligible for customer pickup
            // Add logic here to determine if this order is eligible for this shipping method
            // e.g. Is the entire order in stock? Does the customer have a local address?
    
        // If the order is not eligible, return false
        // Otherwise, continue to build and return $result which includes our shipping method's details

        /** @var MagentoShippingModelRateResult $result */
        $result = $this->rateResultFactory->create();

        /** @var MagentoQuoteModelQuoteAddressRateResultMethod $method */
        $method = $this->rateMethodFactory->create();

        $method->setCarrier(self::SHIPPING_CODE);
        // Get the title from the configuration, as defined in system.xml
        $method->setCarrierTitle($this->getConfigData('title'));

        $method->setMethod(self::SHIPPING_CODE);
        // Get the title from the configuration, as defined in system.xml
        $method->setMethodTitle($this->getConfigData('name'));

        // Get the price from the configuration, as defined in system.xml
        $amount = $this->getConfigData('price');

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

        $result->append($method);

        return $result;
    }
}

Our new shipping method can be customized however we need it to be. For example, we could check whether any items in the customer’s order are being drop shipped, and if so, not offer ‘Customer Pickup’ as an available shipping method; we could also disable our shipping method for orders placed from outside of specific regions.

Configure and test the shipping method

After enabling our new extension and clearing the cache, we can visit our site’s admin section (e.g. https://mystore.xyz/backend/admin) and head to the Shipping Methods section at STORES > Configuration > SALES > Shipping Methods. We should now see a page similar to the one below, with our new shipping method’s default configuration settings we defined earlier in the system.xml file.

To test our new shipping method, we just have to head to our store’s website like a customer would, create a qualifying order (don’t forget what rules you had previously put in place in the model), and head to the shopping cart. As you can see in the screenshot below, our new shipping method is available for us to choose.

If we proceed to checkout, we see in the screenshot below that we again have the option of Customer Pickup as our shipping method.

Conclusion

We have now completed the creation, configuration, and testing of a custom shipping method that allows our local customers to save on the cost of shipping and pick their qualifying orders up at our warehouse at their convenience.

This tutorial can be used to tailor your online store’s shipping logic to match what is right for you and your business, rather than being tied to native rate models that may not fit every scenario. Custom shipping methods can be a powerful tool; and since the creation process is straightforward, it makes for a handy tool in your Magento tool belt!

Share it

Topics

Related Posts

Google and Yahoo Have New Requirements for Email Senders

What ROAS Really Means

Everything You Need to Know About Updating to Google Analytics 4

Contact Us