Fixing Magento 2.2.3 Checkout Issues with Magento 2 Extension Attributes

Fixing Magento 2.2.3 Checkout Issues with Magento 2 Extension Attributes

10 min read

Send to you:

Let’s discuss Magento® 2.2.3 – or, rather, let’s discuss its bugs. We will examine checkout errors related to extension attributes of the entity \Magento\Quote\Model\Quote\Address, and how to fix them. While working on a client project, we encountered a problem when transferring the shipping module to the new Magento 2.2.3 version.

Today, we will tell you more about this problem and how we solved it.

The Shipping Module for Magento 2

A module was implemented for stores on Magento 2.1.9 that allowed order shipping totals to be calculated when the products in the order were shipped from different warehouses using different delivery methods.

The following capabilities are implemented in the module:

  • Store administrators can assign products to warehouses.
  • Products are grouped by the warehouse on the checkout page.
  • Customers can choose a different delivery method allowed by the administrator for each group of products from a warehouse.
  • Total delivery amounts are derived using a specialized delivery method.
  • Information about each chosen shipping method is stored for shopping carts and is copied to orders.
  • Separate shipments can be created for each warehouse.
  • If the delivery service allows, address labels can be printed. The USPS revoked this ability as of February 23, 2018, but many other delivery services support it.

Task: Additional information must be stored to a cart’s delivery address, and the information must be copied to the order using this module.

Solution: We used extension attributes since as of today this is the only correct way to solve such issues.

Checkout Error

The module was initially implemented for Magento 2.1.9 CE. Then, we installed it on Magento 2.2.1 CE. In the Magento 2.2.3 CE module, shipping methods stopped loading at checkout. The delivery method request returned a 500 error at checkout.

This is the error text in the web server’s log:

[Wed Mar 07 15:06:36.693548 2018] [:error] [pid 4286] [client 127.0.0.1:45463] PHP Fatal error: Uncaught Error: Call to a member function getOriginShippingDetails() on array in...

Why is this happening? Eventually, we were able to establish the reason for the error.

Reason for the Error

For cart and order addresses, the module stores information about grouping by the warehouse to extension attributes, defined as follows:

<extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
    <attribute code="origin_shipping_details" type="string">
        <join reference_table="origin_quote_address" join_on_field="address_id" reference_field="address_id">
            <field column="origin_shipping_details">origin_shipping_details</field>
        </join>
    </attribute>
</extension_attributes>
<extension_attributes for="Magento\Sales\Api\Data\OrderAddressInterface">
    <attribute code="origin_shipping_details" type="string">
        <join reference_table="origin_order_address" join_on_field="entity_id" reference_field="entity_id">
            <field column="origin_shipping_details">origin_shipping_details</field>
        </join>
    </attribute>
</extension_attributes>

Nothing out of the ordinary.

There is an additional table where the values of the extension attributes are stored. Then they are automatically joined to the collection of addresses using join processor in the collection load event. There are always two addresses, especially for delivery methods.

This happens in the following way:

class AddressCollectionLoad implements \Magento\Framework\Event\ObserverInterface {

    protected $_joinProcessor;

    public function __construct(\Magento\Framework\Api\ExtensionAttribute\JoinProcessor $joinProcessor){
        $this->_joinProcessor = $joinProcessor;
    }
    public function execute(\Magento\Framework\Event\Observer $observer){
        if($collection = $observer->getEvent()->getQuoteAddressCollection()){
            $this->_joinProcessor->process($collection);
        }
    }
}

The data in an array is serialized in JSON for the convenience of storage. In Magento 2.2.3 CE, a problem arose when reading this value. The error was returned by the following code:

if($extAttribute = $address->getExtensionAttributes()){
    $details = $extAttribute->getOriginShippingDetails();
}

The $address object type was checked. The getExtensionAttributes() method had to return an object that implemented the \Magento\Quote\Api\Data\AddressExtensionInterface interface or null, as it had been written in the method description. Instead, it returned an array. We were unable to find the exact reason for this. However, we have several ideas.

The Temando_Shipping Module

Starting with Magento 2.2.2, the Temando_Shipping module was added to the Magento core. This also implements an extension attribute for cart and order address.

<extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
    <attribute code="checkout_fields" type="Magento\Framework\Api\AttributeInterface[]" />
</extension_attributes>
<extension_attributes for="Magento\Sales\Api\Data\OrderAddressInterface">
    <attribute code="checkout_fields" type="Magento\Framework\Api\AttributeInterface[]" />
</extension_attributes>

Joining Extension Attribute Error

This module was enabled, but it was not configured, so it was not essentially used. Since the Temando attribute is not scalar, but rather is an array of objects, it’s not possible to automatically join the attribute to the collection of addresses. Reading the values of this attribute is entirely up to the module developer.

Assumption 1: When trying to load the value of this attribute to the address, for some reason an array was written to the field extension_attributes of \Magento\Quote\Model\Quote\Address, instead of an object.

Assumption 2: For some reason, Join Processor, the standard extension attribute loader, initialized an array instead of an object. We rewrote the above code:

if($extAttribute = $address->getExtensionAttributes()){
    if(is_object($extAttribute)){
        $details = $extAttribute->getOriginShippingDetails();
    }else{
   if(is_array($extAttribute)&&isset($extAttribute['origin_shipping_details'])){
            $details = $extAttribute['origin_shipping_details'];
        }
    }
}

It’s a bit more complicated, but it does what it has to do.

Ordering Error

So, we figured out joining attributes. But there’s more. When trying to place an order, a different error was written to the server log:

[Wed Mar 07 15:33:00.499754 2018] [:error] [pid 9705] [client 127.0.0.1:45728] PHP Fatal error: Uncaught TypeError: Argument 1 passed to Temando\\Shipping\\Model\\Checkout\\Address::setServiceSelection() must be of the type array, null given, called in /var/www/mercyrobes2/vendor/temando/module-shipping-m2/Observer/SaveCheckoutFieldsObserver.php on line 73 and defined in /var/www/mercyrobes2/vendor/temando/module-shipping-m2/Model/Checkout/Address.php:78\nStack trace:\n#0 /var/www/mercyrobes2/vendor/temando/module-shipping-m2/Observer/SaveCheckoutFieldsObserver.php(73): Temando\\Shipping\\Model\\Checkout\\Address->setServiceSelection(NULL)\n#1 /var/www/mercyrobes2/vendor/magento/framework/Event/Invoker/InvokerDefault.php(72): Temando\\Shipping\\Observer\\SaveCheckoutFieldsObserver->execute(Object(Magento\\Framework\\Event\\Observer))\n#2 /var/www/mercyrobes2/vendor/magento/framework/Event/Invoker/InvokerDefault.php(60): Magento\\Framework\\Event\\Invoker\\InvokerDefault->_callObserverMethod(Object(Temando\\Shipping\\Observer\\SaveCheckoutFieldsObserver), Object(Magento\\Framework\\Event\\Observer))\n#3 /var/www/mercyrobes2/vendor/m in /var/www/mercyrobes2/vendor/temando/module-shipping-m2/Model/Checkout/Address.php on line 78, referer: http://mercyrobes2.loc/index.php/checkout/

Here, the error was definitely in the Temando module, and it had to be resolved.

This problem has already been brought up in a Github thread:

After analyzing the Temando module, it was established that the Temando\Shipping\Observer\SaveCheckoutFieldsObserver class is a handler of the event sales_quote_address_save_after. Its code is below

class SaveCheckoutFieldsObserver implements ObserverInterface
{
    /**
     * @var AddressRepositoryInterface
     */
    private $addressRepository;
    /**
     * @var AddressInterfaceFactory
     */
    private $addressFactory;
    /**
     * SaveCheckoutFieldsObserver constructor.
     * @param AddressRepositoryInterface $addressRepository
     * @param AddressInterfaceFactory $addressFactory
     */
    public function __construct(
        AddressRepositoryInterface $addressRepository,
        AddressInterfaceFactory $addressFactory
    ) {
        $this->addressRepository = $addressRepository;
        $this->addressFactory = $addressFactory;
    }
    /**
     * @param Observer $observer
     * @return void
     */
    public function execute(Observer $observer)
    {
        /** @var \Magento\Quote\Api\Data\AddressInterface|\Magento\Quote\Model\Quote\Address $quoteAddress */
        $quoteAddress = $observer->getData('quote_address');
        if ($quoteAddress->getAddressType() !== \Magento\Quote\Model\Quote\Address::ADDRESS_TYPE_SHIPPING) {
            return;
        }
        if (!$quoteAddress->getExtensionAttributes()) {
            return;
        }
        // persist checkout fields
        try {
            $checkoutAddress = $this->addressRepository->getByQuoteAddressId($quoteAddress->getId());
        } catch (NoSuchEntityException $e) {
            $checkoutAddress = $this->addressFactory->create(['data' => [
                AddressInterface::SHIPPING_ADDRESS_ID => $quoteAddress->getId(),
            ]]);
        }
        $extensionAttributes = $quoteAddress->getExtensionAttributes();
        $checkoutAddress->setServiceSelection($extensionAttributes->getCheckoutFields());
        $this->addressRepository->save($checkoutAddress);
    }
}

As you can see, this observer checks:

  • whether the address is a shipping address;
  • Whether there is any extension attribute in the address.

Then the entity is requested and created. It implements the extension attribute of Temando module. Specific operations are carried out with this entity up to saving this entity to the database. This entity has a Temando\Shipping\Model\Checkout\Address type, and implements setServiceSelection method the following way:

/**
 * @param \Magento\Framework\Api\AttributeInterface[] $services
 * @return void
 */
public function setServiceSelection(array $services)
{
    $this->setData(AddressInterface::SERVICE_SELECTION, $services);
}

Reason for the Ordering Error

The method input parameter must be an array of objects that implement \Magento\Framework\Api\AttributeInterface. Since the Temando module is not configured and is not used on this site, $extensionAttributes->getCheckoutFields() returns null. Then, a fatal type incompatibility error occurs. If the Temando_Shipping module was checked on Magento 2.2.2, the problem would exist there too, since the Temando_Shipping version in Magento 2.2.2 CE is the exact same as in Magento 2.2.3 CE. This use case was not checked in Magento before it had been included in the out-of-box solution.

The Solution to the Ordering Problem

This problem has a fairly simple solution. We have implemented the observer of the sales_quote_address_save_before event, which allows this case to be handled.

The handler of this event looks like this:

class AddressSaveBefore implements \Magento\Framework\Event\ObserverInterface
{
    public function execute(\Magento\Framework\Event\Observer $observer){
        if($address = $observer->getEvent()->getQuoteAddress()){
            if($attributes = $address->getExtensionAttributes()){
                $checkoutFields = $attributes->getCheckoutFields();
                if(is_null($checkoutFields)){
                    $attributes->setCheckoutFields(array());
                    }
                }
            }
        }
}

This observer checks if the value of the checkout_fields attribute is null, and if it is, it replaces it with an empty array. The error will no longer be returned, and the order can be placed successfully.

SUM MARY

Developers of third-party modules that implement extension attributes for \Magento\Quote\Model\Quote\Address are forced to include this use case in their modules and correct module errors that Magento includes in the core. This leads to an increase in time spent on development and support of Magento 2 extensions. On the other hand, this situation forces third-party developers to bolster their expertise and not slack off, inventing new creative approaches to their work with Magento 2. If you have any issues with your store in mind, we can help you with the Magento Support services.

Posted on: May 10, 2018

5.0/5.0

Article rating (8 Reviews)

Do you find this article useful? Please, let us know your opinion and rate the post!

  • Not bad
  • Good
  • Very Good
  • Great
  • Awesome