Magento2 Attributes – Fixing ‘Attribute with the Same Code Already Exists’ Error

attributescustomer-attributemagento2

I am trying to create a customer attribute but it gives the following error whenever you try to run the setup:upgrade

Attribute with the same code already exists

I see that attribute with such code is already created despite of errors. After x-debugging,
I noticed that $attribute->save() is trying to create an attribute instead of updating.

I am using Magento EE 2.1.1.
And the module version is changed from 1.0.0 to 1.0.1 for setup upgrade.

File: MagePsycho/Customer/Setup/UpgradeData.php

<?php

namespace MagePsycho\Customer\Setup;

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

/**
 * @codeCoverageIgnore
 */
class UpgradeData implements UpgradeDataInterface
{
    const CUSTOMER_ATTRIBUTE_TWITTER_HANDLE = 'mp_twitter_handle';

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

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

    public function __construct(
        CustomerSetupFactory $customerSetupFactory,
        AttributeSetFactory $attributeSetFactory
    ) {
        $this->customerSetupFactory = $customerSetupFactory;
        $this->attributeSetFactory  = $attributeSetFactory;
    }

    /**
     * {@inheritdoc}
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     */
    public function upgrade( ModuleDataSetupInterface $setup, ModuleContextInterface $context )
    {
        $installer = $setup;

        $installer->startSetup();

        if ($context->getVersion()
            && version_compare($context->getVersion(), '1.0.1') < 0) {
            $this->_execUpgrade101($setup);
        }

        $installer->endSetup();
    }

    protected function _execUpgrade101(ModuleDataSetupInterface $setup)
    {
        $customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);
        $attributesInfo = [
            self::CUSTOMER_ATTRIBUTE_TWITTER_HANDLE => [
                'label'     => 'Twitter Handle',
                'type'      => 'varchar',
                'input'     => 'text',
                'source'    => '',
                'required'  => false,
                'visible'   => true,
                'system'    => false,
                'user_defined'  => true,
                'backend'       => '',
                'position'  => 333,
            ]
        ];

        $customerEntity = $customerSetup->getEavConfig()->getEntityType('customer');
        $attributeSetId = $customerEntity->getDefaultAttributeSetId();

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

        foreach ($attributesInfo as $attributeCode => $attributeParams) {
            $customerSetup->addAttribute(Customer::ENTITY, $attributeCode, $attributeParams);
        }

        $attribute = $customerSetup->getEavConfig()
            ->getAttribute(Customer::ENTITY, self::CUSTOMER_ATTRIBUTE_TWITTER_HANDLE);
        $attribute->addData([
            'attribute_set_id'      => $attributeSetId,
            'attribute_group_id'    => $attributeGroupId,
            'used_in_forms'         => [
                'adminhtml_customer',
                'adminhtml_checkout'
            ],
        ]);
        $attribute->save();
    }
}

What's wrong with the code?

Best Answer

If you:

  1. Call getAttribute() on Magento 2 Eav Config model for particular entity type,
  2. then add new attribute for that entity type,
  3. then call getAttribute() for that new attribute within same request,

you will not get correct data in response, what makes save() call on such malformed attribute object break with "Attribute with the same code already exists" error. Reason for this is caching inside Eav Config model:

https://github.com/magento/magento2/blob/2.1.11/app/code/Magento/Eav/Model/Config.php#L370-L372

In order to work around this cache, and to avoid "Attribute with the same code already exists" error, one should call clear() method on Eav Config model before second getAttribute() call for entity type in question:

    $attribute = $customerSetup->getEavConfig()
        ->clear() // WILL MAKE getAttribute() REINITIALIZE ATTRIBUTE DATA
        ->getAttribute(Customer::ENTITY, self::CUSTOMER_ATTRIBUTE_TWITTER_HANDLE);
    $attribute->addData([
        'attribute_set_id'      => $attributeSetId,
        'attribute_group_id'    => $attributeGroupId,
        'used_in_forms'         => [
            'adminhtml_customer',
            'adminhtml_checkout'
        ],
    ]);
    $attribute->save();
}

Clearing EavConfig should probably be done automatically within addAttribute call in order to cover this edge case, but sadly it is not at this moment so one must do this manually.

Related Topic