Category: Drupal

Zappos “related items” Drupal7 module (calling their API)

This custom Drupal7 module is something I cooked up to allow your site to connect to the Zappos API.

The module looks at a tag or category in which a certain item has been posted and uses that tag as a parameter to connect to Zappos and search items within the Zappos.com database.
Their sweet API will then return a transparent standard RESTreply with items that it has found in its extensive database, allowing the module to feed a block containing a list of related items straight from planet Zappos.

Basically you’ll end up with something like this (see “Related items on Zappos.com”-sidebar block in picture below):


We’ll start our code walkthrough with our main file zaprelated.module  and, when the logic of the code asks for it, we’ll take some sidesteps into other files or functions when they are called:

/*
* Implements hook_menu().
*/
function zaprelated_menu() {
$items = array();
$items['admin/config/media/zaprelated'] = array(
'title' => 'Zaprelated',
'description' => t('Configure the settings for the Zaprelated application'),
'page callback' => 'drupal_get_form',
'page arguments' => array('zaprelated_config_form'),
'access arguments' => array('administer site configuration'),
'file' => 'zaprelated_config_form.inc',
);
return $items;
}

First, we’re hooking into the Drupal menu system to allow us to create a general admin page (at <mydomain.com>/admin/config/media/zaprelated) that will allow us to configure some general settings for the module.

‘page callback’ is set to ‘drupal_get_form’ … drupal_get_form($form_id) returns our renderable (admin configuration) form.

The argument that we are passing to this callback function has to match the name of the function that will build our form.
In our case, trying to keep things clean from the start, we’re moving the actual creation of the form to a seperate file: ‘zaprelated_config_form.inc’

So, now we’ll create a new file with filename “zaprelated_config_form.inc” which will live inside our module directory. Nice and cosy, next to our zaprelated.module file.

/*
* Zaprelated general module settings form
*/
function zaprelated_config_form($form, &$form_state) {
$form['zap_apikey'] = array(
// code omitted for readability
'#default_value' => variable_get('zap_apikey'),
// code omitted for readability
)

$form['zap_baseurl'] = array(
 // code omitted for readability
 '#default_value' => variable_get('zap_baseurl','http://api.zappos.com/'),
 // code omitted for readability
 )

These first two blocks of code in the zaprelated_config_form  function are pretty basic: we’re setting up 2 text input fields that allow the admin (that would be you!) to enter the Zappos API key and Base URL (Zappos API key can be obtained here).
The baseurl is the url we’ll append searchstrings and methods to in a later stadium and probably won’t change anytime soon, but just keeping things clean and flexible.

Continuing our zaprelated_config_form function:

$form['zaprelated_caching']['zap_caching'] = array(
'#type' => 'checkbox',
'#title' => t('Enable caching'),
'#default_value' => variable_get('zap_caching', false),
'#description' => t('Check if you want API calls to be cached.<br>Zappos rate limits: 2500 calls/day or 2 calls/sec.'),
);

$timeframes = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
$form['zaprelated_caching']['zap_expires'] = array(
'#type' => 'select',
'#title' => t('Cache expires'),
'#default_value' => variable_get('zap_expires', 10800),
'#options' => $timeframes,
'#multiple' => false,
'#description' => t('When should the cache expire and should we connect to the API again.'),
'#states' => array(
'visible' => array(
':input[name="zaprelated_caching[zap_caching]"]' => array('checked' => TRUE),
),
),
);

The above fields will allow admins to enable caching on the API calls to Zappos. Zappos caps off API calls to 2500 calls/day or 2 calls/sec. So in an attempt to keep everyone happy we are caching the results in the dungeons of our Drupal database environment.
So, first field that takes care of that is a checkbox that simply toggles between the booleans TRUE and FALSE (caching enabled / disabled).
When the checkbox is enabled (checked), a tiny piece of jQuery code is fired off that renders a dropdownlist with cache expiration timing options defaulting to 3 days (10800 secs).
The jQuery code mentioned here lives inside the #states element of the zap_expires form element.

You’ll also see that both the checkbox as well as the dropdown are grouped together inside the [‘zaprelated_caching’] fieldset.
Make sure that the name of the fields (in our case zap_caching and zap_expires) match the names of the variables we are calling in the variable_get statements or things will get ugly (it just doesn’t work).

Finalizing “zaprelated_config_form.inc” we’ll return the form array to Drupal which will then gladly take care of rendering it for us.

return(system_settings_form($form));

This returns/renders the admin config form at admin/config/media/zaprelated (see screenshot below):

Back to our zaprelated.module file where we left things off before our little detour to our external admin form.
We’ll be creating the necessary code now to create the block that will hold our related items from the Zappos call.
Skipping the obvious and unexciting hook_block_info we’ll carry on with our hook_block_config to see what we will be serving to the site admin as block level configuration options:

/*
* Implements hook_block_configure().
*/
function zaprelated_block_configure($delta) {
$form = array();
switch ($delta) {
case 'zaprelate_items':
$form['zap_intro'] = array(
'#markup' => t('<p>These are some block specific settings as an extension to the general module settings on <a href="/admin/config/media/zaprelated">the admin page.</a><br><i>Further styling can be done through @path_to_css</p><br>and @path_to_tpl', array('@path_to_css' => drupal_get_path('module','zaprelated').'/zaprelated_row.css', '@path_to_tpl' => drupal_get_path('module','zaprelated').'/zaprelated_row.tpl.php')),
);

$form['zap_number_of_related_items'] = array(
'#type' => 'textfield',
'#title' => t('Configure number of related items to show.'),
'#size' => 6,
'#description' => t('Enter the number of related items that will appear in the block.'),
'#default_value' => variable_get('zap_number_of_related_items', 5),
);

$form['zap_cta'] = array(
'#type' => 'textfield',
'#title' => t('Set \'Read more\' text for listings.'),
'#size' => 36,
'#description' => t('Allows you to configure the clicktrough link text for all Zappos related items.'),
'#default_value' => variable_get('zap_cta', 'View item on Zappos.com'),
);

break;
}
return $form;
}

Code above creates a little snippet of plain text as a small introduction, a text inputfield that accepts the number of related items to be displayed and another inputfield that allows for a customization of the call to action click-text for the items. (enabling the admin to customize the linktext that will take the user to the Zappos storepage for that specific item)

Saving this config data is handled by hook_block_save:

/*
* Implements hook_block_save().
*/
function zaprelated_block_save($delta='', $edit = array()) {
switch($delta) {
case 'zaprelate_items':
variable_set('zap_number_of_related_items', (int)$edit['zap_number_of_related_items']);
variable_set('zap_cta', check_plain((string)$edit['zap_cta']));
break;
}
return;
}

Code above saves/sets 2 internal variables (in the Drupal variables table: the fastest way to handle relatively small amounts of variables that need to be available accross your entire site) coming from our hook_block_config() function.
At this stage, we have our block configuration form setup. Should look something like the screenshot below:

Next up, our hook_block_view. This is the function that does all the hard work and makes the call to the Zappos API.

/*
* Implements hook_block_view().
*/
function zaprelated_block_view($delta = '') {
switch($delta) {
case 'zaprelate_items':
$block['subject'] = t('Zappos related items');
$block['content'] = get_related_items();
break;
}
return $block;
}

Staying true to our initial intentions of keeping everything as clean as possible, I’ve moved all the hard working lines of code into a separate function get_related_items() , keeping the hook_block_view nice and tidy.

So, next up are the lines that make up the heart of the module. I’ll cut up the get_related_items() function in bitesize chuncks, for your viewing pleasure:

/*
* A module defined block content function.
*/
function get_related_items() {

if ( arg(0) == 'node' && is_numeric(arg(1)) && ! arg(2) ) {
$node = node_load(arg(1));
}

We will be using the category-name in which the items are posted as the searchterm for our Zappos API call.
Above lines will enable us to have a look inside the node where the related items block is currently displayed.

$API_KEY = variable_get('zap_apikey');
$BASE_URL = variable_get('zap_baseurl');
$term = $node->field_mobile_category['und'][0]['taxonomy_term']->name;
$limit = variable_get('zap_number_of_related_items', 5);
$service = "Search";
$caching = variable_get('zap_caching');
$expires = variable_get('zap_expires');
$callurl = $BASE_URL . $service . "/term/" . urlencode($term) . "?key=" . $API_KEY . "&limit=" . $limit;
$session = curl_init($callurl);
curl_setopt($session, CURLOPT_HEADER, false);
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);

As you see above, the $term variable is filled up with the category name of the current node and then passed to the $callurl  as a parameter for our search request.
The Zappos API call we will be making is a “Search call” and is formatted as such :  http://api.zappos.com/term/<termname>?key=<YOUR_API_KEY>&limit=<SET_YOUR_LIMIT>
(Read the Zapos API documentation for more parameters/formats/examples at : http://developer.zappos.com/)

All other variables are filled up out of the stored values in the variables table (through variable_get())
We’re also setting up a curl session in the last 3 lines. (but not executing them yet).

 $cid = 'zaprelated_' . md5(serialize($term)) . "_" . md5(serialize($service));
$cache_bin = 'cache';
$cache = cache_get($cid, $cache_bin);

if (is_object($cache) && (time() < $cache->expire) && $caching == TRUE) {
$rows = $cache->data;
print t('Working with cached data: @cid', array('@cid' => $cid));
}

Above we’re setting up a unique cache identifier (which will get stored in the ‘cache’ table) by the means of the md5 function.
The if structure then checks if the cache id already exists in the database and if it finds a match, we’ll be filling up $rows with the cached data.
If no previous cached version of the data is found, we carry on as if nothing happened (in our else condition that follows):

else {
$response = curl_exec($session);
curl_close($session);

if (empty($response)) {
watchdog('Zaprelated block', 'Recieved an unexpected reply from Zappos.<br> ' .
'URL: @url_query<br />' .
'Response: @response', array('@url_query' => $callurl, '@response' => print_r($response, TRUE)),
WATCHDOG_NOTICE);
return array('#markup' => t('No related items available'));
}
else {
$rows = array();
$json=json_decode($response,TRUE);
foreach($json['results'] as $item) {
$rows[] = array(
'productId' => $item['productId'],
'brandName' => $item['brandName'],
'productName' => $item['productName'],
'originalPrice' => $item['originalPrice'],
'thumbnailImageUrl' => $item['thumbnailImageUrl'],
'productUrl' => $item['productUrl'],
'price' => $item['price'],
'percentOff' => $item['percentOff'],
);
} // end foreach

cache_set($cid, $rows, $cache_bin, time() + $expires);
print t('New Cache set: @cid', array('@cid' => $cid));
} // endif empty response
} //endif cache found
return array('#markup' => theme('zaprelated_theme_function',array('items' => $rows)));
}

So, previously we’ve ruled out the option of already having the data in cache.
So this is where we’ll kick our function into gear.
Above chunk of code executes the previously defined curl session and calls the boys and girls at Zappos HQ. If the call remains unanswered, an entry is made into the watchdog log so you can check there what went wrong and at least know who to get mad at for screwing things up.
If the call is successfull tho, we initialize the $rows array and use the json_decode function to read all the items into our array.
We shuv the data in our cache and return the $rows array through the zaprelated_theme_function, which allows us to, well, theme the output.
Next lines will catch that call for theming through the hook_theme() function.

/**
* Implements hook_theme().
*/
function zaprelated_theme() {
$items = array(
'zaprelated_theme_function' => array(
'template' => 'zaprelated_row',
'variables' => array('items' => array()),
),
);
return $items;
}

Previous lines will take our items-array and hand them over to the zaprelated_row template file that will take care of the theming.
This might look like overkill for a relatively small module, but keeping things clean keeps us sane at the end of the day because things clutter up real fast when developing for Drupal.

So, create a new file zaprelated_row.tpl.php inside our module directory and fill it up with whatever code you want to use to display each ‘related Zappos item’.
I’ll share mine since we’re in a sharing mood:

<?php
drupal_add_css(drupal_get_path('module', 'zaprelated') .'/zaprelated_row.css');
foreach ($items as $item) { ?>
<div class='zaprelated row pid_<?php print $item['productId'];?>'>
<span>
<img src="<?php print $item['thumbnailImageUrl']; ?>">
</span>
<span>
<h1><?php print $item['productName']; ?></h1>
<i>by <?php print $item['brandName']; ?></i><br>
Original price: <del><?php print $item['originalPrice']; ?></del><br>
Zappos price: <?php print $item['price']; ?> (<?php print $item['percentOff']; ?> off)<br>
<a href="<?php print $item['productUrl']; ?>"><?php print variable_get('zap_cta','View item on Zappos.com'); ?></a>
</span>
</div>
<?php }    ?>

Pretty straightforward piece of html/php combo I’d say.
To keep in trend with our clean house policy, we’ll even throw in a reference to an external css file so you can style things even further.
The remainder of the above codeblock  is a loop through all the items that Zappos sent us, and printing them out through php print commands. Easy.

This concludes the walkthrough of the Zappos Drupal module (very early beta).
Hopefully, what you end up with, is a block that you can put anywhere on your site that sniffs for a piece of content inside the node (in my case the category of the node) and uses that to make a specific call to the Zappos API, returning related items from their huge database.

Disclaimer: This is nowhere near finished. Still got a lot of things hardcoded. Should allow the admin to select which fields are used to make the API call (or maybe even do a full text search).
I’ll hopefully have some time in the very near future to continue working on this.

Be sure to leave a comment. Corrections/improvements are more than welcome too. This is just a first draft and I’m really a designer first, coding is just something I love to do too, so bear with me if I screw up here or there.

Life is a continuous learning process, right ?