03 328 8646

Blog

Migrating Shopify to Drupal Commerce

We're doing our first Shopify to Drupal Commerce migration. With the need to differentiate between retail vs wholesale customers (pricing) and within wholesale (differing ranges) Shopify just isn't going to cut it for this client anymore. Shopify was perfect for the original spec - simple, quick to set up, cost effective to maintain and a range of great off-the-shelf templates to tweak. In fact, it was so perfect that it made it very clear just how much a strong web presence can do for this company - direct sales, great presentation of product to wholesalers, clearance of retiring lines, better customer communication and much more. Now they'd like more.

It's not the right move for every customer, but sometimes the power and flexibility of Drupal can meet the needs of a business better than Shopify. This is one of those cases - Shopify doesn't offer the right interfaces to manage detailed customer segmentation, it's a story of making the simple things easy and the hard things impossible.

Drupal, however, does a great job of user permissions and will let us use a taxonomy structure of collections, retail vs wholesale, anonymous vs authenticated and more to segment the customer base any which way we choose and to offer the right products to the right customers at the right price. Yay, go Drupal! And it will let us build an interface that makes it almost as easy to manage as using Shopify.

Having set up a structure to let us manage access the next job was to shift over the data from Shopify. Thankfully Shopify are very big about letting you have your data when you want it and their data export is pretty easy to work with - this is part of the reason we don't hesitate to recommend Shopify. If you need to shift to another platform then Shopify won't behave like a petulant 2 year old with your company data.

Rather than using the Migrate or Feeds modules we wrote an import by hand with entity wrappers, it's not pretty code, and it's not very flexible, but it doesn't need to be - it just needs to be faster & more accurate than re-entering the product range would be.

If you are looking at doing the same Shopify to Drupal Commerce shift then it might save you a bit of scratching around. If you improve it please send us a copy

function SNAPPY_FUNCTION_NAME_OR_HOOK() {

  $remote_csv = PATH_TO_FILE . '/products.csv';

  $local_csv = PATH_TO_OTHER_FILE . '/shopify_export.csv';

  $csv = file_get_contents($remote_csv);
  try {
    file_put_contents($local_csv, $csv);
  } catch (Exception $erk) {
     watchdog('HOOK_update', 'Unable to write local csv '.$erk, NULL, WATCHDOG_ERROR);
  }
  $csv_key = array(); //stores the header titles in an array so we can pull
                      //data according to title and not just position

  $sku_title = 'Variant SKU';
  $price_title = 'Variant Price';
  $full_price_title = 'Variant Compare At Price';
  $stock_title = 'Variant Inventory Qty';
  $body_title = 'Body (HTML)';
  $title_title = 'Title'; //this only changes when the variant changes
  $meta_desc_title = 'SEO Description';
  $variant_img_title = 'Image Src';
  $path_title = 'Handle';
  $tags_title = 'Tags';
  $type_title = 'Type';
  $published_title = 'Published';

  $row = 1;

  $handle = fopen($local_csv,'r');


  if ($handle) {
    $prod_identifier = array();//'SKU', 'handle' // just build up a list of them
    //all in case of repeats further thru the file
    $previous_sku = '';
    while (($data = fgetcsv($handle, 0, ",", '"')) !== FALSE) {
      //Look up variant based on SKU - if it exists then add the image to the
      //image field . If it doesn't then go ahead and create it.
      //SKU is empty on duplicates
      //Look this up through an entity wrapper
      //then check if this one already exists
      if ($row == 1) {
        foreach($data as $key => $d) {
          $csv_key[$d] = $key;
        }
        $row++;//only needs incrementing once
      } else {
        // We need to check the next line down here,
	// if it matches the current sku then add the image

        $sku = trim($data[$csv_key[$sku_title]]);
        if (strlen(trim($data[$csv_key[$full_price_title]]))) {
          $price = trim($data[$csv_key[$full_price_title]]) * 100;
        } else {
          $price = trim($data[$csv_key[$price_title]]) * 100;
        }
        
        $stock = 100000;//Client wants to manage stock by hand 
	    //- they manufacture but will occasionally want to list items as out of stock 
        $body = trim($data[$csv_key[$body_title]]);
        $title = trim($data[$csv_key[$title_title]]);
        $meta_description = trim($data[$csv_key[$meta_desc_title]]);
        $img = trim($data[$csv_key[$variant_img_title]]);
        $path = trim($data[$csv_key[$path_title]]);
        $collection = trim($data[$csv_key[$tags_title]]);
        $type = trim($data[$csv_key[$type_title]]);
        $status = (trim($data[$csv_key[$published_title]]) == 'true') ? 1 : 0;


        if (strlen($sku) < 1) {
          //This item is an additional image for the previous entry
          //Get the previous SKU and just add an image to it
          $sku = $previous_sku;
        }

        if ($entity_id = HOOK_update_product_exists($sku)) {
          if ($sku == $previous_sku) {
            HOOK_add_image($entity_id, $img);
          } else {
	    //We can ignore this path!
            //You may not want to...
          }
        } else {
          pk_create_new_product($sku, $price, $stock, $title, $body, $img, $status, $path, $type, $collection);
        }

        $previous_sku = $sku;
      }
    }
    watchdog('HOOK_update', 'POS Import complete', NULL, WATCHDOG_INFO);
  } else {
    watchdog('HOOK_update', 'Unable to open remote_csv', NULL, WATCHDOG_ERROR);
  }
}

function HOOK_add_image($entity_id, $img) {
  $e =  commerce_product_load($entity_id);
  $p_wrapper = entity_metadata_wrapper('commerce_product', $e);
  $p_wrapper->field_images[] = (array)get_external_image($img);
  $p_wrapper->save();
}

function HOOK_update_product_exists($sku) {
  //Look it up, if it exists then return its id
  $ret = FALSE;

  $query = new EntityFieldQuery();
  $results = $query->entityCondition('entity_type', 'commerce_product')
                  ->propertyCondition('sku', $sku)
                  ->execute();

  if(isset($results['commerce_product'])) {
    $entities = $results['commerce_product'];

    foreach ($entities as $key => $e) {
      $ret = $key;
    }

  }

  return $ret;
}


function pk_create_new_product($sku, $price, $stock, $title, $body, $img, $status, $path, $type, $collection) {
  /*
   * Create a product entity and then a node that relates to it
   *
   *  if the product exists and we got here by mistake log it and just move on
   *  Tax inclusive price - get the database default and use it
   */


  $type_ids = array(
    'Type One'  => 5,
    'Type Two'   => 4,
    'Type Three'  => 3,
  );

  $collection_ids = array(
    'Col 1'      => 11,
    'Col 2'    => 12,
    'Col 3'      => 7,
  );

  if ($status) {
    //We're not adding discontinued items
    try {
      //PRODUCT - Create product
      $p_wrapper = entity_metadata_wrapper('commerce_product', commerce_product_new('product'));
      $p_wrapper->title = check_plain($title);//$title;
      $p_wrapper->sku = check_plain($sku);//$sku;
      $p_wrapper->commerce_price->amount = $price;
      $p_wrapper->commerce_price->currency_code = commerce_default_currency();//variable_get('commerce_default_currency', 'NZD');
      if ($default_tax = variable_get('commerce_default_tax_rate_default')) {
        $p_wrapper->commerce_price->data = array('include_tax' => $default_tax);
      }
      $p_wrapper->commerce_stock = check_plain($stock);
      $p_wrapper->field_images[] = (array)get_external_image($img);
      $p_wrapper->save();

      $p_id = $p_wrapper->getIdentifier();

      // NODE - create referring node
      $n_wrapper = entity_metadata_wrapper('node', commerce_product_new('product_display'));
      $n_wrapper->title = check_plain($title);
      $n_wrapper->body->set(
          array(
            'value' => $body,
            'text_format' => 'Full HTML'
          )
        );

      $n_wrapper->status = $status;
      if(isset($type_ids[$type])) {
        $n_wrapper->field_product_type = $type_ids[$type];
      }
      if(isset($collection_ids[$collection])) {
        $n_wrapper->field_product_type = $type_ids[$type];
      }

      $n_wrapper->field_product[] = $p_id;
      $n_wrapper->save();

      $mp = array(
              'source' => '/node/' . $nid,
              'alias ' => 'products/' . $path,
            );
      path_save($mp);
    }
    catch (EntityMetadataWrapperException $exc) {
        watchdog(
          'Shopify',
          'See '  . __FUNCTION__ . '() ' .  $exc->getTraceAsString(),
           NULL, WATCHDOG_ERROR
        );
    }
  }
}


function get_external_image($url) {
/*
 * @return an array that can be saved directly to the image field
 */
  $data = file_get_contents($url);
  //Shopify adds a query string to image names, let's get rid of it
  $out = preg_replace('/\?.*/', '', $url);
  $destination = "public://".basename($out);
  $file = file_save_data($data, $destination);
  return $file;
}