Managing custom product options with a shell script

Today, I’ll tell you how to simplify management of your product custom options with a shell script. You just need to add a criteria for a product collection, and each option of every product from the collection will be updated automatically. Our script will fetch product collections by product SKU value.

First of all, we need to add a shell script.

<?php

require_once 'abstract.php';


class Atwix_Shell_Manage_Options extends Mage_Shell_Abstract
{
    /**
     * @inheritdoc
     */
    public function run()
    {

    }
}

$shell = new Atwix_Shell_Manage_Options();
$shell->run();

Our script will perform two scenarios – remove and create options with their values.

The “run” method looks the following way:

/**
* @inheritdoc
*/
public function run()
{
     $args = $this->_args;
     $method = key($args);
     if ($method == 'add') {
         $this->addOptions($args);
     } else if ($method == 'remove') {
         $this->removeOptions($args);
     }
}

Now we’ll add methods to realize the Add scenario.

/**
     * @param $args
     */
    protected function addOptions($args)
    {
        echo "Start adding options..." . PHP_EOL;
        $this->pullArgs($args, array(
            'filter-type' => array(
                'default' => 'like',
                'allowedValues' => array('eq', 'like')
            ),
            'sku' => array(),
            'option-title' => array(),
        ));

        $valueParams = array(
            'title' => array(
                'prompt' => "value \e[32mTitle\e[00m"
            ),
            'price' => array(
                'prompt' => "value \e[32mPrice\e[00m",
                'required' => false,
            ),
            'price_type' => array(
                'prompt' => "value \e[32mType\e[00m",
                'allowedValues' => array('fixed', 'percent')
            ),
            'sort_order' => array(
                'prompt' => "value \e[32mSort Order\e[00m",
                'required' => false,
            ),
            'sku' => array(
                'prompt' => "value \e[32mSKU\e[00m",
                'required' => false,
            )
        );
        $valueData = array();
        $this->pullArgs($valueData, $valueParams);

        $collection = $this->getCollection(array($args['filter-type'] => $args['sku']));

        list(
            $addedOptionCounter,
            $updatedOptionCounter,
            $valueAddCounter,
            $valueUpdateCounter) = $this->doUpdateOptions($collection, array('title' => $args['option-title']), array($valueData));

        echo "$updatedOptionCounter 'options' was updated. \n";
        echo "$addedOptionCounter 'options' was added. \n";
        echo "$valueUpdateCounter 'value' items was update. \n";
        echo "$valueAddCounter 'value' items was added. \n";

    }



    /**
     * @param Mage_Catalog_Model_Resource_Product_Collection $collection
     * @param array $optionData
     * @param array $valuesData
     * @param boolean $getFromPromptOptionsData
     * @return array
     */
    protected function doUpdateOptions($collection, $optionData, $valuesData, $getFromPromptOptionsData = true)
    {
        $valueAddCounter = 0;
        $valueUpdateCounter = 0;
        $addedOptionCounter = 0;
        $updatedOptionCounter = 0;

        foreach ($collection as $product) {
            /** @var Mage_Catalog_Model_Product_Option $options */
            $options = $product->getOptions();
            $foundOption = null;
            /** @var Mage_Catalog_Model_Product_Option $option */
            foreach ($options as $option) {
                if ($option->getTitle() == $optionData['title']) {
                    $foundOption = $option;
                    break;
                }
            }

            if (!$foundOption) {
                if ($this->saveNewOption($optionData, $valuesData, $product, $getFromPromptOptionsData)
                ) {
                    $addedOptionCounter++;
                    $valueAddCounter += count($valuesData);
                }
            } else {
                foreach ($valuesData as $valueData) {
                    $valueInstance = null;
                    foreach ($foundOption->getValues() as $value) {
                        if ($value['title'] == $valueData['title']) {
                            $valueInstance = $value;
                            $valueInstance->setData(
                                array_merge($value->getData(), $valueData, array(
                                    'store_id' => $product->getStoreId()
                                ))
                            );
                            break;
                        }
                    }
                    if (!$valueInstance) {
                        $valueInstance = $this->createValue($valueData);
                        $valueInstance->setOption($foundOption);

                        try {
                            $valueInstance->saveValues();
                            $updatedOptionCounter++;
                            $valueAddCounter++;
                            Mage::log('ID:' . $product->getId() . " create option value\n", null, self::ADD_LOG_NAME, true);
                        } catch (Exception $e) {
                            echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
                            Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
                        }
                    } else {
                        try {
                            $valueInstance->save();
                            $updatedOptionCounter++;
                            $valueUpdateCounter++;

                            Mage::log('ID:' . $product->getId() . " update option value\n", null, self::ADD_LOG_NAME, true);
                        } catch (Exception $e) {
                            echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
                            Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
                        }
                    }
                }
            }
        }
        return array(
            $addedOptionCounter,
            $updatedOptionCounter,
            $valueAddCounter,
            $valueUpdateCounter,
        );

    }





    /**
     * @param array $optionData
     * @param array $valuesData
     * @param Mage_Catalog_Model_Product $product
     * @param boolean $getFromPromptOptionsData
     * @return bool
     */
    private function saveNewOption($optionData, $valuesData, Mage_Catalog_Model_Product $product, $getFromPromptOptionsData)
    {
        $optionData = $this->pullNewOptionData($optionData, $getFromPromptOptionsData);
        $optionData['values'] = $valuesData;
        /** @var Mage_Catalog_Model_Product_Option $optionInstance */
        $optionInstance = Mage::getModel('catalog/product_option');
        $optionInstance->addOption($optionData);
        $optionInstance->setProduct($product);

        try {
            $resource = Mage::getSingleton('core/resource');
            $adapter = $resource->getConnection('write_read');

            $adapter->update(
                $resource->getTableName('catalog/product'),
                array(
                    'has_options' => 1,
                    'required_options' => 1
                ),
                array('entity_id = ?' => $product->getId())
            );

            $optionInstance->saveOptions();
            Mage::log('ID:' . $product->getId() . " create option\n", null, self::ADD_LOG_NAME, true);
            return true;
        } catch (Exception $e) {
            echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
            Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
            return false;
        }

    }


    /**
     * pull options data from input stream
     * @param $optionData
     * @param boolean $getFromPromptOptionsData
     * @return array
     */
    private function pullNewOptionData($optionData, $getFromPromptOptionsData)
    {
        if (empty($this->filledOptionValues) && $getFromPromptOptionsData) {

            $this->pullArgs($optionData, array(
                'is_require' => array(
                    'prompt' => "option \e[32mIs Required\e[00m",
                    'allowedValues' => array(0, 1),
                    'required' => false
                ),
                'sort_order' => array(
                    'prompt' => "option \e[32mSort Order\e[00m",
                    'required' => false
                )
            ));

        }
        $this->filledOptionValues = array_merge(array(
            'type' => 'drop_down',
            'is_require' => 0,
            'sort_order' => 0
        ), $optionData);
        return $this->filledOptionValues;

    }

    /**
     * @param $valueData
     * @return Mage_Catalog_Model_Product_Option_Value
     */
    private function createValue($valueData)
    {
        $valueData = array_merge(array(
            'sort_order' => 0,
            'price_type' => 'fixed'
        ), $valueData);
        /** @var Mage_Catalog_Model_Product_Option_Value $valueInstance */
        $valueInstance = Mage::getModel('catalog/product_option_value');
        $valueInstance->addValue($valueData);
        return $valueInstance;
    }

As you can see, these methods can update an option if it exists, or create a new one. This is also applicable for custom values.

Next, we will add a Remove option method.

/**
     * @param $args
     */
    protected function removeOptions($args)
    {
        echo "Start removing options..." . PHP_EOL;
        $this->pullArgs($args, array(
            'filter-type' => array(
                'default' => 'like',
                'allowedValues' => array('eq', 'like')
            ),
            'sku' => array(),
            'option-title' => array(),
            'option-value' => array(),
            'option-field-name' => array(
                'default' => 'title'
            )
        ));

        $collection = $this->getCollection(array($args['filter-type'] => $args['sku']));
        $counter = 0;
        foreach ($collection as $product) {
            foreach ($product->getOptions() as $option) {
                if ($option->getTitle() == $args['option-title']) {
                    foreach ($option->getValues() as $value) {
                        if ($value->getData($args['option-field-name']) == $args['option-value']) {
                            try {
                                Mage::log('ID:' . $product->getId() . "\n", null, self::REMOVE_LOG_NAME, true);
                                $value->delete();
                                $counter++;
                            } catch (Exception $e) {
                                echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
                                Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
                            }
                        }
                    }
                }
            }
        }

        echo "$counter items was updated. \n";
    }

And the final script is the following:

<?php

require_once 'abstract.php';


class Atwix_Shell_Manage_Options extends Mage_Shell_Abstract
{
    const REMOVE_LOG_NAME = 'atwix_remove_option.log';
    const ADD_LOG_NAME = 'atwix_add_option.log';
    const ERROR_LOG_NAME = 'atwix_change_option_errors.log';
    /*
     * Field mapping for csv
     */
    protected $fieldMapping = array(
        'option' => array(
            'title' => 'title',
            'is required' => 'is_require',
            'input type' => 'type',
            'sort order' => 'sort_order',
        ),
        'value' => array(
            'title' => 'title',
            'price' => 'price',
            'price type' => 'price_type',
            'sku' => 'sku',
            'sort order' => 'sort_order',
        )
    );

    private $filledOptionValues = array();

    /**
     * @inheritdoc
     */
    public function run()
    {
        $args = $this->_args;
        $method = key($args);
        if ($method == 'add') {
            $this->addOptions($args);
        } else if ($method == 'remove') {
            $this->removeOptions($args);
        }
    }

    /**
     * @param $args
     */
    protected function addOptions($args)
    {
        echo "Start adding options..." . PHP_EOL;
        $this->pullArgs($args, array(
            'filter-type' => array(
                'default' => 'like',
                'allowedValues' => array('eq', 'like')
            ),
            'sku' => array(),
            'option-title' => array(),
        ));

        $valueParams = array(
            'title' => array(
                'prompt' => "value \e[32mTitle\e[00m"
            ),
            'price' => array(
                'prompt' => "value \e[32mPrice\e[00m",
                'required' => false,
            ),
            'price_type' => array(
                'prompt' => "value \e[32mType\e[00m",
                'allowedValues' => array('fixed', 'percent')
            ),
            'sort_order' => array(
                'prompt' => "value \e[32mSort Order\e[00m",
                'required' => false,
            ),
            'sku' => array(
                'prompt' => "value \e[32mSKU\e[00m",
                'required' => false,
            )
        );
        $valueData = array();
        $this->pullArgs($valueData, $valueParams);

        $collection = $this->getCollection(array($args['filter-type'] => $args['sku']));

        list(
            $addedOptionCounter,
            $updatedOptionCounter,
            $valueAddCounter,
            $valueUpdateCounter) = $this->doUpdateOptions($collection, array('title' => $args['option-title']), array($valueData));

        echo "$updatedOptionCounter 'options' was updated. \n";
        echo "$addedOptionCounter 'options' was added. \n";
        echo "$valueUpdateCounter 'value' items was update. \n";
        echo "$valueAddCounter 'value' items was added. \n";

    }



    /**
     * @param Mage_Catalog_Model_Resource_Product_Collection $collection
     * @param array $optionData
     * @param array $valuesData
     * @param boolean $getFromPromptOptionsData
     * @return array
     */
    protected function doUpdateOptions($collection, $optionData, $valuesData, $getFromPromptOptionsData = true)
    {
        $valueAddCounter = 0;
        $valueUpdateCounter = 0;
        $addedOptionCounter = 0;
        $updatedOptionCounter = 0;

        foreach ($collection as $product) {
            /** @var Mage_Catalog_Model_Product_Option $options */
            $options = $product->getOptions();
            $foundOption = null;
            /** @var Mage_Catalog_Model_Product_Option $option */
            foreach ($options as $option) {
                if ($option->getTitle() == $optionData['title']) {
                    $foundOption = $option;
                    break;
                }
            }

            if (!$foundOption) {
                if ($this->saveNewOption($optionData, $valuesData, $product, $getFromPromptOptionsData)
                ) {
                    $addedOptionCounter++;
                    $valueAddCounter += count($valuesData);
                }
            } else {
                foreach ($valuesData as $valueData) {
                    $valueInstance = null;
                    foreach ($foundOption->getValues() as $value) {
                        if ($value['title'] == $valueData['title']) {
                            $valueInstance = $value;
                            $valueInstance->setData(
                                array_merge($value->getData(), $valueData, array(
                                    'store_id' => $product->getStoreId()
                                ))
                            );
                            break;
                        }
                    }
                    if (!$valueInstance) {
                        $valueInstance = $this->createValue($valueData);
                        $valueInstance->setOption($foundOption);

                        try {
                            $valueInstance->saveValues();
                            $updatedOptionCounter++;
                            $valueAddCounter++;
                            Mage::log('ID:' . $product->getId() . " create option value\n", null, self::ADD_LOG_NAME, true);
                        } catch (Exception $e) {
                            echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
                            Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
                        }
                    } else {
                        try {
                            $valueInstance->save();
                            $updatedOptionCounter++;
                            $valueUpdateCounter++;

                            Mage::log('ID:' . $product->getId() . " update option value\n", null, self::ADD_LOG_NAME, true);
                        } catch (Exception $e) {
                            echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
                            Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
                        }
                    }
                }
            }
        }
        return array(
            $addedOptionCounter,
            $updatedOptionCounter,
            $valueAddCounter,
            $valueUpdateCounter,
        );

    }





    /**
     * @param array $optionData
     * @param array $valuesData
     * @param Mage_Catalog_Model_Product $product
     * @param boolean $getFromPromptOptionsData
     * @return bool
     */
    private function saveNewOption($optionData, $valuesData, Mage_Catalog_Model_Product $product, $getFromPromptOptionsData)
    {
        $optionData = $this->pullNewOptionData($optionData, $getFromPromptOptionsData);
        $optionData['values'] = $valuesData;
        /** @var Mage_Catalog_Model_Product_Option $optionInstance */
        $optionInstance = Mage::getModel('catalog/product_option');
        $optionInstance->addOption($optionData);
        $optionInstance->setProduct($product);

        try {
            $resource = Mage::getSingleton('core/resource');
            $adapter = $resource->getConnection('write_read');

            $adapter->update(
                $resource->getTableName('catalog/product'),
                array(
                    'has_options' => 1,
                    'required_options' => 1
                ),
                array('entity_id = ?' => $product->getId())
            );

            $optionInstance->saveOptions();
            Mage::log('ID:' . $product->getId() . " create option\n", null, self::ADD_LOG_NAME, true);
            return true;
        } catch (Exception $e) {
            echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
            Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
            return false;
        }

    }


    /**
     * pull options data from input stream
     * @param $optionData
     * @param boolean $getFromPromptOptionsData
     * @return array
     */
    private function pullNewOptionData($optionData, $getFromPromptOptionsData)
    {
        if (empty($this->filledOptionValues) && $getFromPromptOptionsData) {

            $this->pullArgs($optionData, array(
                'is_require' => array(
                    'prompt' => "option \e[32mIs Required\e[00m",
                    'allowedValues' => array(0, 1),
                    'required' => false
                ),
                'sort_order' => array(
                    'prompt' => "option \e[32mSort Order\e[00m",
                    'required' => false
                )
            ));

        }
        $this->filledOptionValues = array_merge(array(
            'type' => 'drop_down',
            'is_require' => 0,
            'sort_order' => 0
        ), $optionData);
        return $this->filledOptionValues;

    }

    /**
     * @param $valueData
     * @return Mage_Catalog_Model_Product_Option_Value
     */
    private function createValue($valueData)
    {
        $valueData = array_merge(array(
            'sort_order' => 0,
            'price_type' => 'fixed'
        ), $valueData);
        /** @var Mage_Catalog_Model_Product_Option_Value $valueInstance */
        $valueInstance = Mage::getModel('catalog/product_option_value');
        $valueInstance->addValue($valueData);
        return $valueInstance;
    }

    /**
     * @param $args
     */
    protected function removeOptions($args)
    {
        echo "Start removing options..." . PHP_EOL;
        $this->pullArgs($args, array(
            'filter-type' => array(
                'default' => 'like',
                'allowedValues' => array('eq', 'like')
            ),
            'sku' => array(),
            'option-title' => array(),
            'option-value' => array(),
            'option-field-name' => array(
                'default' => 'title'
            )
        ));

        $collection = $this->getCollection(array($args['filter-type'] => $args['sku']));
        $counter = 0;
        foreach ($collection as $product) {
            foreach ($product->getOptions() as $option) {
                if ($option->getTitle() == $args['option-title']) {
                    foreach ($option->getValues() as $value) {
                        if ($value->getData($args['option-field-name']) == $args['option-value']) {
                            try {
                                Mage::log('ID:' . $product->getId() . "\n", null, self::REMOVE_LOG_NAME, true);
                                $value->delete();
                                $counter++;
                            } catch (Exception $e) {
                                echo "An error was occurred: " . $e->getMessage() . PHP_EOL;
                                Mage::log($e->getMessage(), null, self::ERROR_LOG_NAME, true);
                            }
                        }
                    }
                }
            }
        }

        echo "$counter items was updated. \n";
    }


    /**
     * @param $filter
     * @return Mage_Catalog_Model_Resource_Product_Collection
     */
    private function getCollection($filter)
    {
        return $collection = Mage::getModel('catalog/product')
            ->getCollection()
            ->addFieldToFilter('sku', $filter)
            ->addOptionsToResult();
    }

    private function pullArgs(&$args, $params)
    {
        foreach ($params as $paramName => $paramValue) {
            $paramValue = array_merge(array(
                'allowedValues' => array(),
                'required' => true,
                'prompt' => $paramName
            ), $paramValue);

            $prompt = sprintf('Enter %s', $paramValue['prompt']);

            if (!array_key_exists($paramName, $args)) {
                if (isset($paramValue['default'])) {
                    $args[$paramName] = $paramValue['default'];
                } else {
                    $val = $this->prompt($prompt,
                        $paramValue['allowedValues'],
                        $paramValue['required']
                    );
                    if (!empty($val)) {
                        $args[$paramName] = $val;
                    }

                }
            } else if (!empty($paramValue['allowedValues']) && !in_array($args[$paramName], $paramValue['allowedValues'])) {
                $args[$paramName] = $this->prompt($prompt, $paramValue['allowedValues'], $paramValue['required']);
            }
        }
    }

    private function prompt($prompt, $allowedValues = array(), $required = true)
    {
        $styledPrompt = sprintf("\e[36m%s \e[33m%s\e[00m:\e[00m ", $prompt, implode('|', $allowedValues));
        $val = readline($styledPrompt);
        if ($required && empty($val)) {
            return $this->prompt($prompt, $allowedValues, $required);
        } else if (!empty($allowedValues) && !in_array($val, $allowedValues)) {
            return $this->prompt($prompt, $allowedValues, $required);
        }
        return $val;
    }

}

$shell = new Atwix_Shell_Manage_Options();
$shell->run();

Now I’ll show you an example on how to work with this script.

We have a product in our Magento instance with SKU=”sku_value”, and this product doesn’t have any custom options.

2016-12-11_1238

To create a new option run the following command in your terminal:

$ php shell/manage_custom_product_options.php add
$ php shell/manage_custom_product_options.php add
Start adding options...
Enter sku : sku_value
Enter option-title : New title for new option
Enter value Title : New option value title
Enter value Price : 10
Enter value Type fixed|percent: fixed
Enter value Sort Order : 0
Enter value SKU : value-sku
Enter option Is Required 0|1: 1
Enter option Sort Order : 1
0 'options' was updated. 
1 'options' was added. 
0 'value' items was update. 
1 'value' items was added.

The result:

2016-12-11_1248

We’ve added a new option with a value. Also, we can update an existing option and add value to it.

$ php shell/manage_custom_product_options.php add
Start adding options...
Enter sku : sku_value
Enter option-title : New title for new option
Enter value Title : Second value title
Enter value Price : 90
Enter value Type fixed|percent: fixed
Enter value Sort Order : 
Enter value SKU : value-sku-2
1 'options' was updated. 
0 'options' was added. 
0 'value' items was update. 
1 'value' items was added.

As we can see the script has updated the options and added values.

2016-12-11_12491

And the last thing – let’s remove a value from the option:

$ php shell/manage_custom_product_options.php remove
Start removing options...
Enter sku : sku_value
Enter option-title : New title for new option
Enter option-value : New option value title
1 items was updated. 

The script has deleted the value with the title “New option value title” from options.

2016-12-11_1251w

We’ve used “like” filter type for product SKU, which means that you can use a filter mask like “value*” to process more products.