4

I am working through the Add a new field in the address form tutorial. I've copied the tutorial, and the custom field is appearing on my Checkout page. However the fields are not stored when submitting the checkout. In fact, when dumping the database, I can see that unique strings stored in the Custom Attribute field are not present in the dump. This is on a vanilla Magento 2.4.4 running locally in a Docker container on my Ubuntu desktop.

These are my files, what have I done wrong?

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="Dc_Foo" setup_version="0.0.1">
        <sequence>
            <module name="Magento_Customer" />
            <module name="Magento_Checkout" />
        </sequence>
    </module>
</config>

registration.php

<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Dc_Foo', __DIR__);

Plugin/Block/Checkout/LayoutProcessor.php

<?php
namespace Dc\Foo\Plugin\Block\Checkout;
use Magento\Checkout\Block\Checkout\LayoutProcessor as LayoutProcessor_stock;

class LayoutProcessor {

    public function afterProcess(LayoutProcessor_stock $subject, array $jsLayout)
    {
        $customAttributeCode = 'foo';

        $customField = [
            'component' => 'Magento_Ui/js/form/element/abstract',
            'config' => [
                'customScope' => 'shippingAddress.custom_attributes',
                'customEntry' => null,
                'template' => 'ui/form/field',
                'elementTmpl' => 'ui/form/element/input',
                'tooltip' => [
                    'description' => 'The fine Foo field.',
                ],
            ],
            'dataScope' => 'shippingAddress.custom_attributes' . '.' . $customAttributeCode,
            'label' => 'Foo',
            'provider' => 'checkoutProvider',
            'sortOrder' => 0,
            'validation' => [
                'required-entry' => false
            ],
            'options' => [],
            'filterBy' => null,
            'customEntry' => null,
            'visible' => true,
            'value' => '' // value field is used to set a default value of the attribute
        ];

        $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']['shippingAddress']['children']['shipping-address-fieldset']['children'][$customAttributeCode] = $customField;

        return $jsLayout;
    }

}

etc/frontend/di.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
        <plugin name="dc-foo-add-foo-to-addresses" type="Dc\Foo\Plugin\Block\Checkout\LayoutProcessor" sortOrder="10" />
    </type>

</config>

view/frontend/web/js/action/set-shipping-information-mixin.js

/*jshint browser:true jquery:true*/
/*global alert*/
define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper, quote) {
    'use strict';

    return function (setShippingInformationAction) {
        return wrapper.wrap(setShippingInformationAction, function (originalAction) {
            var shippingAddress = quote.shippingAddress();
            if (shippingAddress['extension_attributes'] === undefined) {
                shippingAddress['extension_attributes'] = {};
            }

            var attribute = shippingAddress.customAttributes.find(
                function (element) {
                    return element.attribute_code === 'foo';
                }
            );

            shippingAddress['extension_attributes']['foo'] = attribute.value;
            // pass execution to original action ('Magento_Checkout/js/action/set-shipping-information')
            return originalAction();
        });
    };
});

view/frontend/requirejs-config.js

var config = {
  config: {
    mixins: {
      'Magento_Checkout/js/action/set-shipping-information': {
        'Dc_Foo/js/action/set-shipping-information-mixin': true
      },
    }
  }
};

etc/extension_attributes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
        <attribute code="foo" type="string" />
    </extension_attributes>
</config>

I've then run the following CLI commands (output truncated with ... where appropriate):

$ bin/magento cache:clean
Cleaned cache types:
config
layout
block_html
collections
reflection
db_ddl
compiled_config
eav
customer_notification
config_integration
config_integration_api
full_page
config_webservice
translate

$ bin/magento module:enable Dc_Foo"
The following modules have been enabled:
- Dc_Foo

To make sure that the enabled modules are properly registered, run 'setup:upgrade'.
Cache cleared successfully.
Generated classes cleared successfully. Please run the 'setup:di:compile' command to generate classes.
Info: Some modules might require static view files to be cleared. To do this, run 'module:enable' with the --clear-static-content option to clear them.

$ bin/magento setup:upgrade
Cache types config flushed successfully
Cache cleared successfully
File system cleanup:
...
The directory '/var/www/foo/generated/metadata/' doesn't exist - skipping cleanup
...
Updating modules:
Cache cleared successfully
Schema creation/updates:
Module 'Dc_Foo':
...
Module 'Magento_ComposerRootUpdatePlugin':
Running data recurring...Web Setup Wizard installation of "magento/composer-root-update-plugin" failed; unable to load /var/www/html/composer.json.
...
Module 'Dc_Foo':
...
Enabling caches:
Current status:
layout: 1
block_html: 1
full_page: 1
Nothing to import.
Media files stored outside of 'Media Gallery Allowed' folders will not be available to the media gallery.
Please refer to Developer Guide for more details.

$ bin/magento setup:di:compile
Compilation was started.
Plugin list generation... 9/9 [============================] 100% 47 secs 430.0 MiB
Generated code and dependency injection configuration successfully.

Now, I see the field where I expect it on the Shipping Page:

happy-foo-field

And I see that the data was passed to the Review Page:

Field on Review page

And I certainly completed the order:

Field on Thank You page

But nothing in the fine database?!?

$ mysqldump -uroot -p foodb --skip-extended-insert --default-character-set=utf8mb4 -r dump-foo.sql

$ grep "happy-foo-field" dump-foo.sql

$ wc -l dump-foo.sql
25136 dump-foo.sql

$ head dump-foo.sql # The file is valid
-- MySQL dump 10.13  Distrib 8.0.31, for Linux (x86_64)
--
-- Host: localhost    Database: foodb
-- ------------------------------------------------------
-- Server version       8.0.28

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;

2 Answers 2

4
+100

That tutorial only shows you how to add new fields to default checkout forms but not mentioned how to save the field data (extension attribute value) into the database.

To save the extension attribute value into the database, you need to do more 2 sub-tasks:

  • Adding a new column to the database table
  • Saving the extension attribute value.

Depending on your need, you can save the field data (extension attribute value) into quote_address table or quote table, or even save it to sales_order table,...

To illustrate this ability, I'll add 2 parts: one for saving the value into quote_address table and the other for saving the value to quote table. You can choose one of them or combine them to achieve your need.

Part 1: To saving the value into quote_address table, take the following steps:

  1. Modify module.xml to specify a load order using <sequence> tag to ensure Magento_Quote module is already loaded before your module loads.

app/code/Dc/Foo/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="Dc_Foo" setup_version="0.0.1">
        <sequence>
            <module name="Magento_Customer" />
            <module name="Magento_Checkout" />
            <module name="Magento_Quote" />
        </sequence>
    </module>
</config>
  1. Adding a new column into quote_address table:

app/code/Dc/Foo/etc/db_schema.xml

<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="quote_address" resource="default" engine="innodb">
        <column xsi:type="varchar" name="foo" nullable="true" length="255" comment="Foo"/>
    </table>
</schema>

  1. Declare a plugin to save the field data (extension attribute value) into the database:

app/code/Dc/Foo/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">
    <type name="Magento\Checkout\Model\ShippingInformationManagement">
        <plugin name="SaveExtensionAttributes" type="Dc\Foo\Plugin\SaveExtensionAttributes"/>
    </type>
</config>

  1. Create a plugin file:

app/code/Dc/Foo/Plugin/SaveExtensionAttributes.php

<?php
namespace Dc\Foo\Plugin;

use Magento\Checkout\Model\ShippingInformationManagement;
use Magento\Checkout\Api\Data\ShippingInformationInterface;

class SaveExtensionAttributes
{
    /**
     * Save custom form fields value to database
     *
     * @param ShippingInformationManagement $subject
     * @param $cartId
     * @param ShippingInformationInterface $addressInformation
     */
    public function beforeSaveAddressInformation(
        ShippingInformationManagement $subject,
        $cartId,
        ShippingInformationInterface $addressInformation
    ) {
        $shippingAddress = $addressInformation->getShippingAddress();
        $extAttributes = $shippingAddress->getExtensionAttributes();

        if ($extAttributes) {
            $foo = $extAttributes->getFoo();
            // Save custom form fields value to quote_address table
            $shippingAddress->setFoo($foo);
        }
    }
}

Part 2: To saving the value into quote table, take the following steps:

  1. Modify module.xml to specify a load order using <sequence> tag to ensure Magento_Quote module is already loaded before your module loads.

app/code/Dc/Foo/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="Dc_Foo" setup_version="0.0.1">
        <sequence>
            <module name="Magento_Customer" />
            <module name="Magento_Checkout" />
            <module name="Magento_Quote" />
        </sequence>
    </module>
</config>
  1. Adding a new column into quote table:

app/code/Dc/Foo/etc/db_schema.xml

<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="quote" resource="default" engine="innodb">
        <column xsi:type="varchar" name="foo" nullable="true" length="255" comment="Foo"/>
    </table>
</schema>

  1. Declare a plugin to save the field data (extension attribute value) into the database:

app/code/Dc/Foo/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">
    <type name="Magento\Checkout\Model\ShippingInformationManagement">
        <plugin name="SaveExtensionAttributes" type="Dc\Foo\Plugin\SaveExtensionAttributes"/>
    </type>
</config>

  1. Create a plugin file:

app/code/Dc/Foo/Plugin/SaveExtensionAttributes.php

<?php
namespace Dc\Foo\Plugin;

use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Checkout\Model\ShippingInformationManagement;
use Magento\Checkout\Api\Data\ShippingInformationInterface;

class SaveExtensionAttributes
{
    public function __construct(CartRepositoryInterface $quoteRepository)
    {
        $this->quoteRepository = $quoteRepository;
    }

    /**
     * Save custom form fields value to database
     *
     * @param ShippingInformationManagement $subject
     * @param $cartId
     * @param ShippingInformationInterface $addressInformation
     */
    public function beforeSaveAddressInformation(
        ShippingInformationManagement $subject,
        $cartId,
        ShippingInformationInterface $addressInformation
    ) {
        $shippingAddress = $addressInformation->getShippingAddress();
        $extAttributes = $shippingAddress->getExtensionAttributes();

        if ($extAttributes) {
            $foo = $extAttributes->getFoo();
            $quote = $this->quoteRepository->getActive($cartId);
            // Save custom form fields value to quote table
            $quote->setFoo($foo);
        }
    }
}

Finally, run the following commands:

bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
6
  • 1
    this is the correct answer and should be added to that adobe docs page Commented Jan 30, 2023 at 3:13
  • 1
    Thank you! I used the first method, storing on the quote_address table. When checking out as a logged-in Customer the field is in fact stored in the quote_address database table, so I see that the data is stored! However, the field is not shown in the Customer's dashboard. And though the field does display in the adminhtml Customers area, the field is blank (the stored value is not presented there). In any case, whether you can help me with that or not, I am of course presenting the bounty to this wonderful answer. Thank you Tu Van!
    – dotancohen
    Commented Feb 2, 2023 at 12:49
  • Thanks @KevinChavez, I'll take a look.
    – Tu Van
    Commented Feb 2, 2023 at 17:12
  • @dotancohen You're welcome. You misunderstood the extension attribute vs custom attribute, this topic talks about saving the extension attribute which uses on the checkout page as mentioned in the "Add a new field in the address form" tutorial. What you are talking about in the Customer dashboard and admin Customer area is a custom attribute, not an extension attribute, that means they are different, so you can't see the value of that extension attribute in admin customer area, customer dashboard.
    – Tu Van
    Commented Feb 2, 2023 at 17:13
  • Looks like you need a custom field for customer address and you need to display it on the customer address page, checkout page, and admin customer address page. If so, you need to do more work.
    – Tu Van
    Commented Feb 2, 2023 at 17:13
0

You can use below solution for that

/*global alert*/
define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper, quote) {
    'use strict';

    return function (setShippingInformationAction) {
        return wrapper.wrap(setShippingInformationAction, function (originalAction) {
            var shippingAddress = quote.shippingAddress();
            if (shippingAddress['extension_attributes'] === undefined) {
                shippingAddress['extension_attributes'] = {};
            }

            shippingAddress['extension_attributes']['foo'] = attribute.value;
            if (shippingAddress.customAttributes !== undefined) {
                $.each(shippingAddress.customAttributes, function(index, attribute) {
                    if (attribute.attribute_code !== undefined && attribute.attribute_code === 'foo') {
                        // in case of new address
                        shippingAddress['extension_attributes']['foo'] = attribute.value;
                    } else if (index == 'foo') {
                        // in case of old address
                        shippingAddress['extension_attributes']['foo'] = attribute;
                    }
                });
            }
            // pass execution to original action ('Magento_Checkout/js/action/set-shipping-information')
            return originalAction();
        });
    };
});
1
  • Thank you Girish Parmar. I tried your solution, but it did not help. It seems that this code simply copies the Custom Attribute foo field to Extension Attributes foo field in the HTTP request - apparently that was not the issue. I appreciate the idea, but could the problem be anything else?
    – dotancohen
    Commented Jan 29, 2023 at 8:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.