Magento How to Add Additional Options in Magento 2 Cart

napros

Mecho Network Member
SN Donator
Magento 2 has several well-known features that add great value to an online store. However, many store owners and administrators are always looking for new features that would distinguish their stores from the competition.

Recently, I was looking for a way to add custom input option to Magento 2 cart without affecting the core Magento functionalities (a good practice endorsed by Magento gurus including Ben Marks!)



I did a lot of Google searches and read a number of Stack Overflow threads (even started a thread myself). Finally, here is my solution of the big question of adding additional options in Magento 2 cart.

To add additional options in the frontend of the Magento 2 cart and to carry those values through ordering, invoices, and reordering process, you need two observer events.

First of all, let me briefly explain Observers in Magento.

Observers are special Magento classes that are executed whenever the event manager detects the event observers are configured to watch. The observer classes hold the instance of that event which is used for further pre-defined actions.

For the purpose of this article, assume that your Magento 2 store is installed directly in the public_html folder of the Magento hosting directory. This means that the store is accessible directly from the parent domain (yourdomain.com). Also, assume that the default Magento 2 theme is active and in use.

Following is the process of implementing the custom values in the Magento 2 cart.

Create a Simple Magento 2 Module

The best practice and the recommended code standard for Magento 2 is to create a module.

Here, I will use Cloudways as Module Namespace and Mymodule as Module Name.

First, create a registration.php file in /app/code/Cloudways/Mymodule/. Add the following code to the file.
Code:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Cloudways_Mymodule',
    __DIR__
);
Now, create a module.xml file in /app/code/Cloudways/Mymodule/etc/ with the following content.

Code:
<?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="Cloudways_Mymodule" setup_version="1.0.0"></module>
</config>
Assign Observers to Events
Now, I will configure the observers in the event.xml file to watch certain events.

I will assign the observer CheckoutCartProductAddAfterObserver to the event checkout_cart_product_add_after and SalesModelServiceQuoteSubmitBeforeObserver to sales_model_service_quote_submit_before.

The file should be located at /app/code/Cloudways/Mymodule/etc/ with the following code.

Code:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="checkout_cart_product_add_after">
        <observer name="cloudways_mymodule_checkout_cart_product_add_after" instance="Cloudways\Mymodule\Observer\CheckoutCartProductAddAfterObserver" />
    </event>
    <event name="sales_model_service_quote_submit_before">
        <observer name="cloudways_mymodule_sales_model_service_quote_submit_before" instance="Cloudways\Mymodule\Observer\SalesModelServiceQuoteSubmitBeforeObserver" />
    </event>
</config>
Create the Observers
I will require two observers for the job. Keep in mind that in order to create an observer, you must create an observer class file under the Observer directory of the module. The class should implement Magento\Framework\Event\ObserverInterface and define the observer execute the function.

To add custom option to quote, create CheckoutCartProductAddAfterObserver.php file in /app/code/Cloudways/Mymodule/Observer/ and add the following code in it.

Code:
<?php

namespace Cloudways\Mymodule\Observer;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;

class CheckoutCartProductAddAfterObserver implements ObserverInterface
{
    /**
     * @var \Magento\Framework\View\LayoutInterface
     */
    protected $_layout;
    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    protected $_storeManager;
    protected $_request;
    /**
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Framework\View\LayoutInterface $layout
     */
    public function __construct(
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\View\LayoutInterface $layout,
        \Magento\Framework\App\RequestInterface $request
    )
    {
        $this->_layout = $layout;
        $this->_storeManager = $storeManager;
        $this->_request = $request;
    }
    /**
     * Add order information into GA block to render on checkout success pages
     *
     * @param EventObserver $observer
     * @return void
     */
    public function execute(EventObserver $observer)
    {
        /* @var Magento\Quote\Model\Quote\Item $item */
        $item = $observer->getQuoteItem();
        $additionalOptions = array();
        if ($additionalOption = $item->getOptionByCode('additional_options')){
            $additionalOptions = (array) unserialize($additionalOption->getValue());
        }
        $post = $this->_request->getParam('cloudways');
        if(is_array($post))
        {
            foreach($post as $key => $value)
            {
                if($key == '' || $value == '')
                {
                    continue;
                }
                $additionalOptions[] = [
                    'label' => $key,
                    'value' => $value
                ];
            }
        }
        if(count($additionalOptions) > 0)
        {
            $item->addOption(array(
                'code' => 'additional_options',
                'value' => serialize($additionalOptions)
            ));
        }
        /* To Do */
        // Edit Cart - May need to remove option and readd them
        // Pre-fill remarks on product edit pages
        // Check for comparability with custom option
    }
}
I will now create another observer that will copy option from quote_item to order_item.

Create another file SalesModelServiceQuoteSubmitBeforeObserver.php in /app/code/Cloudways/Mymodule/Observer/ with the following code:

Code:
<?php

namespace Cloudways\Mymodule\Observer;

use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;

class SalesModelServiceQuoteSubmitBeforeObserver implements ObserverInterface
{
    private $quoteItems = [];
    private $quote = null;
    private $order = null;
    /**
     * Add order information into GA block to render on checkout success pages
     *
     * @param EventObserver $observer
     * @return void
     */
    public function execute(EventObserver $observer)
    {
        $this->quote = $observer->getQuote();
        $this->order = $observer->getOrder();
        // can not find a equivalent event for sales_convert_quote_item_to_order_item
        /* @var  \Magento\Sales\Model\Order\Item $orderItem */
        foreach($this->order->getItems() as $orderItem)
        {
            if(!$orderItem->getParentItemId() && $orderItem->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
            {

                if($quoteItem = $this->getQuoteItemById($orderItem->getQuoteItemId())){
                    if ($additionalOptionsQuote = $quoteItem->getOptionByCode('additional_options'))
                    {
                        //To do
                        // - check to make sure element are not added twice
                        // - $additionalOptionsQuote - may not be an array
                        if($additionalOptionsOrder = $orderItem->getProductOptionByCode('additional_options'))
                        {
                            $additionalOptions = array_merge($additionalOptionsQuote, $additionalOptionsOrder);
                        }
                        else
                        {
                            $additionalOptions = $additionalOptionsQuote;
                        }
                        if(count($additionalOptions) > 0)
                        {
                            $options = $orderItem->getProductOptions();
                            $options['additional_options'] = unserialize($additionalOptions->getValue());
                            $orderItem->setProductOptions($options);
                        }

                    }
                }
            }
        }
    }
    private function getQuoteItemById($id)
    {
        if(empty($this->quoteItems))
        {
            /* @var  \Magento\Quote\Model\Quote\Item $item */
            foreach($this->quote->getItems() as $item)
            {
                //filter out config/bundle etc product
                if(!$item->getParentItemId() && $item->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
                {
                    $this->quoteItems[$item->getId()] = $item;
                }
            }
        }
        if(array_key_exists($id, $this->quoteItems))
        {
            return $this->quoteItems[$id];
        }
        return null;
    }
}
Add Input Field to Magento 2 Cart

Last but not the least, I will add a custom input field to Magento 2 Cart template. For this, it is important to know the location from where Magento 2 is rendering the default cart on the product page. This is the location of the template:

Magento/Catalog/view/frontend/templates/catalog/product/view/addtocart.phtml

Copy the addtocart.phtml file to the module directory at /app/code/Cloudways/Mymodule/view/frontend/template/catalog/product/view/ and paste the following code in it:

Code:
<?php
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/

// @codingStandardsIgnoreFile

/** @var $block \Magento\Catalog\Block\Product\View */
?>
<?php $_product = $block->getProduct(); ?>
<?php $buttonTitle = __('Add to Cart'); ?>
<?php if ($_product->isSaleable()): ?>
<div class="box-tocart">
    <div class="fieldset">
        <?php if ($block->shouldRenderQuantity()): ?>
        <div class="field qty">
            <label class="label" for="qty"><span><?php /* @escapeNotVerified */ echo __('Qty') ?></span></label>
            <div class="control">
                <input type="number"
                       name="qty"
                       id="qty"
                       maxlength="12"
                       value="<?php /* @escapeNotVerified */ echo $block->getProductDefaultQty() * 1 ?>"
                       title="<?php /* @escapeNotVerified */ echo __('Qty') ?>" class="input-text qty"
                       data-validate="<?php echo $block->escapeHtml(json_encode($block->getQuantityValidators())) ?>"
                       />
            </div>
        </div>
        <!-- Custom Input Field -->
        <div>
            <input
                type="text"
                name="cloudways[remarks]"
                id="remarks"
                maxlength="255"
                placeholder="Remarks"
            />
        </div>
        <!-- Custom Input Field -->
        <br>
        <?php endif; ?>
        <div class="actions">
            <button type="submit"
                    title="<?php /* @escapeNotVerified */ echo $buttonTitle ?>"
                    class="action primary tocart"
                    id="product-addtocart-button">
                <span><?php /* @escapeNotVerified */ echo $buttonTitle ?></span>
            </button>
            <?php echo $block->getChildHtml('', true) ?>
        </div>
    </div>
</div>
<?php endif; ?>
<script type="text/x-magento-init">
    {
        "#product_addtocart_form": {
            "Magento_Catalog/product/view/validation": {
                "radioCheckboxClosest": ".nested"
            }
        }
    }
</script>
<?php if (!$block->isRedirectToCartEnabled()) : ?>
<script type="text/x-magento-init">
    {
        "#product_addtocart_form": {
            "catalogAddToCart": {
                "bindSubmit": false
            }
        }
    }
</script>
<?php endif; ?>
And that’s all.

Now, the last thing to do is to change the default addtocart.phtml template to a custom template. At this point, Magento 2 does not know that I want to use my own addtocart.phtml file. To remedy this, I will modify the path using the Magento 2 layout.

Create a new file catalog_product_view.xml in /app/code/Cloudways/Mymodule/view/frontend/layout/ and add the following code:

Code:
<?xml version="1.0"?>
<page layout="1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="product.info.addtocart">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">Cloudways_Mymodule::catalog/product/view/addtocart.phtml</argument>
            </action>
        </referenceBlock>
        <referenceBlock name="product.info.addtocart.additional">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">Cloudways_Mymodule::catalog/product/view/addtocart.phtml</argument>
            </action>
        </referenceBlock>
    </body>
</page>
Finally, the custom module that will add additional options to Magento 2 Cart and forward the custom information items across orders, invoices, and reorder processes is ready.

Now, I will enable and activate the module through the following CLI command.

Code:
rm -rf var/di var/generation var/cache/* var/log/* var/page_cache/*
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento cache:clean
php bin/magento cache:flush
php bin/magento indexer:reindex
The final output will look like this:



Conclusion
I hope that by now you have a clear idea about the concept of Observers in Magento 2.
 

therealzeeshu

SN Confirmed Member
can i add a new row to order summary block. I would like to add a service fee in percentage and i would also like to have a row after grandtotal where it will multiple grandtotal with fixed value and display there.

Example:
Order Summary
Item1 $100
Shipping(10%) $10
Tax(25%) $25

GrandTotal $135
Price in PKR(1$=105) PKR14,175

Currently When i set my base currency to PKR it converts all the prices into PKR in storefront. I just want the grand total to be converted. If you can point me to the file where they convert currency rate, I can set the fixed value i.e 105 in my case to multiple by grand total and edit line 'Your credit card will be charged' to 'Price in PKR'.

I am trying to achieve this. I have went thru many websites but for some reason i cant make that work.
Thank you for your time.
 
Top