Custom Shipping Methods and Custom Cart Summary Fields in Magento 2

This entry was posted on February 12, 2020 by Željko Ivanović, Magento/PHP Full Stack Web Developer.

Custom Shipping and Cart Summary

Introduction

In this blog, we are going to make custom shipping methods and custom fields that will be shown in the cart summary. We will use some simple examples, that will contain just the necessary code. This is a piece of must-have knowledge for every developer and it's pretty simple. Every start is hard but it is a start!

Part 1: Custom Shipping Methods

Create the file app/code/Vendor/Module/Model/Carrier/Exampleshipping.php 

The code below is an example skeleton of how it needs to be done. Feel free to play with it, add your custom shipping options, custom prices, etc. 

<?php

namespace Vendor\Module\Model\Carrier;

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

class Exampleshipping extends \Magento\Shipping\Model\Carrier\AbstractCarrier implements
    \Magento\Shipping\Model\Carrier\CarrierInterface
{

    protected $_code = 'exampleshipping';

    protected $_isFixed = true;

    protected $_rateResultFactory;

    protected $_rateMethodFactory;

    /**
     * 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);
    }

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

        $shippingPrice = $this->getConfigData('price');

        $result = $this->_rateResultFactory->create();

        if ($shippingPrice !== false) {
            $method = $this->_rateMethodFactory->create();

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

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

            if ($request->getFreeShipping() === true ||  $request->getPackageQty() == $this->getFreeBoxes()) {
                $shippingPrice = '0.00';
            }

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

            $result->append($method);
        }

        return $result;
    }

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

The previous part is responsible for rendering shipping options for the custom shipping method. 

The following part is config.xml where we are going to state the model and options for this method.

Create a file in etc/config.xml

Add your code for config.xml, below is just an example:

<?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>
			<exampleshipping>
				<model>Vendor\Module\Model\Carrier\Exampleshipping</model>
				<active>0</active>
				<title>Exampleshipping</title>
				<name>Exampleshipping</name>
				<price>0.00</price>
				<specificerrmsg>This shipping method is not available. To use this shipping method, please contact us.</specificerrmsg>
				<sallowspecific>0</sallowspecific>
			</exampleshipping>
		</carriers>
	</default>
</config>

This code will set title to Exampleshipping, price to 0, and specificerrmsg to some custom text. The model should contain a link to the Model/Carrier file which is used for rendering that method and options.

And the last important thing for us and our module is to create a file at etc/adminhtml/system.xml with its example below. 

<?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" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="1000" translate="label">
			<group id="exampleshipping" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10" translate="label">
				<label>Exampleshipping</label>
				<field id="active" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10" translate="label" type="select">
					<label>Enabled</label>
					<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
				</field>
				<field id="name" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="20" translate="label" type="text">
					<label>Method Name</label>
				</field>
				<field id="price" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="30" translate="label" type="text">
					<label>Price</label>
					<validate>validate-number validate-zero-or-greater</validate>
				</field>
				<field id="sort_order" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="40" translate="label" type="text">
					<label>Sort Order</label>
				</field>
				<field id="title" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="50" translate="label" type="text">
					<label>Title</label>
				</field>
				<field id="sallowspecific" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="60" translate="label" type="select">
					<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" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="70" translate="label" type="multiselect">
					<label>Ship to Specific Countries</label>
					<can_be_empty>1</can_be_empty>
					<source_model>Magento\Directory\Model\Config\Source\Country</source_model>
				</field>
				<field id="specificerrmsg" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="80" translate="label" type="textarea">
					<label>Displayed Error Message</label>
				</field>

</group>
		</section>
	<system>
</config>

And with system.xml we can tell that our module is ready for displaying. Run the commands that are stated at the end of the page and refresh admin and frontend.
You should be able to see your custom shipping method and its options.

Part 2: Custom Fields at Cart Summary

Make preference in app/code/Vendor/Module/etc/di.xml

<preference for="Magento\Checkout\Block\Cart\LayoutProcessor" type="Vendor\Module\Model\Checkout\Block\Cart\Shipping" />

Create the following file:
app/code/Vendor/Module/Model/Checkout/Block/Cart/Shipping.php

Copy and paste the code from the core as we are going to extend this file
vendor/magento/module-checkout/Block/Cart/LayoutProcessor.php

In the function public function process($jsLayout), append more fields in $elements array which is already defined there. See an example of a field below.

'postcode' => [
   'visible' => true,
   'formElement' => 'input',
   'label' => __('Zip/Postal Code'),
   'value' => null
]

Instead of setting ‘visible’ to true you can make a function that will return some value, based on your needs, for example:

/**
* Show customField in Shipping Estimation
*
* @return bool
* @codeCoverageIgnore
*/
protected function isCustomFieldActive()
{
   return true;
}

Another example: 

/**
* Show City in Shipping Estimation
*
* @return bool
* @codeCoverageIgnore
*/
protected function isCityActive()
{
   return false;
}

Here are some images of this custom extension that we have built for both backend and frontend:

 

Extension Image 1

Extension Image 1

 

Extension Image 2

Extension Image 2

 

Bonus: Implement Observer for Saving Configuration in Admin

As a bonus, in this blog, we will provide some information on how to implement an observer that will be listening for saving the configuration in admin panel shipping method options. 

First, create events.xml in app/code/Vendor/Module/etc and add the following code:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
   <event name="admin_system_config_changed_section_carriers">
       <observer name="custom_admin_system_config_changed_section_carriers" instance="Vendor\Module\Observer\ConfigChange"/>
   </event>
</config>


After we have created the xml file, we will need to create the ConfigChange.php file, an example path: app/code/Vendor/Module/Observer/ConfigChange.php.

Assuming that you have created the file above, the files need to contain the class name like below.

/**
* Class ConfigChange
* On configuration change it validates entered data
*/
class ConfigChange implements ObserverInterface
{
...

The functions in the file need to be one for execute, and one for validating data, please refer below. And please add all the code you need in the try catch block.

public function execute(EventObserver $observer) { ... }
public function validateData($params) { ... 
	try {
	} catch (\Exception $exception) {
	      	 	  $this->response->setHttpResponseCode($exception->getCode());
	   return $this->messageManager->addError(__($exception->getMessage()));
	}
}

Add breakpoint after { in functions. After you go to the backend and save the shipping method settings your debugger will stop in these breakpoints. Go ahead and try it for yourself, feel free to play with it.  

After some major changes that require a deploy or clearing the cache run the commands below and refresh the frontend/admin page.

php bin/magento setup:di:compile
php bin/magento setup:upgrade
php bin/magento setup:static-content:deploy -f 
php bin/magento c:f

Conclusion

In this blog, we learned how to implement custom shipping methods, custom fields in cart summary (on the cart page), and how to make Observer that will monitor any changes that are being saved for that shipping method in the admin section. 

Stay tuned for more interesting blog posts from our team and myself. If you need any tech help, feel free to contact us at [email protected]

This entry was posted in Magento 2 and tagged Web Development, SyncIt Group, Magento 2, Web, Magento 2 Development, PHP, Magento 2 Admin, Shipping Methods, Custom Shipping Methods, Cart Summary, Custom Cart Summary, XML on February 12, 2020 by Željko Ivanović, Magento/PHP Full Stack Web Developer .