How to import products into Drupal Ubercart with a Script

So, you've setup you brand new Drupal install with Ubercart, and now you want to import your products into the store's database. However, much to your disappointment, you can't find anything that tells you how to do that. As always, the lack of examples makes such a task seem impossible, but in reality it's very simple.

I spent about 2 weeks doing research and trying out code with trial and error, trying to figure this out. I was successful, and I will save you the trouble. Welcome to the much needed tutorial on How to import products into Drupal Ubercart with a Script. With this tutorial, you will import products and categories from an outside data source, and keep them synchronized.

Requirements

First Things first, make sure you meet the minimum prerequisites:

  • Drupal 5.x with Ubercart 1.x Module installed
  • PHP 5.x
  • CCK
  • CCK Node Reference
  • CCK Image Field
  • CCK Text Field
  • CCK Number Field (optional)
  • Image Cache
  • Image Field

Design and Structure

For this tutorial, we will use an example book store, and 4 different data files. I have abstracted the data file structures as 4 different PHP data structures (classes/objects). This is meant to keep the focus on the Drupal end of things and not the parsing of the data file.

At minimum each book needs to have the following key pieces of information.

  • Author
  • Title
  • Description
  • Cover Image
  • Price
  • ISBN #
  • Product SKU

We also need to provide categories to organize the store catalog. Ubercart and Drupal handle this ingeniously by utilizing the Taxonomy feature. So, we will create a vocabulary called "Subject" and import the categories from our data files as terms.


Initial Setup

Create a new product class called "Book". Go to "Administer - Store administration - Products" and click "Manage Product Classes". Enter "book" as the "Class ID" and "Book" as the "Class Name". You may optionally enter a description. Click "Submit" to save the new product class. You should the have something similar to this:

Class ID Name Description
book Book Book

Next, we will create a vocabulary to organize your book categories/subjects. You could skip this next step since Ubercart already provides an appropriate default called "Catalog". I however decided to create my own. Go to "Administer - Content management" and click "Add Vocabulary".

Enter the vocabulary name "Subject"and an optional description. Make sure you select "Book" under Type. If you decided to use Ubercart's built in vocabulary, be sure to edit it and select "Book" under type. Disable "Hierarchy" and make this vocabulary "Required". Leave "Related terms", "Free tagging", and "Multiple select" unchecked. Click "Submit" to save this vocabulary.

You should have something similar to the following:

Name Type Operations
Catalog Product edit vocabulary list terms add terms
Subject Book edit vocabulary list terms add terms

In our store, the "Author" field will be a node reference because we want to offer the costumer the ability to view some information on the author. So lets create an "Author" content type, go to "Administer - Content management" and click "Add Content Type".

  • Name: Author
  • Type: author
  • For now, lets omit the Body field. So leave that blank
  • Description: Book author node
  • Title field label: Display Name

Leave "Body field label" empty and "minimum number of words" set to 0. Type in anything you'd like in "Explanation or submission guidelines". "Default options" should only have "Published" enabled, and comments can be either enabled or disabled. Click "Submit" to create the node type.

Go back and edit the newly created "author" node type, and click "Add field". Create the following additional fields:

    • Name: first_name
    • Field type: Text
    • Label: First Name
    • Rows: 1
    • Required: unchecked
    • Multiple values: unchecked
    • Text processing: Plain Text
    • Maximum length: blank
    • Allowed values list: blank
    • Name: last_name
    • Field type: Text
    • Label: Last Name
    • Rows: 1
    • Required: unchecked
    • Multiple values: unchecked
    • Text processing: Plain Text
    • Maximum length: blank
    • Allowed values list: blank
    • Name: author_id
    • Field type: Text
    • Label: Author
    • Rows: 1
    • Required: checked
    • Multiple values: unchecked
    • Text processing: Plain Text
    • Maximum length: blank
    • Allowed values list: blank

Now lets add additional fields to our "Book" product class:

    • Name: field_authors
    • Field type: Node Reference - Select List
    • Label: Authors
    • Required: unchecked
    • Multiple values: checked
    • Content types that can be referenced: Author
    • Name: field_book_isbn
    • Field type: Text
    • Label: ISBN
    • Rows: 1
    • Required: unchecked
    • Multiple values: unchecked
    • Text processing: Plain Text
    • Maximum length: blank
    • Allowed values list: blank

Importing Categories

If you remember, we will create these as terms for the "Subject" vocabulary. My data file has an unique ID assigned to each category. Although not required, I will use this unique ID to identify my categories instead of using the name. If your categories do not have a unique ID, make sure their names are unique.

The following assumes that your import script is inside a folder named "import" on the same level as our Drupal/Ubercart Installation which is inside "cart", and a "data" directory that contains the data files with a list of products to import. Check to see that your "Subject" vocabulary ID is correct, in my setup it was 2. You can see it by looking at the link it points to when you click on "edit vocabulary" in the categories section in the site administration area.

<?php//Execute without actually writing to the DB$TEST = false;//Change to the Drupal Directorychdir("../cart");//Load and init the Drupal Systemrequire_once 'includes/bootstrap.inc';drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);//Vocabulary ID is 2 for Subject$vocabID = 2;//Open the Data File$handle = fopen("../data/subjects.txt", "r");?>

To help keep my code simple to understand, I've created some convenience functions that will be utilized later on. It allows me to get the database ID from the term/category or in this case book subject that I am importing. This prevents duplicates and allows the synchronization to work. The variable, "ExtID" refers to the ID that is assigned to the subjects in the datafile.

The following two functions allow me to get the internal Drupal database ID of a term using either it's name or unique ID (ExtID) from the data file.

<?phpfunction getTermIdFromTitle($termTitle, $vocabID){    $query = "SELECT * FROM term_data WHERE vid = $vocabID AND name = "$termTitle" LIMIT 1";    $queryResult =  db_query($query);    $tid = false;    echo "Looking up term ID for "$termTitle"" . "<br />\n";        if ($term = db_fetch_array($queryResult)) {        $tid = $term['tid'];        echo "Getting term ID for "$termTitle", Found: tid=" . $tid . "<br />\n";        }    return $tid;}function getTermIdFromExtID($extID, $vocabID){    $query = "SELECT * FROM term_data WHERE vid = $vocabID AND description = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $tid = false;    echo "Looking up term ID for "$extID"" . "<br />\n";        if ($term = db_fetch_array($queryResult)) {        $tid = $term['tid'];        echo "Getting term ID for "$extID", Found: tid=" . $tid . "<br />\n";        }    return $tid;}?>

In this example, the categories are a standard CSV file. So we will iterate through each line until the end of the file, make sure the current line is not empty, and process the data.

<?phpwhile (($data = fgetcsv($handle, 0)) !== FALSE) {    if (!empty($data[0])) {?>

Create the data structure to hold and organize our term data according to Drupal's requirements. In this example, I am assigning the ExtID to the description field. This is OK because that field will not be visible to the costumer. If you do not have an ID just leave the description as NULL. Obviously we assign the "name" as the subject's name as we want it to appear to the costumer.

<?php        //Construct Term Data        $values = array();        $values['name'] = trim($data[1]);        $values['description'] = trim($data[0]);        $values['weight'] = 0;        $values['synonyms'] = '';        $values['vid'] = $vocabID; //Vocabulary ID set previously        $values['relations'] = 0;?>

This next piece of code serves two purposes, it makes sure that no duplicate categories are created, and it allows the synchronization to work. We utilize the convenience function that we created earlier to retrieve the internal Drupal database ID of the the vocabulary term (tid). In this example I am getting it using the category ID from the data file, but you may use the name instead by selecting the appropriate function.

If the term is found, we will just update the term's data using the term's ID (tid) as a reference. If the term is not found a new term is created by setting the term's ID (tid) equal to 0.

<?php        //First see if this already exists in the Database        if (($tid = getTermIdFromExtID($values['description'], $vocabID)) !== false) {            //It Exists, Update it by using the existing term ID                   $values['tid'] = $tid;            echo "Updating: " . $values['description'] . "<br />\n";        }        else {            //Does not Exist, Make new term by using tid = 0                   $values['tid'] = 0;            echo "Creating: " . $values['description'] . "<br />\n";        }?>

The code saves the term to the database. Either updating it (tid != 0) or creating a new term (tid = 0) using the Drupal API function "taxonomy_save_term()".

<?php        //Save Term        if (!$TEST){$result = taxonomy_save_term($values);}        echo "Subject: "" . $values['name'] . "" Saved<br />\n";    ?>

We close the IF block and the WHILE loop, then proceed to the next line in the data file. To be on the safe side, we also tell PHP to give us more time to process the script just in case it's a big file and it will take longer than the default execution time limit as set in the php configuration. In this example, we are giving ourselves an extra 60 seconds each time we process a line.

<?php        //Counter            $row++;        //Reset Timelimit        echo "*************Resetting PHP Execution Time Limit********** <br />\n";            set_time_limit(60);    }}?>

Finally we close the data file.

<?phpecho "Closing File" . "<br />\n";fclose($handle);?>


Importing Authors

Authors, as you remember will be nodes since they will (at some point) contain additional information about the author. The same assumptions are made on file and folder paths. Each author has it's own unique identifier in the data files that needs to be included in the Drupal database. The node ID and author ID are two separate identifiers, we need the author ID in order to retrieve the node ID. This is designed to prevent duplicates and allow synchronization.

We start the same way we did with categories, but you'll notice that this time I am abstracting the data file.

<?php//Execute without actually writing to the DB$TEST = false;//Load the Libraries to abstract the data input fileinclude ("Parser.class.php");//Change to the Drupal Directorychdir("../cart");//Load and init the Drupal Systemrequire_once 'includes/bootstrap.inc';drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);//Create new parser and product data object$parser = new Parser();$dataLine = $parser->parseFile();?>

Similar to the terms/categories I will also utilize some convenience functions to help keep my code clean and organized. These two function allow me to retrieve the author's node ID from either the data file's ID or the Author's display name. Obviously it's much more reliable to retrieve it by using the author's ID in the data file.

<?phpfunction getNodeIdFromTitle($nodeTitle, $nodeType){    $query = "SELECT * FROM node WHERE type = "$nodeType" AND title = "$nodeTitle" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for "$nodeTitle"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for "$nodeTitle", Found: nid=" . $nid . "<br />\n";        }    return $nid;}function getNodeIdFromExtID($extID, $nodeType){    $query = "SELECT * FROM content_type_$nodeType WHERE field_" . $nodeType . "_id_value = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for $nodeType ID: "$extID"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for $nodeType ID: "$extID", Found: nid=" . $nid . "<br />\n";        }    return $nid;}?>

Their is one more function, this one is purely optional (meaning you can place the code for this one directly), but it helps keep this script extensible. It's purpose is to build the node data structure according to Drupal's requirements. This function will make more sense to you afterward.

<?phpfunction buildNodeFields_author($nodeData) {    $values = array();    $values['title'] = $nodeData->displayName;    $values['field_author_id'] = array(array('value' => $nodeData->id));    echo "Node fields built." . "<br />\n";        return $values;}?>

As you are probably realizing by now, the structure of the import script remains pretty much the same for any type of item. It's greatly simplified by the fact that I am abstracting the data file, but it's the same concept as shown with the vocabulary terms (categories/subjects). Go through each line of the data file and process that line if it's not empty.

<?phpforeach ($dataLine as $aItem) {    if (!empty($aItem->id)) {?>

Check to see if this author already exists in the Drupal database by utilizing the function created earlier. We retrieve the node id using the data files ID (ExtID). The use of the function is pretty simple, the first parameter is the ExtID and the second is the node type.

If a node ID is found then we update this author's information. If no node ID is found, then we create a new node with the minimum information only in the structure as require by Drupal. We will fill in the rest later, for now all we need is a node ID to reference. This design allows us to update or create data with the same code.

<?php//First see if this already exists in the Database        if (($nid = getNodeIdFromExtID($aItem->id, 'author')) !== false) {            //It Exists, Update it            $author_node =  node_load($nid);            $author_node->uid = '1';            $author_node->name = 'admin';                echo "Updating: " . $aItem->displayName . "<br />\n";        }        else {            //Does not Exist, Make new node            $author_node = array(                    'uid' => (string) 1,                    'name' => (string) 'admin',                    'type' => 'author',                    'language' => '',                    'body' => NULL,                        'title' => NULL,                        'format' => NULL,                        'status' => true,                        'promote' => false,                        'sticky' => false,                   'revision' => false,                        'comment' => '0'            );                  $author_node = (object) $author_node;            echo "Creating: " . $aItem->displayName . "<br />\n";        }?>

We now construct the author's information from the data file using the function created earlier. The variable "aItem" is an object that represent a line from the data file. Basically I am getting back this line as an associative array with index names as required by Drupal.

<?php        //Construct Node Fields        $node_fields = buildNodeFields_author($aItem);?>

Now that we have the node's data structure properly built, we save it to the Drupal database by utilizing the Drupal API function "drupal_execute". For more information about creating nodes, be sure to visit read the HelpFile Create Drupal Nodes from a Script.

<?php        //Save Node        if (!$TEST){$result = drupal_execute('author_node_form', $node_fields, $author_node);}        //Get node ID        if (!$TEST){$nid = str_replace("node/", "", $result);}        echo "Node: " . $nid . " Saved<br />\n";    ?>

Just like we did with the vocabulary terms, we tell PHP to give us more time and proceed to the next item on the list.

<?php        //Counter            $row++;        //Reset Timelimit        echo "*************Resetting PHP Execution Time Limit********** <br />\n";            set_time_limit(60);    }}?>


Importing Books

The book import script pretty much makes use of the same principals as the other two items, but their are some slight differences. First of all, we need to assign each book a cover image, then link it to an author (or authors), and finally assign it a category (taxamony term). Just like the authors, each book has it's own unique ID in the data file. We will utilize this ID to fill in the required SKU/model field for products.

This part should be strait forward by now, once again I am abstracting the data file's interface.

<?php//Execute without actually writing to the DB$TEST = false;//Load the Libraries to abstract the data input fileinclude ("Parser.class.php");//Change to the Drupal Directorychdir("../cart");//Load and init the Drupal Systemrequire_once 'includes/bootstrap.inc';drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);//Create new parser and product data object$parser = new IngramParser();$dataLine = $parser->parseFile();//Vocabulary ID is 2 for Subject$vocabID = 2;?>

Similar convince functions, but tuned to work for the "Book" node.

<?phpfunction getNodeIdFromTitle($nodeTitle, $nodeType){    $query = "SELECT * FROM node WHERE type = "$nodeType" AND title = "$nodeTitle" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for "$nodeTitle"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for "$nodeTitle", Found: nid=" . $nid . "<br />\n";        }    return $nid;}function getNodeIdFromExtID_book($extID, $nodeType){    $query = "SELECT * FROM uc_products WHERE model = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for $nodeType ID: "$extID"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for $nodeType ID: "$extID", Found: nid=" . $nid . "<br />\n";        }    return $nid;}?>

In our data file, the books and authors are linked to one another with the author ID. So we need a convenient way to convert the author's data file ID to the node ID as stored in the Drupal database. This is the same exact function used in the author import script, but named differently to avoid conflict.

<?phpfunction getNodeIdFromExtID_author($extID, $nodeType){    $query = "SELECT * FROM content_type_$nodeType WHERE field_" . $nodeType . "_id_value = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for $nodeType ID: "$extID"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for $nodeType ID: "$extID", Found: nid=" . $nid . "<br />\n";        }    return $nid;}?>

Just like authors, the books link back to their categories (in the data files) by using the category ID. We need an way to convert between that and Drupal term name. Thus the following function. It's the same one from the vocabulary import script. If your data files don't use category ID's to link books and subjects together, you can probably skip this function and use the name directly.

NOTE: We don not want the term ID's, we want their name.

<?phpfunction getTermNameFromExtID($extID, $vocabID){    $query = "SELECT * FROM term_data WHERE vid = $vocabID AND description = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $tname = false;    echo "Looking up term ID for "$extID"" . "<br />\n";        if ($term = db_fetch_array($queryResult)) {        $tname = $term['name'];        echo "Getting term Name for "$extID", Found: name=" . $tname . "<br />\n";        }    return $tname;}?>

This next function was perhaps one of the most difficult to figure out, but what it does is quite elegant. I designed it so that EVERY book has a cover image, no matter what. So a default book cover is loaded and assigned to every book that is imported in to the store.

I'll take some time to explain each part of this function because it's somewhat important to understand, but first here it is in it's entirety.

<?phpfunction setDefaultCoverImage($book_identifier, $book_title, $nid) {    //Creates the file/image entry in the Database for the product    //Load a default book cover image    $file_temp = file_get_contents('../import/common/DefaultBookCover.jpg');    //Save to the Drupal files directory under book_covers    $dest = file_directory_path() . '/book_covers/' . $book_identifier . '_cover.jpg';    $file_temp = file_save_data($file_temp, $dest, FILE_EXISTS_REPLACE);    $image = array(                                'fid'=>db_next_id('{files}_fid'),                        'title' => $book_title,                         'alt' => "Book Cover Image",                         'nid' => $nid,                     'filename' => basename($file_temp),                         'filepath' => $file_temp,                         'filemime' => 'image/jpg',                         'filesize' => filesize($file_temp)     );    //Create the Database Entry    db_query("INSERT into {files} (fid, nid, filename, filepath, filemime, filesize) VALUES (%d, %d, '%s','%s','%s',%d)", $image['fid'], $image['nid'], $image['filename'], $image['filepath'], $image['filemime'], $image['filesize'] );    //Return the image array    return $image;}?>

Just in case your puzzled, the answer is "yes", the book node must first exist before utilizing this function. This will make sense later on.

The function accepts 3 parameters:

  • book_identifier: This will be the image's file name. I utilize the data file's book ID
  • book_title: Used as the title for the image when displayed to the costumer
  • nid: The book's node ID as stored in the Drupal database, this links the image to the node

For this example, I have a default image already located on the server in the import folder where this script is executing. We have Drupal copy that image to the site's "files" directory under a folder called "book_covers" using the file name we provided in the function parameters (book_identifier). If the image already exists, we replace it. This is OK because (as you will see later) this function only gets called when we create a new book. This is done using the Drupal API function "file_save_data()".

First read the file into a variable, then pass that (image data) to the Drupal API function so it saves it in the proper location.

<?php    //Load a default book cover image    $file_temp = file_get_contents('../import/common/DefaultBookCover.jpg');    //Save to the Drupal files directory under book_covers    $dest = file_directory_path() . '/book_covers/' . $book_identifier . '_cover.jpg';    $file_temp = file_save_data($file_temp, $dest, FILE_EXISTS_REPLACE);?>

This is where things get a little difficult to understand. We need to build an data structure (node object) so we can link this image to a node. By looking at the code below, you would think that it's enough to put the book's node ID in this image's node object. It's not, you have to take this entire object and assign it as a field value in the book's node. It's confusing, but it will all make sense later.

<?php    $image = array(                                'fid'=>db_next_id('{files}_fid'),                        'title' => $book_title,                         'alt' => "Book Cover Image",                         'nid' => $nid,                     'filename' => basename($file_temp),                         'filepath' => $file_temp,                         'filemime' => 'image/jpg',                         'filesize' => filesize($file_temp)     );?>

Drupal doesn't have an API to create an image node, so we have to manually insert the required information into the database. Thankfully, Drupal makes database manipulation a one liner. Every field shown below is required, the good news is that we already generated most of them earlier.

<?php        //Create the Database Entry    db_query("INSERT into {files} (fid, nid, filename, filepath, filemime, filesize) VALUES (%d, %d, '%s','%s','%s',%d)", $image['fid'], $image['nid'], $image['filename'], $image['filepath'], $image['filemime'], $image['filesize'] );?>

For a complete explanation on how to attach images to nodes, take a look at the HelpFile titled How to attach images to nodes

Our image exists as a node in the Drupal system we can have the function return the image object node so we can complete it's assignment to the book node. We will get more into that part later.

<?php    //Return the image object node    return $image;?>

Now that you understand the function, lets get back to the main routine, we do our data file processing just like the other scripts.

<?phpforeach ($dataLine as $aItem) {    if (!empty($aItem->BookID)) {?>

Here, things start getting interesting, and possibly a little confusing. Just like the authors and vocabulary terms we want to make sure no duplicates are made, and allow existing items to get updated. The purpose of this block is to get a node ID we can reference.

<?php                 //First see if this already exists in the Database        if (($nid = getNodeIdFromExtID_book($aItem->BookID, 'book')) !== false) {            //It Exists, Update it            $book_node =  node_load($nid);            $book_node->uid = '1';            $book_node->name = 'admin';                echo "Updating: " . $aItem->title . "<br />\n";        }        else {            //Does not Exist, Make new node            $book_node = array(                    'uid' => (string) 1,                    'name' => (string) 'admin',                    'type' => 'book',                    'language' => '',                    'body' => NULL,                        'title' => NULL,                        'format' => NULL,                        'status' => 0,  //Products will not be available by default                        'promote' => false,                        'sticky' => false,                   'revision' => false,                        'comment' => '0'            );                              $book_node = (object) $book_node;            echo "Creating: " . $aItem->title . "<br />\n";                        ...?>

There is a slight difference on how we create a new book node, primarily due to the fact that it is not your ordinary standard node type. It's also a Product node. So we need to create additional default fields/values.

<?php                         ...            //Product nodes need to be handled differently            //First Create a product node with the basics            $values = array ();            $values['title'] = $aItem->title;            $values['body'] = '';            $values['model'] = (string) $aItem->BookID;            $values['sell_price'] = '0';            $values['format'] = '1';            $values['status'] = '0'; //Products will not be available by default                        ...?>

The rest is pretty standard, use "drupal_execute()" to create a new node.

<?php            if (!$TEST) {                //Create Node                drupal_execute('book_node_form', $values, $book_node);                //Now, reload the node                //Get the Node ID                $nid = getNodeIdFromExtID_book($values['model'], "book");                $book_node = node_load($nid);                $book_node->uid = '1';                $book_node->name = 'admin';                                ...?>

Since we just created a new book, we must assign it an image. This is where our "setDefaultCoverImage()" function comes in. It all makes sense now, doesn't it? Remember, we now have a complete book node object in the variable "book_node".

<?php                                ...                //Create a default book cover image and attach the image to the node                $book_node->field_image_cache = array(setDefaultCoverImage($aItem->BookID, $aItem->title, $nid));                            }        }?>

So by this point we have a fully constructed book node that exists in the Drupal database (whether is has information or not). It's time to fill in the fields from the data file. Lets start with the author(s), if you remeber from before, the book author(s) is a CCK node reference field. Drupal is very strict on the data structure of your array objects, so be sure to pay attention.

The following is the way to assign data to CCK fields. For more information on this, please read the HelpFile on Create Drupal Nodes from a Script. Notice how we are utilizing our convenience function "getNodeIdFromExtID_author()". Our data file assigns up to 3 authors per book. The abstracted data file object returns a 0 if an author is not assigned. To Drupal a 0 means no value for this CCK field.

<?php        $book_node->field_authors[0] = array('nid' => getNodeIdFromExtID_author($aItem->author_id1, 'author'));        $book_node->field_authors[1] = array('nid' => getNodeIdFromExtID_author($aItem->author_id2, 'author'));        $book_node->field_authors[2] = array('nid' => getNodeIdFromExtID_author($aItem->author_id3, 'author'));?>

Similar to the author CCK field, we have a CCK Text field that contains the Book's ISBN.

<?php        $book_node->field_book_isbn[0] = array('value' => (string) $aItem->ISBN);?>

We can now save the node. Thier is a ton of other information you could (and probably should) apply to your product at this point, but it's safe to skip it for the sake of this HelpFile.

You'll notice something different here, we are using "node_save()" instead of "drupal_execute()" because drupal_execute will not work in this context. This is something that took me a very long time to figure out.

<?php               //Save Node        if (!$TEST){node_save($book_node);}        echo "Node: " . $nid . " Saved<br />\n";?>

Assign the catagory/subject (vocabulary term). Since my categories have unique ID's associated to them, and the book data file refers to them by the ID, I use my convenience function to retrieve the term name. As mentioned before, you may just utilize the catagory name directly if possible.

As described in the Drupal HelpFile Create Drupal Nodes from a Script, we use the Drupal API functions "taxonomy_get_term_by_name()" and "taxonomy_node_save()"

<?php        //Assign Category (aka. Subject)        $n_catagory = getTermNameFromExtID($aItem->subject_id, $vocabID);        $term = taxonomy_get_term_by_name($n_catagory);        if (!$TEST){taxonomy_node_save($nid, $term);}    ?>

Just like the other times, we give ourselves more time.

<?php        //Counter            $row++;        //Reset Timelimit        echo "<p>*************Resetting PHP Execution Time Limit********** </p>\n";            set_time_limit(60);    }}?>


Importing Additional Product Information

You may have noticed something very important is missing from the book import script. Can you guess? Yes! it's the price! I did that on purpose because the data files I am importing from keep the price data in a separate file from the book data. To add the price data, we will load the book node, add the missing data, and save the node.

We begin the same way.

<?php//Execute without actually writing to the DB$TEST = false;//Load the Libraries to abstract the data input fileinclude ("Parser.class.php");//Change to the Drupal Directorychdir("../cart");//Load and init the Drupal Systemrequire_once 'includes/bootstrap.inc';drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);//Create new parser and data object$parser = new IngramParser();$dataLine = $parser->parseFile();function getNodeIdFromExtID_book ($extID, $nodeType) {    $query = "SELECT * FROM uc_products WHERE model = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for $nodeType ID: "$extID"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for $nodeType ID: "$extID", Found: nid=" . $nid . "<br />\n";        }    return $nid;}foreach($dataLine as $aItem) {    if (!empty($aItem->BookID)) {?>

Here we try to locate and load the book product node from the Drupal DB. The price data file links back to the books data file with the book ID. So I utilize the "getNodeIdFromExtID_book()" function to retrieve the node ID.

I have some special code in their to format and mark up the price. In Ubercart, the price that the costumer pays is stored in the "sell_price" field. For reference and tracking you could also fill in the field called "cost". The costumer never sees that field, and it's provided specifically for you the store owner.

After making the proper modifications, we once again use the "node_save()" API function. It's the only way.

<?php                //Try to load node        if (($nid = getNodeIdFromExtID_book($aItem->BookID, 'book')) !== false) {            //It Exists, We create or update it's price            $book_node =  node_load($nid);                echo "Found SKU#: " . $aItem->BookID . "<br />\n";            //Apply Price, mark it up by 15%            $book_node->sell_price = number_format($aItem->price * .15 + $aItem->price, 2, '.', '');            echo "Sell Price for "" . $book_node->title . "" Set to: $" . $book_node->sell_price . "<br />\n";                //Apply Regular Cost            $book_node->cost = $aItem->price;            echo "Cost of "" . $book_node->title . "" Set to: $" . $book_node->cost . "<br />\n";                //Save Node            if (!$TEST){node_save($book_node);}        }        else {                        echo "Book SKU# " . $aItem->BooksID . " NOT FOUND<br />\n";            }        //Counter            $row++;        //Reset Timelimit        echo "<p>*************Resetting PHP Execution Time Limit********** </p>\n";            set_time_limit(60);    }}?>

I can utilize the same concept above to add in additional information such as the book description

<?php//Execute without actually writing to the DB$TEST = false;//Load the Parse Librariesinclude ("IngramParser.class.php");//Change to the Drupal Directorychdir("../cart");//Load and init the Drupal Systemrequire_once 'includes/bootstrap.inc';drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);//Create new parser and data object$parser = new IngramParser();$dataLine = $parser->parseFile();function getNodeIdFromExtID_book ($extID, $nodeType) {    $query = "SELECT * FROM uc_products WHERE model = "$extID" LIMIT 1";    $queryResult =  db_query($query);    $nid = false;    echo "Looking up node ID for $nodeType ID: "$extID"" . "<br />\n";        if ($node = db_fetch_array($queryResult)) {        $nid = $node['nid'];        echo "Getting node ID for $nodeType ID: "$extID", Found: nid=" . $nid . "<br />\n";        }    return $nid;}foreach($dataLine as $aItem) {    if (!empty($aItem->BookID)) {        //First see if this exists in the Database        if (($nid = getNodeIdFromExtID_book($aItem->BookID, 'book')) !== false) {            //It Exists, We create or update it's annotation            $book_node =  node_load($nid);                echo "Found SKU#: " . $aItem->BookID. "<br />\n";            //Apply Annotation            $book_node->body = $aItem->annotation;            //Save Node            if (!$TEST){node_save($book_node);}            echo "Annotation for "" . $book_node->title . "" Saved<br />\n";            }        else {                        echo "Book SKU# " . $aItem->BookID. " NOT FOUND<br />\n";            }        //Counter            $row++;        //Reset Timelimit        echo "<p>*************Resetting PHP Execution Time Limit********** </p>\n";            set_time_limit(60);    }}?>


Conclusion

You can run those scripts over and over to update products since they have built in support for it. Also, once thing not mentioned here is how to replace the default covers with the real covers. It's as easy as just replacing files. The database only references file names, all your book covers (or product images) will be stored in the directory you specify. However, I think you may also need to regenerate the thumbnails and such.

You'll notice that throughout my code examples I have a number of "echo" statements and I have a variable named TEST at the top of each import script. You can run the code in test mode by setting the TEST variable to "true". This let you to see what is happening without actually writing information to the database.

Although these instructions were specific to building a book store, they can be adapted to any product type. The main purpose of this HelpFile was to show you how to create products in Drupal and Ubercart using a PHP script. Feel free to leave your comments below.