Magento – How to use extension attributes for customer EAV attributes

best practicecustomerextension-attributesmagento2

I've created custom customer attributes via InstallData and want to make them available as extension attributes. But I have trouble with the admin interface and am unsure if I'm doing it right.

I've come so far:

Register extension attributes

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\CustomerInterface">
        <attribute code="customer_number" type="string"/>
    </extension_attributes>
</config>

Create plugins

<?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\Customer\Api\CustomerRepositoryInterface">
        <plugin name="save_extension_attributes" type="Project\CustomerAttributes\Plugin\CustomerSave"/>
        <plugin name="get_extension_attributes" type="Project\CustomerAttributes\Plugin\CustomerGet"/>
    </type>
    <type name="\Magento\Customer\Api\Data\CustomerInterfaceFactory">
        <plugin name="create_extension_attributes" type="Project\CustomerAttributes\Plugin\CustomerCreate" />
    </type>
</config>

There are plugins for:

  • CustomerRepositoryInterface::beforeSave
  • CustomerRepositoryInterface::afterGetList
  • CustomerRepositoryInterface::afterGet
  • CustomerRepositoryInterface::afterGetById
  • CustomerInterfaceFactory::afterCreate

In the plugins I copy the value from and to the custom attributes. Abbreviated example for a plugin:

public function afterCreate(CustomerInterfaceFactory $subject, CustomerInterface $customer) //@codingStandardsIgnoreLine
{
    $this->extensionAttributes->addFromCustomAttributes($customer);
    return $customer;
}

with this method in a common class

public function addFromCustomAttributes(CustomerInterface $customer)
{
    if ($customer->getExtensionAttributes() === null) {
        $customer->setExtensionAttributes($this->extensionAttributeFactory->create(CustomerInterface::class));
    }
    $customer->getExtensionAttributes()->setCustomerNumber(
        $this->getCustomAttributeValue($customer, 'customer_number')
    );
}

The issue

When saving a customer from the admin panel, the extension attribute data is lost. The controller instantiates the customer like this:

$customer = $this->customerDataFactory->create();
$this->dataObjectHelper->populateWithArray(
    $customer,
    $customerData,
    '\Magento\Customer\Api\Data\CustomerInterface'
);

...

$this->_customerRepository->save($customer);

The $customerData array contains the custom attributes. My beforeSave plugin overwrites them from the extension attributes, which were never populated.

A solution?

Now I could add some checks, like if there are already changed values in the custom attributes, or if the extension attribute value is null. But this will probably lead to subtle bugs if for example I want to set an extension attribute value to null explicitly.

But while debugging, I found that DataObjectHelper::populateWithArray() does something with the extension attributes as well.

For extensible models, it calls \Magento\Framework\Api\ExtensionAttribute\JoinProcessor::extractExtensionAttributes, which looks for "join directives" in the extension attribute configuration. .

I've seen examples like this: How to add extension attribute Magento 2 (this one with an interesting comment in the answer by Magento architect Vitaliy Korotun that the join processor is not used everywhere. But for customers it's obviously used)

But the examples are always with external tables. What if I just want to use the customer EAV tables? I tried to make it explicit:

<attribute code="customer_number" type="string">
    <join reference_table="customer_entity_varchar" join_on_field="entity_id" reference_field="entity_id">
        <field>value</field>
    </join>
</attribute>

This does not work, it looks for the key extension_attribute_customer_number_value in the data array, where I just need customer_number.

So the main question is, is there a standard way to use extension attributes for EAV attributes, with the join processor involved?

Or is such a standard use case really that complicated?

Best Answer

same issue on M2.1.8 community edition

  1. magento logic ignore extension attribute data since it's not custom attribute, as result after populateWithArray() we lost extension attribute

  2. if use xml join directive magento logic expects for the key wich built like

"extension_attribute_" + xml attribute code value + xml field value

but actually we have only key without prefixes

  1. as test experiment if we hack vendor code with this key (add expected prefix) the logic starts work as needed, the customer is populated with needed data, interesting..