Broken Magento Shopping Cart: Cause and Solution

Broken Magento Shopping Cart: Cause and Solution

11 min read

Send to you:

Today, we will be examining the broken cart bug in Magento® 2. Below, you will see a step-by-step description of the Magento cart problem and a method for fixing it.

Description of the Problem

There is one Magento 2 bug inherited from Magento 1 that has not yet been fixed at the core level. Here is the gist of the bug: A customer goes to a site, logs in, and adds a product to their cart. Then, the customer may attempt to log in again after some time has passed. In Magento 1, the customer receives a message about a fatal error (administrators see a link to a report). The administrator will see the same fatal error if they want to edit or view this customer’s account. In Magento 2, the customer can’t add products to their cart and go to checkout, but static pages, categories, and product pages are available. When this happens, the server administrator sees the following message in the global error log:

[Mon Aug 06 16:49:26.148250 2018] [:error] [pid 10400] [client 127.0.0.1:49255] PHP Fatal error: Uncaught Error: Call to a member function getThumbnail() on null in /var/www/zenzii_beta/vendor/magento/module-configurable-product/CustomerData/ConfigurableItem.php:66\nStack trace:\n#0 /var/www/zenzii_beta/vendor/magento/module-checkout/CustomerData/DefaultItem.php(78): Magento\\ConfigurableProduct\\CustomerData\\ConfigurableItem->getProductForThumbnail()\n#1 /var/www/zenzii_beta/vendor/magento/module-checkout/CustomerData/AbstractItem.php(31): Magento\\Checkout\\CustomerData\\DefaultItem->doGetItemData()\n#2 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(58): Magento\\Checkout\\CustomerData\\AbstractItem->getItemData(Object(Magento\\Quote\\Model\\Quote\\Item))\n#3 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(138): Magento\\ConfigurableProduct\\CustomerData\\ConfigurableItem\\Interceptor->___callParent('getItemData', Array)\n#4 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(153): Magento\\ConfigurableProduct\\CustomerData\\ConfigurableItem\\Intercep in /var/www/zenzii_beta/vendor/magento/module-configurable-product/CustomerData/ConfigurableItem.php on line 66, referer: http://zenzii-beta.loc/index.php/all-necklaceThis is the broken cart bug and one of Magento 2 issues. The following conditions are needed to reproduce it:
  1. The site must allow product configuration
  2. A logged-in customer must add a configured product to their cart
  3. The server administrator must delete the same product option (the simple product) that the customer added to their basket, and then the customer must log back into the site.

Below, we examine the reason for this bug using an example with Magento 2.2 code. We also examine a method for fixing it.

Why the Bug Occurs with Magento 2.2, and How to Fix It

When a user adds a configured product to their cart, two cart elements are actually added: the configured product and the simple product. In this case, the parent element is the configured product, and the hidden element is the simple product. Magento gets the weight, cost, and SKU from the simple product. What happens if the administrator deletes the simple product that the user has configured and added to their cart?

The following plugin is defined in the Magento_Quote core module

<type name="Magento\Catalog\Model\ResourceModel\Product">
    <plugin name="clean_quote_items_after_product_delete" type="Magento\Quote\Model\Product\Plugin\RemoveQuoteItems"/>
    <plugin name="update_quote_items_after_product_save" type="Magento\Quote\Model\Product\Plugin\UpdateQuoteItems"/>
</type>

Let’s examine the class of the clean_quote_items_after_product_delete plugin.

{
    /**
     * @var \Magento\Quote\Model\Product\QuoteItemsCleanerInterface
     */
    private $quoteItemsCleaner;
    /**
     * @param \Magento\Quote\Model\Product\QuoteItemsCleanerInterface $quoteItemsCleaner
     */
    public function __construct(\Magento\Quote\Model\Product\QuoteItemsCleanerInterface $quoteItemsCleaner)
    {
        $this->quoteItemsCleaner = $quoteItemsCleaner;
    }
    /**
     * @param ProductResource $subject
     * @param ProductResource $result
     * @param \Magento\Catalog\Api\Data\ProductInterface $product
     * @return ProductResource
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function afterDelete(
        ProductResource $subject,
        ProductResource $result,
        \Magento\Catalog\Api\Data\ProductInterface $product
    ) {
        $this->quoteItemsCleaner->execute($product);
        return $result;
    }
}

Note that until version 2.2, the around plugin was used instead of after, since after plugins didn’t support the input parameters of the original method. The \Magento\Quote\Model\Product\QuoteItemsCleanerInterface class looks as follows.

class QuoteItemsCleaner implements \Magento\Quote\Model\Product\QuoteItemsCleanerInterface
{
    /**
     * @var \Magento\Quote\Model\ResourceModel\Quote\Item
     */
    private $itemResource;
    /**
     * @param \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource
     */
    public function __construct(\Magento\Quote\Model\ResourceModel\Quote\Item $itemResource)
    {
        $this->itemResource = $itemResource;
    }
    /**
     * {@inheritdoc}
     */
    public function execute(\Magento\Catalog\Api\Data\ProductInterface $product)
    {
        $this->itemResource->getConnection()->delete(
            $this->itemResource->getMainTable(),
            'product_id = ' . $product->getId()
        );
    }
}

As you can see, when the administrator deletes a product, the plugin simply deletes all the elements in the carts of all the users who had added the product. But the parent element isn’t deleted from the cart, and we end up with an orphaned item. To understand what is happening when trying to display the contents of a cart on the front end, we look at the code

namespace Magento\ConfigurableProduct\CustomerData;
use Magento\Catalog\Model\Config\Source\Product\Thumbnail as ThumbnailSource;
use Magento\Checkout\CustomerData\DefaultItem;
/**
 * Configurable item
 */
class ConfigurableItem extends DefaultItem
{
    /**
     * @var \Magento\Framework\App\Config\ScopeConfigInterface
     */
    protected $_scopeConfig;
    /**
     * @param \Magento\Catalog\Helper\Image $imageHelper
     * @param \Magento\Msrp\Helper\Data $msrpHelper
     * @param \Magento\Framework\UrlInterface $urlBuilder
     * @param \Magento\Catalog\Helper\Product\ConfigurationPool $configurationPool
     * @param \Magento\Checkout\Helper\Data $checkoutHelper
     * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
     * @param \Magento\Framework\Escaper|null $escaper
     */
    public function __construct(
        \Magento\Catalog\Helper\Image $imageHelper,
        \Magento\Msrp\Helper\Data $msrpHelper,
        \Magento\Framework\UrlInterface $urlBuilder,
        \Magento\Catalog\Helper\Product\ConfigurationPool $configurationPool,
        \Magento\Checkout\Helper\Data $checkoutHelper,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Framework\Escaper $escaper = null
    ) {
        parent::__construct(
            $imageHelper,
            $msrpHelper,
            $urlBuilder,
            $configurationPool,
            $checkoutHelper,
            $escaper
        );
        $this->_scopeConfig = $scopeConfig;
    }
    /**
     * {@inheritdoc}
     */
    protected function getProductForThumbnail()
    {
        /**
         * Show parent product thumbnail if it must be always shown according to the related setting in system config
         * or if child thumbnail is not available
         */
        $config = $this->_scopeConfig->getValue(
            \Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable::CONFIG_THUMBNAIL_SOURCE,
            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
        );
        $product = $config == ThumbnailSource::OPTION_USE_PARENT_IMAGE
            || (!$this->getChildProduct()->getThumbnail() || $this->getChildProduct()->getThumbnail() == 'no_selection')
            ? $this->getProduct()
            : $this->getChildProduct();
        return $product;
    }
    /**
     * Get item configurable child product
     *
     * @return \Magento\Catalog\Model\Product
     */
    protected function getChildProduct()
    {
        if ($option = $this->item->getOptionByCode('simple_product')) {
            return $option->getProduct();
        }
        return $this->getProduct();
    }
}

An attempt to retrieve the child product, which doesn’t exist, also results in this error. In fact, until version 2.1.3, a foreign key from the catalog_product_entity table was used instead of the plugin. This key deleted cart items for deleted products.

A fix for this error will be described below. The best option, in this case, is to delete the parent element from the cart along with the child. Unfortunately, you cannot use a foreign key to do this, since the child element is already linked to the parent by a foreign key. The solution that uses a MySQL trigger will also not work. MySQL doesn’t allow the same table to be used in the delete subquery, and it doesn’t allow deleting from the same table in the deletion trigger. This is because in such cases MySQL cannot determine the presence of circular references in queries, and it simply blocks them. Let’s add a trigger to the database:

DELIMITER //
create trigger quote_item_delete after deleting on quote_item for each row
begin
if OLD.parent_item_id is not NULL then
delete from quote_item where item_id = OLD.parent_item_id;
end if;

end;//
DELIMITER ;

Then, an attempt from the admin panel to delete the product will result in an error message for the administrator, and the following error will be added to the log: “SQLSTATE[HY000]: General error: 1442 Can’t update table ‘quote_item’ in stored function/trigger because it is already used by statement which invoked this stored function/trigger., query was:..”Therefore, you have to delete the parent element on the Magento side. Let’s create a module for this, which will consist of the following files.

registration.php

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Web4pro_BrokenCart',
    __DIR__
);
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="Web4pro_BrokenCart" setup_version="0.0.1"></module>
</config>

etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Quote\Model\Product\QuoteItemsCleanerInterface" type="Web4pro\BrokenCart\Model\QuoteItemsCleaner" />
</config>

As you can see, we replaced the class for implementing the interface Magento\Quote\Model\Product\QuoteItemsCleanerInterface. The class will have the following implementation:

namespace Web4pro\BrokenCart\Model;
class QuoteItemsCleaner extends  \Magento\Quote\Model\Product\QuoteItemsCleaner
{
    /**
     * @var \Magento\Quote\Model\ResourceModel\Quote\Item
     */
    protected $itemResource;
    /**
     * @param \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource
     */
    public function __construct(\Magento\Quote\Model\ResourceModel\Quote\Item $itemResource)
    {
        $this->itemResource = $itemResource;
        parent::__construct($itemResource);
    }
    public function execute(\Magento\Catalog\Api\Data\ProductInterface $product){
        $select = $this->itemResource->getConnection()->select();
        $select->from($this->itemResource->getMainTable(),'parent_item_id')
               ->where('parent_item_id is not NULL and product_id='.$product->getId());
        $parentItemIds = $this->itemResource->getConnection()->fetchCol($select);
        if(count($parentItemIds)){
            $this->itemResource->getConnection()
                 ->delete($this->itemResource->getMainTable(),'item_id in ('.implode(',',$parentItemIds).')');
        }
        return parent::execute($product);
    }
}

SUM MARY

We had to redefine the constructor to fix the problems with shopping carts since the itemResource variable is private and isn't visible in the child variables. We had to redefine it as protected. As you can see, we receive all parent elements of deleted products and delete them. This fixes the broken cart issue. This fix for Magento shopping cart will be necessary until Magento includes code for deleting parent elements from carts when deleting child elements in the core. If you have issues with bug fixing or you need the support of your site, you can refer to our Magento Support services.

Posted on: October 09, 2018

5.0/5.0

Article rating (6 Reviews)

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

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