Magento 2 Customer Attributes
Tags: magento, php, custom, attributes
Published: 2021-07-04 16:55:42

## Introduction

Adding a customer attribute programmatically in Magento 2 can be overwhelming to start with, with all its various settings, and trying to get _just_ the right combination of the settings to make an attribute appear on the Admin, being able to save it, and render the saved value on the Admin grid; let alone customize its behavior. This is surely not documented enough for a developer to feel comfortable with.

There are many possible values for each attribute setting and I'm going to try to guide you through what's necessary with scenario based examples. Yes, there are multiple Stack Overflow answers and personal blogs for the very same topic, but here's my take on it. 

We will be using [Magento Data Patches](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/declarative-schema/data-patches.html) for this. I have tested this code on the current latest version - Magento `2.4.2-p1` and I'll try my best to make the code as clean as possible following the Magento best practices. :)

## Simple Text Field

For our first task, let's create a simple text field as our customer attribute. This field will hold the ID of the customer from an external system.

To achieve this, we have main four steps to complete:

- create the attribute with our desired settings
- add the attribute to an attribute set and group
- add additional data to the attribute model (form visibility)
- save the modified attribute model

### Prerequisites

- Create the data patch class under `\Manick\Customer\Setup\Patch\Data` implementing the `\Magento\Framework\Setup\Patch\DataPatchInterface`. For now, implement the interface stubs as empty methods. This class must be placed under the module's `Setup\Patch\Data` namespace to be auto picked up by Magento when we run `bin/magento setup:upgrade`. Note - data patches are applied only once.

```php
class ExternalId implements DataPatchInterface {}
```

- Inject all necessary dependencies in the constructor as follows

```php
public function __construct(
    \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup,
    \Magento\Customer\Setup\CustomerSetupFactory $customerSetupFactory,
    \Magento\Customer\Model\ResourceModel\Attribute $attributeResource,
    \Psr\Log\LoggerInterface $logger
) {
    $this->moduleDataSetup = $moduleDataSetup;
    $this->customerSetup = $customerSetupFactory->create(['setup' => $moduleDataSetup]);
    $this->attributeResource = $attributeResource;
    $this->logger = $logger;
}
```

- Start and end the setup process inside the `apply` method. This performs a couple of prerequisites in MySQL, like turning off foreign key checks and modifying the SQL mode. 

```php
public function apply()
{
    $this->moduleDataSetup->getConnection()->startSetup();

    ... insert patch code from the below steps here ...

    $this->moduleDataSetup->getConnection()->endSetup();
}
```

- We will be using constants from the `\Magento\Customer\Api\CustomerMetadataInterface` class, so make sure to import that.

### Create the attribute

```php
$this->customerSetup->addAttribute(
    CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER,
    'manick_external_id',
    [
        'label' => 'External ID',
        'required' => 0,
        'position' => 100,
        'system' => 0,
        'user_defined' => 1,
        'is_used_in_grid' => 1,
        'is_visible_in_grid' => 1,
        'is_filterable_in_grid' => 1,
        'is_searchable_in_grid' => 1,
    ]
);
```

The arguments passed to `addAttribute` are - entity type ID, attribute code, and attribute settings.

Now let's go over the attribute settings here. By default, an attribute is created as a `text` HTML input for the browser and as a `varchar` database value type. Since we want a simple text field, we do not need to specify the already default value. However, we need to provide the following settings:

setting | meaning | value
--- | --- | ---
label | user facing label | string
required | is the attribute a required field? | `0` or `1`
position | sort order for the form it appears in | number
system | is the attribute system-defined? this should always be `0` for custom attributes | `0` or `1`
user_defined | is the attribute user-defined? this should always be `1` for custom attributes | `0` or `1`
is_used_in_grid | is the attribute used in the customer grid? this needs to be `1` for any of the below values to take effect | `0` or `1`
is_visible_in_grid | is the attribute visible in the customer grid as a column? | `0` or `1`
is_filterable_in_grid | is the attribute filterable in the customer grid under the filter settings? | `0` or `1`
is_searchable_in_grid | is the attribute searchable in the customer grid using the search box? | `0` or `1`

### Add the attribute to set & group

```php
$this->customerSetup->addAttributeToSet(
    CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER,
    CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER,
    null,
    'manick_external_id'
);
```

The arguments passed to `addAttributeToSet` are - entity type ID, attribute set ID, attribute group ID, and attribute code.

There is only one attribute set and group for the customer entity. Setting the group ID as null, allows Magento to pick up the default attribute group ID for the customer entity's only attribute set.

### Display the attribute in forms

```php
$attribute = $this->customerSetup->getEavConfig()->getAttribute(
    CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER,
    'manick_external_id'
);

$attribute->setData('used_in_forms', [
    'adminhtml_customer'
]);
```

This fetches the newly created attribute's model, and makes the attribute visible in the Magento Admin's customer edit page under the _Account Information_ section.

### Save the attribute

Finally, let's save our modified attribute model using its resource model.

```php
$this->attributeResource->save($attribute);
```

### Finishing touches

Before we finish, let's implement the other two method stubs:

```php
public static function getDependencies(): array
{
    return [];
}

public function getAliases(): array
{
    return [];
}
```

The `getDependencies` method should return a list of patches which should be run before this patch, if any dependencies exist. The `getAliases` method should return the list of previous patch names in case this was renamed so that Magento knows not to run this again since patches are run only once.

Now, let's run `bin/magento setup:upgrade` and see our new customer attribute in the Magento Admin.

- Magento Admin's customer form
![customer attribute form field](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ys8ln9hxfg7nwt5ijf1.png)

- Magento Admin's customer grid
![customer attribute grid column](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/25ibfrfsv8xdtx1kywl4.png)
 
Please refer to my [GitHub gist](https://gist.github.com/drpayyne/b58619197b6a7fcffced71a6da2cd5f0) for the full data patch code.