Magento 2 Create A Customer Widget Attribute And Display In Customer Header After Logging In

Hello Guys,
If you want to create a customer widget attribute and display the attribute widget in customer header section after logging in to my account section, you can use the following code snippet.
The following code sample is converted to small extension so you can use it directly in your website. You can also download the module zip by clicking here.

Step 1: Create the module registration php file as registration.php.

Location: app/code/Magemeta/Customer/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magemeta_Customer',
    __DIR__
);

Location: app/code/Magemeta/Customer/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="Magemeta_Customer" setup_version="1.0.0"></module>
</config>

Location: app/code/Magemeta/Customer/composer.json 
{
    "name": "magemeta/module-customer",
    "description": "This module is used to create customer widget attribute and render it in frontend",
    "type": "magento2-module",
    "license": "OSL-3.0",
    "authors": [
        {
            "email": "info@magemeta.com",
            "name": "magemeta"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Magemeta\\Customer\\": ""
        }
    }
}

Step 2: To create a customer custom attribute for Magento version greater or equal to 2.3.x, we need to add the patch data which works same as setup installer which was used in previous versions of Magento 2. 

Location: app/code/Magemeta/Customer/Setup/Patch/Data/AddAccountNumber.php 

<?php

namespace Magemeta\Customer\Setup\Patch\Data;

use Magento\Customer\Model\Customer;
use Magento\Customer\Setup\CustomerSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Eav\Model\Entity\Attribute\SetFactory as AttributeSetFactory;

/**
 * Class AddAccountNumber
 * @package Magemeta\Customer\Setup\Patch\Data
 */
class AddAccountNumber implements DataPatchInterface
{
    const ACCOUNT_NUMBER = 'account_number';
    const ACCOUNT_NUMBER_LABEL = 'Account Number';

    /**
     * @var ModuleDataSetupInterface
     */
    private $moduleDataSetup;

    /**
     * @var CustomerSetupFactory
     */
    private $customerSetupFactory;

    /**
     * @var AttributeSetFactory
     */
    private $attributeSetFactory;

    /**
     * AddAccountNumber constructor.
     * @param ModuleDataSetupInterface $moduleDataSetup
     * @param CustomerSetupFactory $customerSetupFactory
     * @param AttributeSetFactory $attributeSetFactory
     */
    public function __construct(
        ModuleDataSetupInterface $moduleDataSetup,
        CustomerSetupFactory $customerSetupFactory,
        AttributeSetFactory $attributeSetFactory
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
        $this->customerSetupFactory = $customerSetupFactory;
        $this->attributeSetFactory = $attributeSetFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function apply()
    {
        $customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]);
        $customerEntity = $customerSetup->getEavConfig()->getEntityType('customer');
        $attributeSetId = $customerEntity->getDefaultAttributeSetId();

        /** @var $attributeSet AttributeSet */
        $attributeSet = $this->attributeSetFactory->create();
        $attributeGroupId = $attributeSet->getDefaultGroupId($attributeSetId);

        $customerSetup->addAttribute(
            Customer::ENTITY,
            self::ACCOUNT_NUMBER,
            [
                'type' => 'varchar',
                'label' => self::ACCOUNT_NUMBER_LABEL,
                'input' => 'text',
                'required' => false,
                'sort_order' => 999,
                'user_defined' => true,
                'visible' => true,
                'system' => false,
                'validate_rules' => '{"max_text_length":200,"min_text_length":1}',
                'is_unique' => 1,
            ]
        );
        $attribute = $customerSetup->getEavConfig()->getAttribute(Customer::ENTITY, self::ACCOUNT_NUMBER)
            ->addData([
                'attribute_set_id' => $attributeSetId,
                'attribute_group_id' => $attributeGroupId,
                'used_in_forms' => ['adminhtml_customer', 'customer_account_create', 'customer_account_edit', 'checkout_register'],
            ]);
        $attribute->save();
    }

    /**
     * {@inheritdoc}
     */
    public static function getDependencies()
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    public function getAliases()
    {
        return [];
    }
}

Step 3: We need to add custom configuration in admin store configuration where you can select/set your attribute to be displayed as required, optional or no options on frontend.

Location: app/code/Magemeta/Customer/Setup/Patch/Data/AddAccountNumber.php 
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="customer" translate="label" sortOrder="300">
            <label>Customers</label>
        </tab>
        <section id="customer" translate="label" sortOrder="130" showInDefault="1" showInWebsite="1" showInStore="1">
            <tab>customer</tab>
            <resource>Magento_Customer::config_customer</resource>
            <group id="address" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
                <field id="account_number_show" translate="label" type="select" sortOrder="999" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Account Number</label>
                    <source_model>Magento\Config\Model\Config\Source\Nooptreq</source_model>
                    <backend_model>Magento\Customer\Model\Config\Backend\Show\AddressOnly</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>



Step 4: Now we need to add the di.xml file where we will declare the custom attribute set and get methods which will extend the the customer API interface. We have also extended customer data models the same way to set and save the custom attribute data in both customer_entity and customer_address_entity in Magento database.
To set the custom attribute data in customer section data, we have declared after plugin named as `magemeta_customer_plugin_customerData_customer` where we will set the custom attribute data and retrieve in the knockout js further to use. 

Location: app/code/Magemeta/Customer/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="Magemeta\Customer\Api\Data\AddressInterface" type="Magemeta\Customer\Model\Data\Address"/>
    <preference for="Magemeta\Customer\Api\Data\CustomerInterface" type="Magemeta\Customer\Model\Data\Customer"/>
    <preference for="Magento\Customer\Model\Data\Address" type="Magemeta\Customer\Model\Data\Address"/>
    <preference for="Magento\Customer\Model\Data\Customer" type="Magemeta\Customer\Model\Data\Customer"/>
    <type name="Magento\Customer\CustomerData\Customer">
        <plugin disabled="false" name="magemeta_customer_plugin_customerData_customer" sortOrder="10" type="Magemeta\Customer\Plugin\CustomerData\Customer"/>
    </type>
</config>

Step 5: The customer API and models we have overrided in di.xml used to set and get the attribute data. 

Location: app/code/Magemeta/Customer/Model/Data/Address.php

<?php

namespace Magemeta\Customer\Model\Data;

use Magento\Customer\Api\Data\RegionInterface;
use \Magento\Framework\Api\AttributeValueFactory;

/**
 * Class Address
 * @package Magemeta\Customer\Model\Data
 */
class Address extends \Magento\Customer\Model\Data\Address implements
    \Magemeta\Customer\Api\Data\AddressInterface
{

    /**
     * Get account number
     *
     * @return mixed|null
     */
    public function getAccountNumber()
    {
        return $this->_get(self::ACCOUNT_NUMBER);
    }

    /**
     * Set account number
     *
     * @param mixed $accountNumber
     * @return $this
     */
    public function setAccountNumber($accountNumber)
    {
        return $this->setData(self::ACCOUNT_NUMBER, $accountNumber);
    }
}

Location: app/code/Magemeta/Customer/Model/Data/Customer.php
<?php

namespace Magemeta\Customer\Model\Data;

use \Magento\Framework\Api\AttributeValueFactory;

/**
 * Class Customer
 * @package Magemeta\Customer\Model\Data
 */
class Customer extends \Magento\Customer\Model\Data\Customer implements
    \Magemeta\Customer\Api\Data\CustomerInterface
{

    /**
     * Get account number
     *
     * @return mixed|null
     */
    public function getAccountNumber()
    {
        return $this->_get(self::ACCOUNT_NUMBER);
    }

    /**
     * Set account number
     *
     * @param mixed $accountNumber
     * @return $this
     */
    public function setAccountNumber($accountNumber)
    {
        return $this->setData(self::ACCOUNT_NUMBER, $accountNumber);
    }
}

Location: app/code/Magemeta/Customer/Api/Data/AddressInterface.php
<?php

namespace Magemeta\Customer\Api\Data;

/**
 * Interface AddressInterface
 * @package Magemeta\Customer\Api\Data
 */
interface AddressInterface extends \Magento\Customer\Api\Data\AddressInterface
{
    const ACCOUNT_NUMBER = 'account_number';

    /**
     * Get account number
     *
     * @return mixed|null
     */
    public function getAccountNumber();

    /**
     * Set account number
     *
     * @param mixed $accountNumber
     * @return $this
     */
    public function setAccountNumber($accountNumber);
}

Location: app/code/Magemeta/Customer/Api/Data/CustomerInterface.php
<?php
namespace Magemeta\Customer\Api\Data;

/**
 * Interface CustomerInterface
 * @package Magemeta\Customer\Api\Data
 */
interface CustomerInterface extends \Magento\Customer\Api\Data\CustomerInterface
{
    const ACCOUNT_NUMBER = 'account_number';

    /**
     * Get account number
     *
     * @return mixed|null
     */
    public function getAccountNumber();

    /**
     * Set account number
     *
     * @param mixed $accountNumber
     * @return $this
     */
    public function setAccountNumber($accountNumber);
}

Location: app/code/Magemeta/Customer/Plugin/CustomerData/Customer.php

Here we have used following plugin override to set the custom attribute data in customer section in the following way:

<?php

namespace Magemeta\Customer\Plugin\CustomerData;

use Magento\Customer\Api\GroupRepositoryInterface;
use Magento\Customer\Model\Session\Proxy;
use Magento\Framework\Phrase;

/**
* Class Customer
* @package Magemeta\Customer\Plugin\CustomerData
*/
class Customer
{
/**
* @var Proxy
*/
private $customerSession;
/**
* @var GroupRepositoryInterface
*/
private $groupRepository;

/**
* Customer constructor.
* @param Proxy $customerSession
* @param GroupRepositoryInterface $groupRepository
*/
public function __construct(
Proxy $customerSession,
GroupRepositoryInterface $groupRepository
) {
$this->customerSession = $customerSession;
$this->groupRepository = $groupRepository;
}

/**
* @param \Magento\Customer\CustomerData\Customer $subject
* @param $result
* @return mixed
*/
public function afterGetSectionData(\Magento\Customer\CustomerData\Customer $subject, $result)
{
if ($this->customerSession->isLoggedIn() && $this->customerSession->getCustomerId()) {
$customer = $this->customerSession->getCustomer();
$result['account_number'] = $customer->getAccountNumber();
}
return $result;
}
}
Step 6: To save the custom attribute data, we have used following customer save before event. 

Location: app/code/Magemeta/Customer/events.xml

<?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="customer_save_before">
        <observer name="magemeta_customer_save_before" instance="Magemeta\Customer\Observer\BeforeSaveObserver" />
    </event>
</config>


Location: app/code/Magemeta/Customer/Observer/BeforeSaveObserver.php

<?php

namespace Magemeta\Customer\Observer;

use Magento\Framework\App\RequestInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

/**
 * Class BeforeSaveObserver
 * @package Magemeta\Customer\Observer
 */
class BeforeSaveObserver implements ObserverInterface
{
    /**
     * @var RequestInterface
     */
    protected $_request;

    /**
     * BeforeSaveObserver constructor.
     * @param RequestInterface $request
     */
    public function __construct(
        RequestInterface $request
    ) {
        $this->_request = $request;
    }

    /**
     * @param Observer $observer
     */
    public function execute(Observer $observer)
    {
        $accountNumber = $this->_request->getPost('account_number');
        if ($accountNumber) {
            $customer = $observer->getCustomer();
            $customer->setAccountNumber($accountNumber);
        }
    }
}

Step 7: To create the custom widget for customer attribute, we have used following custom class: 

Location: app/code/Magemeta/Customer/Block/Widget/AccountNumber.php

<?php

namespace Magemeta\Customer\Block\Widget;

use Magento\Customer\Api\AddressMetadataInterface;
use Magento\Customer\Api\CustomerMetadataInterface;
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Helper\Address as AddressHelper;
use Magento\Customer\Model\Options;
use Magento\Framework\View\Element\Template\Context;
use Magento\Customer\Block\Widget\AbstractWidget;

/**
 * Class AccountNumber
 * @package Magemeta\Customer\Block\Widget
 */
class AccountNumber extends AbstractWidget
{

    /**
     * the attribute code
     */
    const ATTRIBUTE_CODE = 'account_number';

    /**
     * @var AddressMetadataInterface
     */
    protected $addressMetadata;

    /**
     * @var Options
     */
    protected $options;

    /**
     * @param Context $context
     * @param AddressHelper $addressHelper
     * @param CustomerMetadataInterface $customerMetadata
     * @param Options $options
     * @param AddressMetadataInterface $addressMetadata
     * @param array $data
     */
    public function __construct(
        Context $context,
        AddressHelper $addressHelper,
        CustomerMetadataInterface $customerMetadata,
        Options $options,
        AddressMetadataInterface $addressMetadata,
        array $data = []
    ) {
        $this->options = $options;
        parent::__construct($context, $addressHelper, $customerMetadata, $data);
        $this->addressMetadata = $addressMetadata;
        $this->_isScopePrivate = true;
    }

    /**
     * @return void
     */
    public function _construct()
    {
        parent::_construct();

        // default template location
        $this->setTemplate('Magemeta_Customer::widget/account_number.phtml');
    }

    /**
     * Can show config value
     *
     * @param string $key
     *
     * @return bool
     */
    protected function _showConfig($key)
    {
        return (bool)$this->getConfig($key);
    }

    /**
     * Can show prefix
     *
     * @return bool
     */
    public function showAccountNumber()
    {
        return $this->_isAttributeVisible(self::ATTRIBUTE_CODE);
    }

    /**
     * @inheritdoc
     */
    protected function _getAttribute($attributeCode)
    {
        if ($this->getForceUseCustomerAttributes() || $this->getObject() instanceof CustomerInterface) {
            return parent::_getAttribute($attributeCode);
        }

        try {
            $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode);
        } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
            return null;
        }

        if ($this->getForceUseCustomerRequiredAttributes() && $attribute && !$attribute->isRequired()) {
            $customerAttribute = parent::_getAttribute($attributeCode);
            if ($customerAttribute && $customerAttribute->isRequired()) {
                $attribute = $customerAttribute;
            }
        }

        return $attribute;
    }

    /**
     * Retrieve store attribute label
     *
     * @param string $attributeCode
     *
     * @return string
     */
    public function getStoreLabel($attributeCode)
    {
        $attribute = $this->_getAttribute($attributeCode);
        return $attribute ? __($attribute->getStoreLabel()) : '';
    }

    /**
     * Get string with frontend validation classes for attribute
     *
     * @param string $attributeCode
     *
     * @return string
     */
    public function getAttributeValidationClass($attributeCode)
    {
        return $this->_addressHelper->getAttributeValidationClass($attributeCode);
    }

    /**
     * @param string $attributeCode
     *
     * @return bool
     */
    private function _isAttributeVisible($attributeCode)
    {
        $attributeMetadata = $this->_getAttribute($attributeCode);
        return $attributeMetadata ? (bool)$attributeMetadata->isVisible() : false;
    }

    /**
     * Check if account number attribute enabled in system
     *
     * @return bool
     */
    public function isEnabled()
    {
        return $this->_getAttribute(self::ATTRIBUTE_CODE) ? (bool)$this->_getAttribute(self::ATTRIBUTE_CODE)->isVisible(
        ) : false;
    }

    /**
     * Check if account number attribute marked as required
     *
     * @return bool
     */
    public function isRequired()
    {
        return $this->_getAttribute(self::ATTRIBUTE_CODE) ? (bool)$this->_getAttribute(self::ATTRIBUTE_CODE)
            ->isRequired() : false;
    }
}


Step 8: Now to display our custom attribute in the header links of header part, we have declared the custom block to render the custom template: 

Location: app/code/Magemeta/Customer/view/frontend/layout/customer_account.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd" label="Customer My Account (All Pages)" design_abstraction="custom">
    <body>
        <referenceBlock name="header.links">
            <block class="Magemeta\Customer\Block\Header"  name="magemeta_header"
                   template="Magemeta_Customer::welcome.phtml" before="-"/>
        </referenceBlock>
    </body>
</page>

Location: app/code/Magemeta/Customer/Block/Header.php

<?php

namespace Magemeta\Customer\Block;

use Magento\Framework\View\Element\Template\Context;

/**
* Class Header
* @package Magemeta\Customer\Block
*/
class Header extends \Magento\Framework\View\Element\Template
{
/**
* Header constructor.
* @param Context $context
* @param array $data
*/
public function __construct(
Context $context,
array $data = []
) {
parent::__construct($context, $data);
}
}

Location: app/code/Magemeta/Customer/view/frontend/templates/welcome.phtml

<?php
/** @var \Magemeta\Customer\Block\Header $block */
?>
<li class="account-number" data-bind="scope: 'customer'">
<!-- ko if: customer().account_number -->
<div>
<span class="logged-in"
data-bind="text: new String('<?= $block->escapeHtml(__('Account Number: %1', '%1')) ?>').replace('%1', customer().account_number)">
</span>
</div>
<!-- /ko -->
</li>

Step 9: Now to call the custom widget for custom attribute, we need to call the following code snippet in customer following templates: 

Location: app/code/Magemeta/Customer/view/frontend/templates/customer/form/edit.phtml

This is used to display custom attribute inside edit account form on where you can edit and save the attribute.
<?php /*************Start of custom code ***************/?>
<?php $_accountNumber = $block->getLayout()->createBlock('Magemeta\Customer\Block\Widget\AccountNumber'); ?>
<?php if ($_accountNumber->isEnabled()): ?>
    <?= $_accountNumber->setAccountNumber($block->getCustomer()->getAccountNumber())->toHtml() ?>
<?php endif ?>
<?php /*************End of custom code ***************/?>

Location: app/code/Magemeta/Customer/view/frontend/templates/customer/form/register.phtml

This is used to display custom attribute inside create account form on create account page.

<?php /*************Start of custom code ***************/?>
<?php $_accountNumber = $block->getLayout()->createBlock('Magemeta\Customer\Block\Widget\AccountNumber'); ?>
<?php if ($_accountNumber->isEnabled()): ?>
    <?= $_accountNumber->setAccountNumber($block->getFormData()->getAccountNumber())->toHtml() ?>
<?php endif ?>
<?php /*************End of custom code ***************/?>

We are done with the custom extension for our custom requirement. That's it.

 

Leave a Reply