Magento Attribut Optionen on-thy-fly anlegen im Import

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.