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?