Steps
- Add address attribute via setup script
- Add the attributes to the extension attributes of the Address api
- Change the input field name and data scope to {address}.custom_attributes.{attribute_code}
- Add javascript mixins to transport custom_attributes values to extension_attributes values
- Write a plugin to save the extension_attributes data to your database destination
Tools
- Mage2gen.com
- Knockout Chrome Extension
Examples
- https://github.com/experius/Magento-2-Module-Experius-ExtraCheckoutAddressFields
- https://github.com/experius/Magento-2-Module-Experius-ExtraCheckoutAddressFieldsTest
Params
- Vendor name: Alldu
- Module name: AddressAttributeToCheckout
- Attribute name: example
Step 1: Add address attribute via setup script
Create File: Setup/InstallData.php
<?php
namespace Alldu\AddressAttributeToCheckout\Setup;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Customer\Model\Customer;
use Magento\Customer\Setup\CustomerSetupFactory;
class InstallData implements InstallDataInterface
{
private $customerSetupFactory;
/**
* Constructor
*
* @param \Magento\Customer\Setup\CustomerSetupFactory $customerSetupFactory
*/
public function __construct(
CustomerSetupFactory $customerSetupFactory
) {
$this->customerSetupFactory = $customerSetupFactory;
}
/**
* {@inheritdoc}
*/
public function install(
ModuleDataSetupInterface $setup,
ModuleContextInterface $context
) {
$customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);
$customerSetup->addAttribute('customer_address', 'example', [
'label' => 'example',
'input' => 'text',
'type' => 'varchar',
'source' => '',
'required' => false,
'position' => 333,
'visible' => true,
'system' => false,
'is_used_in_grid' => false,
'is_visible_in_grid' => false,
'is_filterable_in_grid' => false,
'is_searchable_in_grid' => false,
'backend' => ''
]);
$attribute = $customerSetup->getEavConfig()->getAttribute('customer_address', 'example')
->addData(['used_in_forms' => [
'customer_address_edit',
'customer_register_address'
]]);
$attribute->save();
$installer->getConnection()->addColumn(
$installer->getTable('quote_address'),
'example',
[
'type' => 'varchar',
'length' => 255
]
);
$installer->getConnection()->addColumn(
$installer->getTable('sales_order_address'),
'example',
[
'type' => 'varchar',
'length' => 255
]
);
}
}
Checkpoint 1: Address field should show in the checkout.
Step 2: Add the attributes to the extension attributes of the Address api
Create File: 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\Customer\Api\Data\AddressInterface">
<attribute code="example" type="string"/>
</extension_attributes>
</config>
Checkpoint 2: The following files are generated
var/generation/Magento/Quote/Api/Data/AddressExtension.php
var/generation/Magento/Quote/Api/Data/AddressExtensionInterface.php
It should contain getters and setters for your address attribute. In this case. setExample(), getExample()
Step 3: Change the input field name and data scope to {address}.custom_attributes.{attribute_code}
Warning: this is not pretty.
Create File: etc/frontend/di.xml
This adds an extra layout processor to the Magento checkout onepage block. Its gives us the possibility to change the form fields before the checkout is loaded.
<?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\Block\Onepage">
<arguments>
<argument name="layoutProcessors" xsi:type="array">
<item name="experius_extra_checkout_address_fields_layoutprocessor" xsi:type="object">Alldu\AddressAttributeToCheckout\Block\Checkout\LayoutProcessor</item>
</argument>
</arguments>
</type>
</config>
Create File: Block/Checkout/LayoutProcessor.php
This our own layout processor. The $result param in the process method contains a array with all the fields that will be rendered on the checkout page.
<?php
namespace Alldu\AddressAttributeToCheckout\Block\Checkout;
class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcessorInterface
{
public function process($result) {
$result = $this->getShippingFormFields($result);
$result = $this->getBillingFormFields($result);
return $result;
}
public function getAdditionalFields($addressType='shipping'){
if($addressType=='shipping') {
return ['example'];
}
return ['example'];
}
public function getShippingFormFields($result){
if(isset($result['components']['checkout']['children']['steps']['children']
['shipping-step']['children']['shippingAddress']['children']
['shipping-address-fieldset'])
){
$shippingPostcodeFields = $this->getFields('shippingAddress.custom_attributes','shipping');
$shippingFields = $result['components']['checkout']['children']['steps']['children']
['shipping-step']['children']['shippingAddress']['children']
['shipping-address-fieldset']['children'];
if(isset($shippingFields['street'])){
unset($shippingFields['street']['children'][1]['validation']);
unset($shippingFields['street']['children'][2]['validation']);
}
$shippingFields = array_replace_recursive($shippingFields,$shippingPostcodeFields);
$result['components']['checkout']['children']['steps']['children']
['shipping-step']['children']['shippingAddress']['children']
['shipping-address-fieldset']['children'] = $shippingFields;
}
return $result;
}
public function getBillingFormFields($result){
if(isset($result['components']['checkout']['children']['steps']['children']
['billing-step']['children']['payment']['children']
['payments-list'])) {
$paymentForms = $result['components']['checkout']['children']['steps']['children']
['billing-step']['children']['payment']['children']
['payments-list']['children'];
foreach ($paymentForms as $paymentMethodForm => $paymentMethodValue) {
$paymentMethodCode = str_replace('-form', '', $paymentMethodForm);
if (!isset($result['components']['checkout']['children']['steps']['children']['billing-step']['children']['payment']['children']['payments-list']['children'][$paymentMethodCode . '-form'])) {
continue;
}
$billingFields = $result['components']['checkout']['children']['steps']['children']
['billing-step']['children']['payment']['children']
['payments-list']['children'][$paymentMethodCode . '-form']['children']['form-fields']['children'];
$billingPostcodeFields = $this->getFields('billingAddress' . $paymentMethodCode . '.custom_attributes','billing');
$billingFields = array_replace_recursive($billingFields, $billingPostcodeFields);
$result['components']['checkout']['children']['steps']['children']
['billing-step']['children']['payment']['children']
['payments-list']['children'][$paymentMethodCode . '-form']['children']['form-fields']['children'] = $billingFields;
}
}
return $result;
}
public function getFields($scope,$addressType){
$fields = [];
foreach($this->getAdditionalFields($addressType) as $field){
$fields[$field] = $this->getField($field,$scope);
}
return $fields;
}
public function getField($attributeCode,$scope) {
$field = [
'config' => [
'customScope' => $scope,
],
'dataScope' => $scope . '.'.$attributeCode,
];
return $field;
}
Checkpoint 3: the name attribute from the input field should now contain custom_attributes
Modify file: etc/module.xml
Add a sequence so our module is loaded after the Magento Checkout module and our layout processor is triggered after the one from the magento checkout module
<?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="Alldu_AddressAttributeToCheckout" setup_version="1.0.0">
<sequence>
<module name="Magento_Checkout"/>
</sequence>
</module>
</config>
Step 4: Add javascript mixins to transport custom_attributes values to extension_attributes values
The javascript of the checkout looks for extra fields with name customer_attributes and includes them in the postdata to the checkout api
The api looks for extra fields to save in the extension_attributes. So we have to transport them from custom_attributes to extension_attributes.
What is a javascript mixin? Its the javascript equivalent of plugin (interceptor). Thats how i see it being a backend developer
Create file: view/frontend/requirejs-config.js
var config = {
config: {
mixins: {
'Magento_Checkout/js/action/set-billing-address': {
'Experius_CodeBlogAddressAttributeToCheckout/js/action/set-billing-address-mixin': true
},
'Magento_Checkout/js/action/set-shipping-information': {
'Experius_CodeBlogAddressAttributeToCheckout/js/action/set-shipping-information-mixin': true
},
'Magento_Checkout/js/action/create-shipping-address': {
'Experius_CodeBlogAddressAttributeToCheckout/js/action/create-shipping-address-mixin': true
},
'Magento_Checkout/js/action/place-order': {
'Experius_CodeBlogAddressAttributeToCheckout/js/action/set-billing-address-mixin': true
},
'Magento_Checkout/js/action/create-billing-address': {
'Experius_CodeBlogAddressAttributeToCheckout/js/action/set-billing-address-mixin': true
}
}
}
};
Register the mixins
Create file: view/frontend/web/js/action/create-shipping-address-mixin.js
define([
'jquery',
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function ($, wrapper,quote) {
'use strict';
return function (setShippingInformationAction) {
return wrapper.wrap(setShippingInformationAction, function (originalAction, messageContainer) {
if (messageContainer.custom_attributes != undefined) {
$.each(messageContainer.custom_attributes , function( key, value ) {
messageContainer['custom_attributes'][key] = {'attribute_code':key,'value':value};
});
}
return originalAction(messageContainer);
});
};
});
For the logged in customers the create shipping addres mixin is needed and it requires a key value content. Only in this case
Create file: view/frontend/web/js/action/set-billing-address-mixin.js
define([
'jquery',
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function ($, wrapper,quote) {
'use strict';
return function (setBillingAddressAction) {
return wrapper.wrap(setBillingAddressAction, function (originalAction, messageContainer) {
var billingAddress = quote.billingAddress();
if(billingAddress != undefined) {
if (billingAddress['extension_attributes'] === undefined) {
billingAddress['extension_attributes'] = {};
}
if (billingAddress.customAttributes != undefined) {
$.each(billingAddress.customAttributes, function (key, value) {
if($.isPlainObject(value)){
value = value['value'];
}
billingAddress['extension_attributes'][key] = value;
});
}
}
return originalAction(messageContainer);
});
};
});
Create file: view/frontend/web/js/action/set-shipping-information-mixin.js
define([
'jquery',
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function ($, wrapper,quote) {
'use strict';
return function (setShippingInformationAction) {
return wrapper.wrap(setShippingInformationAction, function (originalAction, messageContainer) {
var shippingAddress = quote.shippingAddress();
if (shippingAddress['extension_attributes'] === undefined) {
shippingAddress['extension_attributes'] = {};
}
if (shippingAddress.customAttributes != undefined) {
$.each(shippingAddress.customAttributes , function( key, value ) {
if($.isPlainObject(value)){
value = value['value'];
}
shippingAddress['customAttributes'][key] = value;
shippingAddress['extension_attributes'][key] = value;
});
}
return originalAction(messageContainer);
});
};
});
Checkpoint 4: Enable your browsers inspector, monitor ajax calls. When you save the shipping or billing address the example value should now be in both the custom_attributes and extension_attributes when you look at the post data
Step 5 Write a plugin to save the extension_attributes data to your database destination
Create file: etc/di.xml
This registers your plugin classes
<?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\Quote\Model\BillingAddressManagement">
<plugin disabled="false" name="Alldu_AddressAttributeToCheckout_Plugin_Magento_Quote_Model_BillingAddressManagement" sortOrder="10" type="Alldu_AddressAttributeToCheckout\Plugin\Magento\Quote\Model\BillingAddressManagement"/>
</type>
<type name="Magento\Quote\Model\ShippingAddressManagement">
<plugin disabled="false" name="Alldu_AddressAttributeToCheckout_Plugin_Magento_Quote_Model_ShippingAddressManagement" sortOrder="10" type="Alldu_AddressAttributeToCheckout\Plugin\Magento\Quote\Model\ShippingAddressManagement"/>
</type>
</config>
Create file: Plugin/Magento/Quote/Model/BillingAddressManagement.php
<?php
namespace Alldu\AddressAttributeToCheckout\Plugin\Magento\Quote\Model;
class BillingAddressManagement
{
protected $logger;
public function __construct(
\Psr\Log\LoggerInterface $logger
) {
$this->logger = $logger;
}
public function beforeAssign(
\Magento\Quote\Model\BillingAddressManagement $subject,
$cartId,
\Magento\Quote\Api\Data\AddressInterface $address,
$useForShipping = false
) {
$extAttributes = $address->getExtensionAttributes();
if (!empty($extAttributes)) {
try {
$address->setExample($extAttributes->getExample());
} catch (\Exception $e) {
$this->logger->critical($e->getMessage());
}
}
}
}
Create file: Plugin/Magento/Quote/Model/ShippingAddressManagement.php
<?php
namespace Alldu\AddressAttributeToCheckout\Plugin\Magento\Quote\Model;
class ShippingAddressManagement
{
protected $logger;
public function __construct(
\Psr\Log\LoggerInterface $logger
) {
$this->logger = $logger;
}
public function beforeAssign(
\Magento\Quote\Model\ShippingAddressManagement $subject,
$cartId,
\Magento\Quote\Api\Data\AddressInterface $address
) {
$extAttributes = $address->getExtensionAttributes();
if (!empty($extAttributes)) {
try {
$address->setExample($extAttributes->getExample());
} catch (\Exception $e) {
$this->logger->critical($e->getMessage());
}
}
}
}
Checkpoint 5: Place your order. Check the quote_address table in the database. You should see the entered value in the example field in the example column