B2B stores often need extra customer data: a trade account number, a company registration number or a preferred delivery depot. In Magento Open Source you add these as customer EAV attributes with a data patch. In this guide we add a Trade Account Number attribute, make it editable in the admin, show it in the customer grid and collect it at registration.
Step 1: Declare Module Dependencies
Our module, MageServices_Customer, depends on the customer module, so declare it in 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="MageServices_Customer">
<sequence>
<module name="Magento_Customer"/>
</sequence>
</module>
</config>
Step 2: The Data Patch
Customer attributes need two extra things compared to product attributes: an attribute set and group assignment, and a list of forms where the attribute is used. The forms control where Magento accepts and saves the value.
<?php
declare(strict_types=1);
namespace MageServices\Customer\Setup\Patch\Data;
use Magento\Customer\Model\Customer;
use Magento\Customer\Model\ResourceModel\Attribute as AttributeResource;
use Magento\Customer\Setup\CustomerSetup;
use Magento\Customer\Setup\CustomerSetupFactory;
use Magento\Eav\Model\Entity\Attribute\SetFactory as AttributeSetFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;
class AddTradeAccountNumberAttribute implements DataPatchInterface, PatchRevertableInterface
{
private const ATTRIBUTE_CODE = 'trade_account_number';
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly CustomerSetupFactory $customerSetupFactory,
private readonly AttributeSetFactory $attributeSetFactory,
private readonly AttributeResource $attributeResource
) {
}
public function apply(): self
{
$this->moduleDataSetup->getConnection()->startSetup();
/** @var CustomerSetup $customerSetup */
$customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]);
$customerEntity = $customerSetup->getEavConfig()->getEntityType(Customer::ENTITY);
$attributeSetId = (int) $customerEntity->getDefaultAttributeSetId();
$attributeGroupId = (int) $this->attributeSetFactory->create()->getDefaultGroupId($attributeSetId);
$customerSetup->addAttribute(Customer::ENTITY, self::ATTRIBUTE_CODE, [
'type' => 'varchar',
'label' => 'Trade Account Number',
'input' => 'text',
'required' => false,
'visible' => true,
'user_defined' => true,
'system' => false,
'position' => 200,
'validate_rules' => '{"max_text_length":32,"input_validation":"alphanumeric"}',
'is_used_in_grid' => true,
'is_visible_in_grid' => true,
'is_filterable_in_grid' => true,
'is_searchable_in_grid' => true,
]);
$attribute = $customerSetup->getEavConfig()->getAttribute(Customer::ENTITY, self::ATTRIBUTE_CODE);
$attribute->addData([
'attribute_set_id' => $attributeSetId,
'attribute_group_id' => $attributeGroupId,
'used_in_forms' => [
'adminhtml_customer',
'customer_account_create',
'customer_account_edit',
],
]);
$this->attributeResource->save($attribute);
$this->moduleDataSetup->getConnection()->endSetup();
return $this;
}
public function revert(): void
{
$this->moduleDataSetup->getConnection()->startSetup();
/** @var CustomerSetup $customerSetup */
$customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]);
$customerSetup->removeAttribute(Customer::ENTITY, self::ATTRIBUTE_CODE);
$this->moduleDataSetup->getConnection()->endSetup();
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}
We save the attribute through the customer attribute resource model rather than calling $attribute->save(), which is deprecated.
Step 3: Run the Patch and Reindex the Grid
bin/magento setup:upgrade
bin/magento indexer:reindex customer_grid
bin/magento cache:flush
Open a customer in the admin: the field appears in the Account Information tab. After reindexing, it can be added as a column in the customer grid and used as a filter.
Step 4: Add the Field to the Registration Form
Magento Open Source does not render custom customer attributes on the storefront automatically. Because the attribute is used in customer_account_create, Magento will save a submitted value — we only need to output the input. The registration form has a container called form.additional.info for this purpose.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="form.additional.info">
<block name="mageservices.customer.trade_account"
template="MageServices_Customer::form/trade-account.phtml"/>
</referenceContainer>
</body>
</page>
<?php
declare(strict_types=1);
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
?>
<div class="field trade-account-number">
<label class="label" for="trade_account_number">
<span><?= $escaper->escapeHtml(__('Trade Account Number')) ?></span>
</label>
<div class="control">
<input type="text"
id="trade_account_number"
name="trade_account_number"
maxlength="32"
class="input-text"
title="<?= $escaper->escapeHtmlAttr(__('Trade Account Number')) ?>">
</div>
</div>
For the account edit page use the customer_account_edit.xml handle and pre-fill the value from the logged-in customer, for example through a ViewModel that uses Magento\Customer\Model\Session or the customer repository.
On Hyvä, the registration template is different but the approach is the same: add a block to the form's additional-info container in your Hyvä theme or module and style the input with Tailwind classes.
Reading the Value in Code
$customer = $this->customerRepository->getById($customerId);
$attribute = $customer->getCustomAttribute('trade_account_number');
$tradeAccount = $attribute ? (string) $attribute->getValue() : null;
Adobe Commerce Note
Adobe Commerce includes a Customer Attributes screen that creates attributes and adds them to storefront forms without code. A data patch is still useful there when you want the same attribute in every environment automatically.
Exposing the Attribute in APIs
Custom customer attributes are returned in the REST API under custom_attributes of the customer object automatically. For GraphQL, customer custom attributes are exposed through the customer attribute features available in recent Magento versions; for older versions or special formatting, add a field to the Customer type in your module's schema.graphqls with a small resolver.
Validation and Privacy
- Use
validate_rulesto restrict length and characters, and validate again in any custom form handling. - Do not store sensitive data such as identity document numbers in customer attributes unless you have a clear legal basis and access controls.
- Remember that customer data in development databases should be anonymised.
Frequently Asked Questions
Why is my customer attribute not saved from the registration form?
Check that customer_account_create is in used_in_forms and that the input name matches the attribute code exactly.
How do I add a customer address attribute?
Use the customer_address entity with CustomerSetup and the customer_address_edit and customer_register_address forms.
Do I need to reindex after adding a grid attribute?
Yes. Run bin/magento indexer:reindex customer_grid so the column and filters work.
Need help with this on your store?
Our Magento engineers can implement it for you, review your code or take on the whole project.
Written by the Magento Services engineering team — Magento 2, Adobe Commerce and Hyvä specialists since 2014. We write about problems we solve on real client stores.