Magento2 - programmatically add product attribute options

  • 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.

  • Ryan Hoerr

    Ryan Hoerr Correct answer

    5 years ago

    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->attributeRepository = $attributeRepository;
            $this->tableFactory = $tableFactory;
            $this->attributeOptionManagement = $attributeOptionManagement;
            $this->optionLabelFactory = $optionLabelFactory;
            $this->optionFactory = $optionFactory;
        }
    
        /**
         * Get attribute by code.
         *
         * @param string $attributeCode
         * @return \Magento\Catalog\Api\Data\ProductAttributeInterface
         */
        public function getAttribute($attributeCode)
        {
            return $this->attributeRepository->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) < 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.
    
                /** @var \Magento\Eav\Model\Entity\Attribute\OptionLabel $optionLabel */
                $optionLabel = $this->optionLabelFactory->create();
                $optionLabel->setStoreId(0);
                $optionLabel->setLabel($label);
    
                $option = $this->optionFactory->create();
                $option->setLabel($optionLabel);
                $option->setStoreLabels([$optionLabel]);
                $option->setSortOrder(0);
                $option->setIsDefault(false);
    
                $this->attributeOptionManagement->add(
                    \Magento\Catalog\Model\Product::ENTITY,
                    $this->getAttribute($attributeCode)->getAttributeId(),
                    $option
                );
    
                // Get the inserted ID. Should be returned from the installer, but it isn't.
                $optionId = $this->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->getAttribute($attributeCode);
    
            // Build option array if necessary
            if ($force === true || !isset($this->attributeValues[ $attribute->getAttributeId() ])) {
                $this->attributeValues[ $attribute->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->tableFactory->create();
                $sourceModel->setAttribute($attribute);
    
                foreach ($sourceModel->getAllOptions() as $option) {
                    $this->attributeValues[ $attribute->getAttributeId() ][ $option['label'] ] = $option['value'];
                }
            }
    
            // Return option ID if exists
            if (isset($this->attributeValues[ $attribute->getAttributeId() ][ $label ])) {
                return $this->attributeValues[ $attribute->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+.

    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.

    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.

    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.

    @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.

    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.

    @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.

    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 :)

    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.

    Why not use `\Magento\Eav\Api\AttributeOptionManagementInterface`?

    Hello Ryan, How can I execute same process at time of module's installation.

    @RyanHoerr i have try this code but option not created

    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"

    Yes this code not working

    @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

    update: 2020-07-07 it still does not return an option id. cool

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM

Tags used