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
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
Params
- Vendor name: Experius
- Module name: CodeBlogAddressAttributeToCheckout
- Attribute name: example
Start with a basic module: download here
Step 1: Add address attribute via setup script
Create File: Setup/InstallData.php
<?php namespace Experius\CodeBlogAddressAttributeToCheckout\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">Experius\CodeBlogAddressAttributeToCheckout\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 Experius\CodeBlogAddressAttributeToCheckout\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="Experius_CodeBlogAddressAttributeToCheckout" 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="Experius_CodeBlogAddressAttributeToCheckout_Plugin_Magento_Quote_Model_BillingAddressManagement" sortOrder="10" type="Experius_CodeBlogAddressAttributeToCheckout\Plugin\Magento\Quote\Model\BillingAddressManagement"/> </type> <type name="Magento\Quote\Model\ShippingAddressManagement"> <plugin disabled="false" name="Experius_CodeBlogAddressAttributeToCheckout_Plugin_Magento_Quote_Model_ShippingAddressManagement" sortOrder="10" type="Experius_CodeBlogAddressAttributeToCheckout\Plugin\Magento\Quote\Model\ShippingAddressManagement"/> </type> </config>
Create file: Plugin/Magento/Quote/Model/BillingAddressManagement.php
<?php namespace Experius\CodeBlogAddressAttributeToCheckout\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 Experius\CodeBlogAddressAttributeToCheckout\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