Wir legen manche Magento Attribut Optionen während des CSV Imports an. Dazu gibt es seit Jahren Fragen auf Stackoverflow und co wie das geht. Die Antworten haben aber alle 2 Bugs:

  1. Schreibt magento, wenn man Mage_Eav_Model_Entity_Setup::addAttributeOption benutzt, alle IDs der Option neu, weil es ein delete+insert macht. Damit sind alle Produkte, die das Attribut nutzen, ihren Wert los: Die optionIDs werden als varchar im Attribut gespeichert, die Datenbank kriegt von der Neu-Nummerierung nix von mit.
  2. Der Import Prozess cached die Attributdaten, dieser Cache muss auch aktualisiert werden. Sonst muss man immer 2 mal importieren.

Die Lösung:

Für 1. braucht es eine Alternative zu addAttributeOption. Der Methodennahme ist auch völlig irreführend. Es ist eigentlich update/add (aber nur für Admin Store)/delete in einem. Egal. Hier unsere Alternative:

class Bobbie_Export_Model_Entity_Setup extends Mage_Eav_Model_Entity_Setup {
	
	/**
	 * Append (ie add) Attribure Option to attribute, WITHOUT deleting all options.
	 * This delete would make all products that use the attributes options loose those
	 * as the attribute option value is stored as varchar
	 * This doesn't have the fancy delete/update mechanism the original addAttributeOption code has
	 * And it doesn't use the clumsy array with magic values approach
	 * Also, you can update the Product Entity caches with the new value
	 * @param int $attributeId ID of attribute 
	 * @param array $values mapping of storeId->AttributeLabel
	 * @param Bobbie_Export_Model_Import_Entity_Product $entityAdapter used to update cache on the fly
	 */
	public function appendAttributeOption($attributeId, $values,$entityAdapter) {
		$optionTable        = $this->getTable('eav/attribute_option');
		$optionValueTable   = $this->getTable('eav/attribute_option_value');
		
		$optionTableData = array(
			'attribute_id'  => $attributeId,
			'sort_order'    => 0,
		);
		$this->_conn->insert($optionTable, $optionTableData);
		$intOptionId = $this->_conn->lastInsertId($optionTable);
		
		foreach ($values as $storeId => $label) {
			$data = array(
				'option_id' => $intOptionId,
				'store_id'  => $storeId,
				'value'     => $label,
			);
			$this->_conn->insert($optionValueTable, $data);
		}
		
		if($entityAdapter) {
			$entityAdapter->addAttributeOptionToTypeModelCache($attributeId,$values[Mage::app()->getStore()->getStoreId()],$intOptionId);
		}
		
	}
}

Das ist schonmal die halbe Miete. Unten Sieht man auch schon den Cache Aufruf. Das geht so:

class Bobbie_Export_Model_Import_Entity_Product extends Mage_ImportExport_Model_Import_Entity_Product {
	/* When attribute options are added on the fly during import, this is needed to store them in cache
	 *
	 */
	public function addAttributeOptionToTypeModelCache($attributeId, $optionLabel, $optionID) {
		foreach($this->_productTypeModels as $type => $typeModel) {
			if(method_exists($typeModel, 'addAttributeOptiontoCache')) {
				$typeModel->addAttributeOptiontoCache($attributeId, $optionLabel, $optionID);
			}
		}
	}
}

class Bobbie_Export_Model_Import_Entity_Product_Type_Simple extends Mage_ImportExport_Model_Import_Entity_Product_Type_Simple {

	/* When attribute options are added on the fly during import, this is needed to store them in cache
	 * 
	 */
	public function addAttributeOptiontoCache($attributeId, $optionLabel, $optionID) {
		foreach($this->_attributes as $attributeSet => &$attributes) {
			foreach($attributes as &$attribute) {
				if($attribute['id'] == $attributeId) {
					$attribute['options'][$optionLabel] = $optionID;
				}
			}
		}
	}
}

Damit das auch aufgerufen wird, braucht es noch ein paar Modifikationen des Stock Codes und ein Helper

abstract class Mage_ImportExport_Model_Import_Entity_Abstract
{

[...]

/**
     * Check one attribute. Can be overridden in child.
     *
     * @param string $attrCode Attribute code
     * @param array $attrParams Attribute params
     * @param array $rowData Row data
     * @param int $rowNum
     * @return boolean
     */
    public function isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum)
    { 
	$valid = false;
	
        switch ($attrParams['type']) {
            case 'varchar':
                $val   = Mage::helper('core/string')->cleanString($rowData[$attrCode]);
                $valid = Mage::helper('core/string')->strlen($val) < self::DB_MAX_VARCHAR_LENGTH;
                break;
            case 'decimal':
                $val   = trim($rowData[$attrCode]);
                $valid = (float)$val == $val;
                break;
            case 'select':
            case 'multiselect':
				$attributeOption = strtolower($rowData[$attrCode]);
				if (array_key_exists($attributeOption,$attrParams['options'])) {
					$valid = true;
				} else {
					//Modified stock code: allow creating super attributes on the fly
					if($attrCode == 'configurable_color' || $attrCode == 'configurable_size' ){ 
						$valid = null !== Mage::helper('export')->saveAttributeOption($attrParams['id'],$attributeOption,$this);  
					}
				}
                break;
            case 'int':
                $val   = trim($rowData[$attrCode]);
                $valid = (int)$val == $val;
                break;
            case 'datetime':
                $val   = trim($rowData[$attrCode]);
                $valid = strtotime($val) !== false
                    || preg_match('/^\d{2}.\d{2}.\d{2,4}(?:\s+\d{1,2}.\d{1,2}(?:.\d{1,2})?)?$/', $val);
                break;
            case 'text':
                $val   = Mage::helper('core/string')->cleanString($rowData[$attrCode]);
                $valid = Mage::helper('core/string')->strlen($val) < self::DB_MAX_TEXT_LENGTH;
                break;
            default:
                $valid = true;
                break;
        }

        if (!$valid) { 
        	$this->addRowError("Invalid value " . $rowData[$attrCode] . " for " . $attrCode . ", type " . $attrParams['type'] . " possible is " .  implode(array_keys($attrParams['options']),","), $rowNum, $attrCode);
        } elseif (!empty($attrParams['is_unique'])) {
            if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]])) {
                $this->addRowError(Mage::helper('importexport')->__("Duplicate Unique Attribute for '%s'"), $rowNum, $attrCode);
                return false;
            }
            $this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] = true;
        }
        return (bool) $valid;
    }

[...]

}

class Bobbie_Export_Helper_Data extends Inchoo_PHP7_Helper_Data {

/**
     * This function add new attribute option value for configurable product
     * Due to caching in calling classes, this code will re-ceck if the attribute exists before creating it.
     * @param $attributeCode string
     * @param $superAttributeOption string
     */
    public function saveAttributeOption($attrId,$superAttributeOption,$entityAdapter){
    	$attribute = Mage::getModel('eav/config')->getAttribute('catalog_product', $attrId);
    	$existingOption = $this->getAttributeOption($attribute,$superAttributeOption);
    	if (! is_null($existingOption)) {
    		return $existingOption;
    	}
    	//attribute option doesn't exist, create it.
    	$values[0] = $superAttributeOption;
    	$values[1] = $superAttributeOption;
    	$setup = new Bobbie_Export_Model_Entity_Setup('core_setup');
    	$setup->appendAttributeOption($attrId,$values,$entityAdapter);
    	
    	//reload attribute, and re-set options, in order to flush the option cache
    	$attribute = Mage::getModel('eav/config')->getAttribute('catalog_product', $attrId);
    	$source = Mage::getModel($attribute->getSourceModel());
    	if (!$source) {
    		throw Mage::exception('Mage_Eav',
    				Mage::helper('eav')->__('Source model "%s" not found for attribute "%s"',$this->getSourceModel(), $this->getAttributeCode())
    				);
    	}
    	$source = $source->setAttribute($attribute);
    	$attribute->setSource($source);
    	return $this->getAttributeOption($attribute,$superAttributeOption);
    }
    
    protected function getAttributeOption($attribute,$attributeOption ) {
    	foreach ( $attribute->getSource()->getAllOptions(true, true) as $option){
    		if($attributeOption == $option['label']){
    			return $option['value'];
    		}
    	}
    	return null;
    } 
    
    /**
     * Get option value for config
     *
     * @param array $attr
     * @param array $productData
     * @param string $configurableAttribute
     * @return string $optionValueFoConfig
     */
    public function getOptionValueForConfig($attr, $attributeOptionValueId, $configurableAttribute) {
    	$optionValueFoConfig = '';
    	if ($attr->usesSource ()) {
    		$optionValueFoConfig = $attr->getSource ()->getOptionId ( $attributeOptionValueId );
    		if(!$optionValueFoConfig && ($attr['attribute_code'] == 'configurable_color' || $attr['attribute_code'] == 'configurable_size')){
    			$optionValueFoConfig = $this->saveAttributeOption($attr->getAttributeId(),$attributeOptionValueId);
    		}
    	}
    	return $optionValueFoConfig;
    }

}

class Bobbie_Export_Model_Import_Entity_Product_Type_Configurable extends Mage_ImportExport_Model_Import_Entity_Product_Type_Configurable {

	 /**
	 * Validate particular attributes columns.
	 *
	 * @param array $rowData        	
	 * @param int $rowNum        	
	 * @return bool
	 */
	protected function _isParticularAttributesValid(array $rowData, $rowNum) {
		if (! empty ( $rowData ['_super_attribute_code'] )) {
			$superAttrCode = $rowData ['_super_attribute_code'];
			
			if (! $this->_isAttributeSuper ( $superAttrCode )) { // check attribute superity
				$this->_entityModel->addRowError ( self::ERROR_ATTRIBUTE_CODE_IS_NOT_SUPER, $rowNum );
				return false;
			} elseif (isset ( $rowData ['_super_attribute_option'] ) && strlen ( $rowData ['_super_attribute_option'] )) {
				$optionKey = strtolower ( $rowData ['_super_attribute_option'] );
				if (! isset ( $this->_superAttributes [$superAttrCode] ['options'] [$optionKey] )) {
					if ($superAttrCode == 'configurable_color' || $superAttrCode == 'configurable_size') {
						$productAttributeOption = Mage::getModel ( 'catalog/product' );
						$attr = $productAttributeOption->getResource ()->getAttribute ( $superAttrCode );
						$this->_superAttributes [$superAttrCode] ['options'] [$optionKey] = Mage::helper ( 'export' )->getOptionValueForConfig ( $attr, $optionKey, $superAttrCode, $this );
					} else {
						$this->_entityModel->addRowError ( self::ERROR_INVALID_OPTION_VALUE, $rowNum );
						return false;
					}
				}
				// check price value
				if (! empty ( $rowData ['_super_attribute_price_corr'] ) && ! $this->_isPriceCorr ( $rowData ['_super_attribute_price_corr'] )) {
					$this->_entityModel->addRowError ( self::ERROR_INVALID_PRICE_CORRECTION, $rowNum );
					return false;
				}
			}
		}
		return true;
	}

Wir haben hier die Attribute Codes auf configurable_size und configurable_color hardcoded. Das muss natürlich nicht. Es gibt 2 Stellen wo das ganze aufgerufen werden kann, je nachdem ob zuerst das configurable oder das simple product importiert wird: Wir nutzen das feature für super attributes. Das ganze muss natürlich noch in entsprechende Module gepackt werden. Das überlasse ich mal dem geneigten Leser, ist ja so schon komplex genug.