Magento Attribute Migration Generator

Like this article? Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

For the tech savvy single store owner, managing product attributes isn’t that big a deal. They go to the admin console, use the UI to create an attribute, and add it to an attribute set. Over the course of the morning, day, or week they go back to the attribute and tweak it until everything works right.

For the agency developer things are a little trickier. Unless you’re with a shop that’s a thinly veiled recruiting/resale firm, you’re going to be working within a team based development process. That always includes working first on a development/staging server, and then moving your work over to a live server.

If you’re a developer creating a module/extension for redistribution there’s an even greater challenge. In an agency environment you can work around a broken deployment process by fiddling with a site after you’ve pushed all your code live. With an extension, after your user installs things it’s up to them to smooth out any wrinkles. Too many wrinkles, and they’re going to find another solution.

Magento Migrations

This is where Magento’s setup resource scripts come in handy. Similar to migrations in other frameworks, a setup resource allows you to write scripts that adjust Magento’s database/attribute store automatically, without any user intervention.

At least, that’s the theory, and for general entity setup the theory plays out in practice. However, with EAV attributes it’s another story. As you’re developing a new feature in Magento (or teaching yourself the attribute system), you often end up constantly adjusting your attribute as your understanding of the problem changes.

If you start with a migration script, this means each time you make an adjustment you need to subtly change your migration script. Conversely, if you start by configuring an attribute using the UI, when it comes time to launch you need to slog through the UI and database to extract the information needed write your migration script.

Either case introduces friction to the development process, friction that prevents you from getting into flow. Friction that makes many developers dread using attributes in their projects.

Tedious Busy Work

Of course, when I say most developers I mean most developers named Alan Storm. I find writing product attribute migration scripts tedious, error prone, and frustrating. In large part it’s because the format of the attribute array

$this->addAttribute('catalog_product','ncr_gridproduct_swatchcolor',array(
    //...attributes omitted...
    'user_defined' => '1',
    'default' => '',
    'unique' => '0',
    'note' => NULL,
    'input_renderer' => NULL,
    'global' => '1',    
    //...attributes omitted...
));

isn’t officially documented anywhere, and is different from the data properties of a the “proper” attribute objects used by the Magento UI. These are small, trivial differences (global vs. is_global), but they add up. It’s particularly tedious when testing your migration scripts means a full database restore to the point prior to the migration running or having the confidence to selectively remove the attribute information from the various tables its definition is spread across. There’s also the problem of the various EAV attribute types each having their own setup resource classes. Using the wrong class won’t fail, but a number of your attribute properties will be discarded and you’ll end up creating an attribute that isn’t quite the type Magento wants.

None of this is hard, but this sort of friction is mind numbing and where a developer’s soul goes to die.

Tedious Busy Work is for Computers

Fortunately, in the mid-20th century human beings created something to deal with tedious and error prone busy work. It’s called a computer. The PHP script below is the start to a solution

#/usr/bin/env php
<?php

function bootstrap()
{
    /**
     * Error reporting
     */
    error_reporting(E_ALL | E_STRICT);

    $mageFilename = 'app/Mage.php';    
    $maintenanceFile = 'maintenance.flag';    
    require_once $mageFilename;

    #Varien_Profiler::enable();

    Mage::setIsDeveloperMode(true);

    ini_set('display_errors', 1);

    umask(0);

    /* Store or website code */
    $mageRunCode = isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : '';

    /* Run store or run website */
    $mageRunType = isset($_SERVER['MAGE_RUN_TYPE']) ? $_SERVER['MAGE_RUN_TYPE'] : 'store';

    Mage::init($mageRunCode, $mageRunType);

    return $mageFilename;
}


function get_option_array_for_attribute($attribute)
{
    $read   = Mage::getModel('core/resource')->getConnection('core_read');
    $select = $read->select()
    ->from('eav_attribute_option')
    ->join('eav_attribute_option_value','eav_attribute_option.option_id=eav_attribute_option_value.option_id')
    ->where('attribute_id=?',$attribute->getId())
    ->where('store_id=0')
    ->order('eav_attribute_option_value.option_id');

    $query = $select->query();

    $values = array();
    foreach($query->fetchAll() as $rows)
    {
        $values[] = $rows['value'];
    }

    //$values = array('#f00000','abc123');
    return array('values'=>$values);
}

function get_key_legend()
{
    return array(

        //catalog
        'frontend_input_renderer'       => 'input_renderer',
        'is_global'                     => 'global',
        'is_visible'                    => 'visible',
        'is_searchable'                 => 'searchable',
        'is_filterable'                 => 'filterable',
        'is_comparable'                 => 'comparable',
        'is_visible_on_front'           => 'visible_on_front',
        'is_wysiwyg_enabled'            => 'wysiwyg_enabled',
        'is_visible_in_advanced_search' => 'visible_in_advanced_search',
        'is_filterable_in_search'       => 'filterable_in_search',
        'is_used_for_promo_rules'       => 'used_for_promo_rules',


        'backend_model'                 => 'backend',
        'backend_type'                  => 'type',
        'backend_table'                 => 'table',
        'frontend_model'                => 'frontend',
        'frontend_input'                => 'input',
        'frontend_label'                => 'label',
        'source_model'                  => 'source',
        'is_required'                   => 'required',
        'is_user_defined'               => 'user_defined',
        'default_value'                 => 'default',
        'is_unique'                     => 'unique',
        'is_global'                     => 'global',

        );  
}

function get_migration_script_for_attribute($code)
{
    //load the existing attribute model
    $m = Mage::getModel('catalog/resource_eav_attribute')
    ->loadByCode('catalog_product',$code);

    //get a map of "real attribute properties to properties used in setup resource array
    $real_to_setup_key_legend = get_key_legend();

    //swap keys from above
    $data = $m->getData();
    $keys_legend = array_keys($real_to_setup_key_legend);
    $new_data    = array();
    foreach($data as $key=>$value)
    {
        if(in_array($key,$keys_legend)) 
        {
            $key = $real_to_setup_key_legend[$key];
        }
        $new_data[$key] = $value;
    }

    //unset items from model that we don't need and would be discarded by 
    //resource script anyways
    $attribute_code = $new_data['attribute_code'];
    unset($new_data['attribute_id']);
    unset($new_data['attribute_code']);
    unset($new_data['entity_type_id']);

    //chuck a few warnings out there for things that were a little murky
    if($new_data['attribute_model'])
    {
        echo "//WARNING, value detected in attribute_model.  We've never seen a value there before and this script doesn't handle it.  Caution, etc. " . "\n";
    }

    if($new_data['is_used_for_price_rules'])
    {
        echo "//WARNING, non false value detected in is_used_for_price_rules.  The setup resource migration scripts may not support this (per 1.7.0.1)" . "\n";
    }


    //load values for attributes (if any exist)
    $new_data['option'] = get_option_array_for_attribute($m);

    //get text for script
    $array = var_export($new_data, true);

    $script = "<?php
if(! (\$this instanceof Mage_Catalog_Model_Resource_Setup) )
{
    throw new Exception(\"Resource Class needs to inherit from \" .
    \"Mage_Catalog_Model_Resource_Setup for this to work\");
}

\$attr = $array;
\$this->addAttribute('catalog_product','$attribute_code',\$attr);

";
    return $script;
}

function usage()
{
    echo "USAGE: magento-create-setup.php attribute_code" . "\n";
}

function main($argv)
{
    $script = array_shift($argv);
    $code   = array_shift($argv);
    if(!$code)
    {
        usage();
        exit;
    }
    $script = get_migration_script_for_attribute($code);        
    echo $script;
}

bootstrap();
main($argv);

If you run this script from the root folder of your Magento system and pass it a product attribute code,

$ php magento-create-setup.php gender

it’ll spit back a script suitable to use as a Mage_Catalog_Model_Resource_Setup setup script.

if(! ($this instanceof Mage_Catalog_Model_Resource_Setup) )
{
    throw new Exception("Resource Class needs to inherit from " .
    "Mage_Catalog_Model_Resource_Setup for this to work");
}

$attr = array (
  'attribute_model' => NULL,
  'backend' => NULL,
  'type' => 'int',
  'table' => NULL,
  'frontend' => NULL,
  'input' => 'select',
  'label' => 'Gender',
  'frontend_class' => '',
  'source' => 'eav/entity_attribute_source_table',
  'required' => '1',
  'user_defined' => '1',
  'default' => NULL,
  'unique' => '0',
  'note' => '',
  'input_renderer' => NULL,
  'global' => '1',
  'visible' => '1',
  'searchable' => '1',
  'filterable' => '0',
  'comparable' => '0',
  'visible_on_front' => '0',
  'is_html_allowed_on_front' => '0',
  'is_used_for_price_rules' => '1',
  'filterable_in_search' => '0',
  'used_in_product_listing' => '0',
  'used_for_sort_by' => '0',
  'is_configurable' => '1',
  'apply_to' => 'simple,grouped,configurable',
  'visible_in_advanced_search' => '1',
  'position' => '1',
  'wysiwyg_enabled' => '0',
  'used_for_promo_rules' => '1',
  'option' => 
  array (
    'values' => 
    array (
      0 => 'Womens',
      1 => 'Mens',
      2 => 'Boys',
      3 => 'Girls',
    ),
  ),
);
$this->addAttribute('catalog_product','gender',$attr);

As I mentioned, this script is a start. Right now it only works with catalog_product entities, has no option for adding the attribute to a group, only uses the values key for option values, (which only inserts (doesn’t update) new values), and only does so for the admin/default store view. Also, the code itself looks like it was banged out on a Saturday night after a trip to the pub. Myriad areas for improvement.

This is where you come in. I’ve dropped the script on Pulse Storm’s public github account. Fork it, fix it, hack it to pieces. Beyond helping out the Magento development community, this is a great way to familiarize yourself with the structure of Magento’s attributes.

Originally published July 30, 2012
blog comments powered by Disqus