35

What is the right(official) way to programmatically add product attribute option in M2? E.g. for manufacturer product attribute. Obviously existing option would be matched by "Admin" title value.

werd
  • 545
  • 2
  • 5
  • 9

7 Answers7

73

Here's the approach I've come up with for handling attribute options. Helper class:

<?php
namespace My\Module\Helper;

class Data extends \Magento\Framework\App\Helper\AbstractHelper { /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface */ protected $attributeRepository;

/**
 * @var array
 */
protected $attributeValues;

/**
 * @var \Magento\Eav\Model\Entity\Attribute\Source\TableFactory
 */
protected $tableFactory;

/**
 * @var \Magento\Eav\Api\AttributeOptionManagementInterface
 */
protected $attributeOptionManagement;

/**
 * @var \Magento\Eav\Api\Data\AttributeOptionLabelInterfaceFactory
 */
protected $optionLabelFactory;

/**
 * @var \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory
 */
protected $optionFactory;

/**
 * Data constructor.
 *
 * @param \Magento\Framework\App\Helper\Context $context
 * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository
 * @param \Magento\Eav\Model\Entity\Attribute\Source\TableFactory $tableFactory
 * @param \Magento\Eav\Api\AttributeOptionManagementInterface $attributeOptionManagement
 * @param \Magento\Eav\Api\Data\AttributeOptionLabelInterfaceFactory $optionLabelFactory
 * @param \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory $optionFactory
 */
public function __construct(
    \Magento\Framework\App\Helper\Context $context,
    \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository,
    \Magento\Eav\Model\Entity\Attribute\Source\TableFactory $tableFactory,
    \Magento\Eav\Api\AttributeOptionManagementInterface $attributeOptionManagement,
    \Magento\Eav\Api\Data\AttributeOptionLabelInterfaceFactory $optionLabelFactory,
    \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory $optionFactory
) {
    parent::__construct($context);

    $this-&gt;attributeRepository = $attributeRepository;
    $this-&gt;tableFactory = $tableFactory;
    $this-&gt;attributeOptionManagement = $attributeOptionManagement;
    $this-&gt;optionLabelFactory = $optionLabelFactory;
    $this-&gt;optionFactory = $optionFactory;
}

/**
 * Get attribute by code.
 *
 * @param string $attributeCode
 * @return \Magento\Catalog\Api\Data\ProductAttributeInterface
 */
public function getAttribute($attributeCode)
{
    return $this-&gt;attributeRepository-&gt;get($attributeCode);
}

/**
 * Find or create a matching attribute option
 *
 * @param string $attributeCode Attribute the option should exist in
 * @param string $label Label to find or add
 * @return int
 * @throws \Magento\Framework\Exception\LocalizedException
 */
public function createOrGetId($attributeCode, $label)
{
    if (strlen($label) &lt; 1) {
        throw new \Magento\Framework\Exception\LocalizedException(
            __('Label for %1 must not be empty.', $attributeCode)
        );
    }

    // Does it already exist?
    $optionId = $this-&gt;getOptionId($attributeCode, $label);

    if (!$optionId) {
        // If no, add it.

        /** @var \Magento\Eav\Model\Entity\Attribute\OptionLabel $optionLabel */
        $optionLabel = $this-&gt;optionLabelFactory-&gt;create();
        $optionLabel-&gt;setStoreId(0);
        $optionLabel-&gt;setLabel($label);

        $option = $this-&gt;optionFactory-&gt;create();
        $option-&gt;setLabel($optionLabel-&gt;getLabel());
        $option-&gt;setStoreLabels([$optionLabel]);
        $option-&gt;setSortOrder(0);
        $option-&gt;setIsDefault(false);

        $this-&gt;attributeOptionManagement-&gt;add(
            \Magento\Catalog\Model\Product::ENTITY,
            $this-&gt;getAttribute($attributeCode)-&gt;getAttributeId(),
            $option
        );

        // Get the inserted ID. Should be returned from the installer, but it isn't.
        $optionId = $this-&gt;getOptionId($attributeCode, $label, true);
    }

    return $optionId;
}

/**
 * Find the ID of an option matching $label, if any.
 *
 * @param string $attributeCode Attribute code
 * @param string $label Label to find
 * @param bool $force If true, will fetch the options even if they're already cached.
 * @return int|false
 */
public function getOptionId($attributeCode, $label, $force = false)
{
    /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
    $attribute = $this-&gt;getAttribute($attributeCode);

    // Build option array if necessary
    if ($force === true || !isset($this-&gt;attributeValues[ $attribute-&gt;getAttributeId() ])) {
        $this-&gt;attributeValues[ $attribute-&gt;getAttributeId() ] = [];

        // We have to generate a new sourceModel instance each time through to prevent it from
        // referencing its _options cache. No other way to get it to pick up newly-added values.

        /** @var \Magento\Eav\Model\Entity\Attribute\Source\Table $sourceModel */
        $sourceModel = $this-&gt;tableFactory-&gt;create();
        $sourceModel-&gt;setAttribute($attribute);

        foreach ($sourceModel-&gt;getAllOptions() as $option) {
            $this-&gt;attributeValues[ $attribute-&gt;getAttributeId() ][ $option['label'] ] = $option['value'];
        }
    }

    // Return option ID if exists
    if (isset($this-&gt;attributeValues[ $attribute-&gt;getAttributeId() ][ $label ])) {
        return $this-&gt;attributeValues[ $attribute-&gt;getAttributeId() ][ $label ];
    }

    // Return false if does not exist
    return false;
}

}

Then, either in the same class or including it via dependency injection, you can add or get your option ID by calling createOrGetId($attributeCode, $label).

For example, if you inject My\Module\Helper\Data as $this->moduleHelper, then you can call:

$manufacturerId = $this->moduleHelper->createOrGetId('manufacturer', 'ABC Corp');

If 'ABC Corp' is an existing manufacturer, it will pull the ID. If not, it will add it.

UPDATED 2016-09-09: Per Ruud N., the original solution used CatalogSetup, which resulted in a bug starting in Magento 2.1. This revised solution bypasses that model, creating the option and label explicitly. It should work on 2.0+.

Ryan Hoerr
  • 12,271
  • 7
  • 47
  • 54
  • Thanks! I guess this is not the "official" way of solving the issue, right? However I was not able to find any actual reference to product attribute option management in Magento2 source itself. – werd Feb 29 '16 at 15:27
  • 3
    It's as official as you're going to get. All of the lookups and option adding go through core Magento. My class is just a wrapper for those core methods that makes them easier to use. – Ryan Hoerr Feb 29 '16 at 15:35
  • Thanks Ryan H. it's very useful piece of code.

    Actually I have a question. If option is added to store with id 0 (admin) it will be rendered with the same label in front in store f.e. with id 1. What can I do to change label for frontend? Just 1 => 'something else'? If I have done it adopting this tutorial: http://magentorex.com/magento-get-product-attributes-option-id-from-option-label/ But function getting option id, only returns ids of attributes for store 1 (frontend) and not for admin.

    – Bartosz Kubicki Jun 17 '16 at 17:48
  • @lord_of_strings: Yes, that's correct. Where you see 0 => $label,, that's an array of store translations. The key is the store ID, value is the text for that store. The code assumes you'll be setting and getting everything in the default scope, but you could adapt it to use a store. – Ryan Hoerr Jun 17 '16 at 17:54
  • Ok, thank you I will test in on monday, the key is change something here:

    `$productModel = Mage::getModel('catalog/product');

    $attr = $productModel->getResource()->getAttribute("size"); if ($attr->usesSource()) { echo $size_id = $attr->getSource()->getOptionId("XL"); }`

    to force to look not i default scope but in admin. Is it correct if I have some integer value for attribute option in scope of admin and some string for frontend - I don't know if I understand concept of these labels correctly.

    – Bartosz Kubicki Jun 17 '16 at 18:01
  • @lord_of_strings: Note that the code I posted is for Magento 2. If you're not using Magento 2, it won't be of much use to you. – Ryan Hoerr Jun 17 '16 at 18:16
  • I am writing it also in Magento 2 :) I know Magento 1.9 too, so tutorial I posted isn't exactly useful, but can be easily adopted. Names of methods are similar or even the same and I have already found the way to get admin value of atrribute's option it can be done (I think) with use of some collecion and proper filtering :) – Bartosz Kubicki Jun 17 '16 at 18:22
  • 1
    Hi Ryan, you shouldn't set the value on the option, this is the internal id magento uses and I found out the hard way that if you set the value to a string value with a leading number like '123 abc corp' it causes some serious problems due to the implementation of Magento\Eav\Model\ResourceModel\Entity\Attribute::_processAttributeOptions. See for yourself, if you remove the $option->setValue($label); statement from your code, it will save the option, then when you fetch it Magento will return the value from an auto-increment on the eav_attribute_option table. – quickshiftin Sep 12 '16 at 19:37
  • Why not use \Magento\Eav\Api\AttributeOptionManagementInterface? – Erfan Jun 07 '17 at 04:16
  • Hello Ryan, How can I execute same process at time of module's installation. – DEEP JOSHI Oct 13 '17 at 12:44
  • @RyanHoerr i have try this code but option not created – Ajay Patel Nov 20 '17 at 07:36
  • 2
    if I add this in a foreach function, in the second iteration I will get the error "Magento\Eav\Model\Entity\Attribute\OptionManagement::setOptionValue() must be of the type string, object given" – JJ15 Dec 12 '18 at 14:57
  • 1
    Yes this code not working – Sourav Mar 23 '19 at 11:45
  • 2
    @JELLEJ If you are getting issue Uncaught TypeError: Argument 3 passed to Magento\Eav\Model\Entity\Attribute\OptionManagement::setOptionValue() must be of the type string, object given in foreach function then change $option->setLabel($optionLabel); to $option->setLabel($label); at line 102 – Nadeem0035 Apr 17 '19 at 07:21
  • update: 2020-07-07 it still does not return an option id. cool – Kharidas Chebotaryov Jul 16 '20 at 12:29
  • 1
    The code is working on Magento 2.4.1, except for a little bug: the check if the option value already exists is done in case sensitive, but Magento give then an error saying the value already exists, if the same label with different case is found. I solved it by adding strtolower on the label in the last isset check – Francesco Gasparetto Sep 16 '21 at 15:29
15

tested on Magento 2.1.3.

I didn't find any workable way how to create attribute with options at once. So initially we need to create an attribute and then add options for it.

Inject following class \Magento\Eav\Setup\EavSetupFactory

 $setup->startSetup();

 /** @var \Magento\Eav\Setup\EavSetup $eavSetup */
 $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);

Create new attribute:

$eavSetup->addAttribute(
    'catalog_product',
    $attributeCode,
    [
        'type' => 'varchar',
        'input' => 'select',
        'required' => false,
        ...
    ],
);

Add custom options.

Function addAttribute doesn't return anything useful which can be used in future. So after attribute creation we need to retrieve attribute object by ourself. !!!Important We need it because function expects only attribute_id, but don't want to work with attribute_code.

In that case we need to get attribute_id and pass it to attribute creation function.

$attributeId = $eavSetup->getAttributeId('catalog_product', 'attribute_code');

Then we need to generate options array in the way magento expects:

$options = [
        'values' => [
        'sort_order1' => 'title1',
        'sort_order2' => 'title2',
        'sort_order3' => 'title3',
    ],
    'attribute_id' => 'some_id',
];

As example:

$options = [
        'values' => [
        '1' => 'Red',
        '2' => 'Yellow',
        '3' => 'Green',
    ],
    'attribute_id' => '32',
];

And pass it to function:

$eavSetup->addAttributeOption($options);
zhartaunik
  • 3,848
  • 4
  • 23
  • 52
10

Using the Magento\Eav\Setup\EavSetupFactory or even the \Magento\Catalog\Setup\CategorySetupFactory class may lead to the following problem: https://github.com/magento/magento2/issues/4896.

The classes you should use:

protected $_logger;

protected $_attributeRepository;

protected $_attributeOptionManagement;

protected $_option;

protected $_attributeOptionLabel;

 public function __construct(
    \Psr\Log\LoggerInterface $logger,
    \Magento\Eav\Model\AttributeRepository $attributeRepository,
    \Magento\Eav\Api\AttributeOptionManagementInterface $attributeOptionManagement,
    \Magento\Eav\Api\Data\AttributeOptionLabelInterface $attributeOptionLabel,
    \Magento\Eav\Model\Entity\Attribute\Option $option
  ){
    $this->_logger = $logger;
    $this->_attributeRepository = $attributeRepository;
    $this->_attributeOptionManagement = $attributeOptionManagement;
    $this->_option = $option;
    $this->_attributeOptionLabel = $attributeOptionLabel;
 }

Then in your function do something like this:

 $attribute_id = $this->_attributeRepository->get('catalog_product', 'your_attribute')->getAttributeId();
$options = $this->_attributeOptionManagement->getItems('catalog_product', $attribute_id);
/* if attribute option already exists, remove it */
foreach($options as $option) {
  if ($option->getLabel() == $oldname) {
    $this->_attributeOptionManagement->delete('catalog_product', $attribute_id, $option->getValue());
  }
}

/* new attribute option */
  $this->_option->setValue($name);
  $this->_attributeOptionLabel->setStoreId(0);
  $this->_attributeOptionLabel->setLabel($name);
  $this->_option->setLabel($this->_attributeOptionLabel);
  $this->_option->setStoreLabels([$this->_attributeOptionLabel]);
  $this->_option->setSortOrder(0);
  $this->_option->setIsDefault(false);
  $this->_attributeOptionManagement->add('catalog_product', $attribute_id, $this->_option);
Ruud N.
  • 121
  • 1
  • 7
  • 1
    Thanks, you are correct. I've updated my answer accordingly. Note that $attributeOptionLabel and $option are ORM classes; you should not inject them directly. The proper approach is to inject their factory class, then create an instance as needed. Also note you aren't using the API data interfaces consistently. – Ryan Hoerr Sep 09 '16 at 14:55
  • 3
    Hi @Rudd, see my comment on Ryan's answer. You don't want to call $option->setValue() as that is for an internal magento option_id field on the eav_attribute_option table. – quickshiftin Sep 12 '16 at 19:42
  • Thank you. That's what I found out too. Will edit my answer accordingly. – Ruud N. Sep 14 '16 at 05:49
  • 1
    @RuudN.,not working for me, \OptionManagement::setOptionValue() must be of the type string, object given, called in – Jafar Pinjar Mar 23 '20 at 07:39
2

Here's updated code based on Ryan Hoerr. Since magento 2.3.x it's a little bit different.

Update createOrGetId function.

public function createOrGetId($attributeCode, $label) {
        if (strlen($label) < 1) {
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Label for %1 must not be empty.', $attributeCode)
            );
        }
    try {
      $optionId = $this-&gt;getOptionId($attributeCode, $label);

      if (!$optionId) {
          // If no, add it.

          /** @var \Magento\Eav\Model\Entity\Attribute\OptionLabel $optionLabel */
          $optionLabel = $this-&gt;optionLabelFactory-&gt;create();
          $optionLabel-&gt;setStoreId(0);
          $optionLabel-&gt;setLabel($label);

          $option = $this-&gt;optionFactory-&gt;create();
          $option-&gt;setLabel($optionLabel-&gt;getLabel());
          // $option-&gt;setLabel($label);
          $option-&gt;setStoreLabels([$optionLabel]);
          $option-&gt;setSortOrder(0);
          $option-&gt;setIsDefault(false);

          $this-&gt;attributeOptionManagement-&gt;add(
              \Magento\Catalog\Model\Product::ENTITY,
              $this-&gt;getAttribute($attributeCode)-&gt;getAttributeId(),
              $option
          );

          // Get the inserted ID. Should be returned from the installer, but it isn't.
          $optionId = $this-&gt;getOptionId($attributeCode, $label, true);
      }
    } catch (\Exception $e) {
      throw new \Exception($e-&gt;getMessage());
    }

    // Does it already exist?

    return $optionId;
}

Xin Li
  • 21
  • 1
1

For Magento 2.3.3 I found that you can take Magento DevTeam approach.

  • Add Patch
bin/magento setup:db-declaration:generate-patch Vendor_Module PatchName
  • Add CategorySetupFactory to constructor
public function __construct(
        ModuleDataSetupInterface $moduleDataSetup,
        Factory $configFactory
        CategorySetupFactory $categorySetupFactory
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
        $this->configFactory = $configFactory;
        $this->categorySetupFactory = $categorySetupFactory;
}
  • Add attribute in apply() function

    public function apply()
    {
        $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]);
    
        $categorySetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'custom_layout',
            [
                'type' => 'varchar',
                'label' => 'New Layout',
                'input' => 'select',
                'source' => \Magento\Catalog\Model\Product\Attribute\Source\Layout::class,
                'required' => false,
                'sort_order' => 50,
                'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
                'group' => 'Schedule Design Update',
                'is_used_in_grid' => true,
                'is_visible_in_grid' => false,
                'is_filterable_in_grid' => false
            ]
        );
    }
    
embed0
  • 192
  • 2
  • 12
  • uhmm i just find out that i wanted to add this answer to diffrent question. I will just live it here and add reference to this answer there. I hope that it is ok. This is partiaclly answer for this question as well :) – embed0 Nov 04 '19 at 13:59
  • Can you please add the complete solution if possible ? Thanks in advance :) – shankar boss Feb 11 '20 at 07:16
1

For those who are receiving error after implementing accepted answer of @reyan-hoeer

PHP Fatal error:  Uncaught TypeError: Argument 3 passed to Magento\\Eav\\Model\\Entity\\Attribute\\OptionManagement::setOptionValue() must be of the type string, object given, called in ....

To fix this issue, update the function createOrGetId with following

    /**
     * Find or create a matching attribute option
     *
     * @param string $attributeCode Attribute the option should exist in
     * @param string $label Label to find or add
     * @return int
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function createOrGetId($attributeCode, $label)
    {
        if (strlen($label) < 1) {
             throw new \Magento\Framework\Exception\LocalizedException(
                 __('Label for %1 must not be empty.', $attributeCode)
             );
        }

        // Does it already exist?
        $optionId = $this->getOptionId($attributeCode, $label);

        if (!$optionId) {
            // If no, add it.
            try{
                /** @var \Magento\Eav\Model\Entity\Attribute\OptionLabel $optionLabel */
                $optionLabel = $this->optionLabelFactory->create();
                $optionLabel->setStoreId(0);
                $optionLabel->setLabel($label);

                $option = $this->optionFactory->create();
                $option->setLabel($label); // this line is changed to fix the error.
                $option->setStoreLabels([$optionLabel]);
                $option->setSortOrder(0);
                $option->setIsDefault(false);

                $this->attributeOptionManagement->add(
                    \Magento\Catalog\Model\Product::ENTITY,
                    $this->getAttribute($attributeCode)->getAttributeId(),
                    $option
                );
            }catch(\Exception $e){
                $message = $e->getMessage();
                echo $message;
            }

            // Get the inserted ID. Should be returned from the installer, but it isn't.
            $optionId = $this->getOptionId($attributeCode, $label, true);
        }

        return $optionId;
    }
1

My two cents here (similar to some of the answers provided):

<?php
declare(strict_types=1);

namespace Acme\Setup\Setup\Patch\Data;

use Magento\Customer\Model\Indexer\Address\AttributeProvider; use Magento\Customer\Setup\CustomerSetupFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface;

class CustomerAddressTitleAttributeExtraOptions implements DataPatchInterface { private ModuleDataSetupInterface $moduleDataSetup;

private CustomerSetupFactory $customerSetupFactory;

/**
 * @param ModuleDataSetupInterface $moduleDataSetup
 * @param CustomerSetupFactory     $customerSetupFactory
 */
public function __construct(
    ModuleDataSetupInterface $moduleDataSetup,
    CustomerSetupFactory $customerSetupFactory,
) {
    $this-&gt;moduleDataSetup = $moduleDataSetup;
    $this-&gt;customerSetupFactory = $customerSetupFactory;
}

/**
 * @return void
 * @throws LocalizedException
 */
public function apply(): void
{
    $customerSetup = $this-&gt;customerSetupFactory-&gt;create(['setup' =&gt; $this-&gt;moduleDataSetup]);

    $attributeCode = 'title';
    $attributeId = $customerSetup-&gt;getAttributeId(AttributeProvider::ENTITY, $attributeCode);

    $options = [
        &quot;Baron&quot;,
        &quot;Baroness&quot;,
        &quot;Countess&quot;,
        &quot;Dame&quot;,
        &quot;Dr&quot;,
        &quot;Earl&quot;,
        &quot;Lady&quot;,
        &quot;Lord&quot;,
        &quot;Miss&quot;,
        &quot;Mr&quot;,
        &quot;Mrs&quot;,
        &quot;Ms&quot;,
        &quot;Prince&quot;,
        &quot;Princess&quot;,
        &quot;Professor&quot;,
        &quot;Sir&quot;,
        &quot;The Duchess of&quot;,
        &quot;The Duchess of&quot;,
        &quot;The Duke of&quot;,
        &quot;The Earl of&quot;,
        &quot;The Marchioness of&quot;,
        &quot;The Marquess&quot;,
        &quot;The Revd&quot;,
        &quot;Viscount&quot;,
        &quot;Viscountess&quot;,
        &quot;The Hon&quot;,
        &quot;The Hon Mrs&quot;,
        &quot;None&quot;,
    ];

    $customerSetup-&gt;addAttributeOption([
        'values'       =&gt; $options,
        'attribute_id' =&gt; $attributeId
    ]);
}

/**
 * {@inheritdoc}
 */
public static function getDependencies(): array
{
    return [];
}

/**
 * {@inheritdoc}
 */
public function getAliases(): array
{
    return [];
}

}

diazwatson
  • 2,430
  • 2
  • 27
  • 37