The PHP Recipe application is a small but complete example that demonstrates the five Google Base API services: queries, insertions, deletions, updates, and batch commands. It uses the standard libcurl library to perform efficient HTTP requests for authentication and interaction with Google Base API.
You can download the source code for the sample program and host it on your own web server. You should use PHP 4.0.2 or later, since these versions ship with support for libcurl built-in. If you want to use an earlier version of PHP, you will need to install the libcurl library separately.
After installing the PHP sample application, replace line 4 with a valid API Key tied to your domain.
When you first use the Recipe application, you will have to log in to your Google Account and grant the script permission to access your Google Base entries.
The Recipe application allows users to upload new recipes to Google Base, edit or delete recipes they have already added, or delete all their recipes with a batch request. It uses GET and POST parameters to maintain two key elements of program state: the user's AuthSub token and requested action (insert, update, delete, or batch).
This section describes how the Recipe application initially interacts with the user and obtains a session token from the AuthSub server.
showIntroPage()When a user first arrives at the Recipe application, no GET or POST parameters are provided to the form. This indicates that the user has not yet obtained a single-session token. The code below sets up a link to the AuthSubRequest page with these options:
function showIntroPage() {
global $itemsFeedURL;
$next_url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
$redirect_url = 'https://www.google.com/accounts/AuthSubRequest?session=1';
$redirect_url .= '&next=';
$redirect_url .= urlencode($next_url);
$redirect_url .= "&scope=";
$redirect_url .= urlencode($itemsFeedURL);
...
}
The remainder of showIntroPage (omitted for brevity) prints the HTML tags to create a table containing this link.
exchangeToken()After the user authenticates to his Google Account using AuthSub, he is redirected back to the Recipe application page (as a result of the next parameter to AuthSubRequest). The redirection URL will contain one GET parameter: the single-use token granted by AuthSub.
The exchangeToken() function below is used to exchange the user's single-use token for a multi-use (session) token. The resulting session token is used for all interaction with the Google Base API: queries, insertions, deletions, updates, and batch commands. exchangeToken() uses the standard procedure for performing an HTTP request with libcurl, namely:
curl_init().curl_setopt(). In this case, we specify the AuthSubSessionToken URL (CURLOPT_URL) as well as the single-use AuthSub token (CURLOPT_HTTPHEADER). We also request that the HTTP response be returned rather than printed directly (CURLOPT_RETURNTRANSFER) and that the request should fail silently if an error occurs (CURLOPT_FAILONERROR).curl_exec() and catch the result.curl_close().Finally, the HTTP response (Token=...) is parsed with the split() function and the result returned.
function exchangeToken($token) {
$ch = curl_init(); /* Create a CURL handle. */
curl_setopt($ch, CURLOPT_URL,
"https://www.google.com/accounts/AuthSubSessionToken");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: AuthSub token="' . $token . '"'
));
$result = curl_exec($ch); /* Execute the HTTP command. */
curl_close($ch);
$splitStr = split("=", $result);
return trim($splitStr[1]);
}
This section describes how the Recipe application builds XML requests to be sent to the Google Base feed and how it parses the XML responses.
buildInsertXML()The buildInsertXML() function is used to construct the XML document that is sent when the user inserts a new recipe. It simply formats the POST fields submitted by the user (recipe_title, cuisine, and so forth) to conform to the Atom format for a single entry.
function buildInsertXML() {
$result = "<?xml version='1.0'?>" . "\n";
$result .= "<entry xmlns='http://www.w3.org/2005/Atom'" .
" xmlns:g='http://base.google.com/ns/1.0'>" . "\n";
$result .= "<category scheme='http://base.google.com/categories/itemtypes'" .
" term='Recipes'/>" . "\n";
$result .= "<title type='text'>" . $_POST['recipe_title'] . "</title>" .
"\n";
$result .= "<g:cuisine>" . $_POST['cuisine'] . "</g:cuisine>" . "\n";
$result .= "<g:item_type type='text'>Recipes</g:item_type>" . "\n";
$result .= "<g:cooking_time type='intUnit'>" . $_POST['time_val'] .
" " . $_POST['time_units'] . "</g:cooking_time>" . "\n";
$result .= "<g:main_ingredient type='text'>" . $_POST['main_ingredient'] .
"</g:main_ingredient>" . "\n";
$result .= "<g:serving_count type='number'>" . $_POST['serves'] .
"</g:serving_count>" . "\n";
$result .= "<content>" . $_POST['recipe_text'] . "</content>" . "\n";
$result .= "</entry>" . "\n";
return $result;
}
buildBatchXML()The buildBatchXML() function creates the XML document used to perform a batch deletion request. It formats the links to each item's feed URI (submitted as POST fields when the user clicks the Delete All button) into the Atom format for batch processing.
function buildBatchXML() {
$counter = 0;
$result = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$result .= '<feed xmlns="http://www.w3.org/2005/Atom"' . "\n";
$result .= ' xmlns:g="http://base.google.com/ns/1.0"' . "\n";
$result .= ' xmlns:batch="http://schemas.google.com/gdata/batch">"' . "\n";
foreach($_POST as $key => $value) {
if(substr($key, 0, 5) == "link_") {
$counter++;
$result .= '<entry>' . "\n";
$result .= '<id>' . $value . '</id>' . "\n";
$result .= '<batch:operation type="delete"/>' . "\n";
$result .= '<batch:id>' . $counter . '</batch:id>' . "\n";
$result .= '</entry>' . "\n";
}
}
$result .= '</feed>' . "\n";
return $result;
}
The Recipe application uses PHP's standard xml_parse function to parse the user's items feed into an internal array of entries. This function executes three provided handlers, or callback functions, as it parses an XML document: one when a start tag is read (startElement()), one when an end tag is read (endElement()) and one when character data is read (characterData()). These handlers coordinate using the following global variables:
$parsedEntries (array)$parsedEntries is itself a hashtable of recipe attributes (keys) and their values.$foundEntry (Boolean)true) or another XML tag (false). $foundEntry is set to true when startElement reads an opening ENTRY tag, and set to false when endElement reads a closing ENTRY tag.$curElement (string): $curElement is set by startElement(), then used by characterData() to update $parsedEntries.
/**
* Callback function for XML start tags parsed by
* xml_parse.
*/
function startElement($parser, $name, $attrs) {
global $curElement, $foundEntry, $parsedEntries;
$curElement = $name;
if($curElement == "ENTRY") {
$foundEntry = true;
$parsedEntries[count($parsedEntries)] = array();
} else if($foundEntry && $curElement == "LINK") {
$parsedEntries[count($parsedEntries) - 1][$attrs["REL"]] = $attrs["HREF"];
}
}
/**
* Callback function for XML end tags parsed by
* xml_parse.
*/
function endElement($parser, $name) {
global $curElement, $foundEntry, $parsedEntries;
if($name == "ENTRY") {
$foundEntry = false;
}
}
/**
* Callback function for XML character data parsed by
* xml_parse.
*/
function characterData($parser, $data) {
global $curElement, $foundEntry, $parsedEntries;
if($foundEntry) {
$parsedEntries[count($parsedEntries) - 1][strtolower($curElement)] = $data;
}
}
This section describes how the Recipe application interacts with the Google Base feed to retrieve, insert, update, and delete items.
getItems()The getItems() function, called to create the array of user-entered recipes, uses libcurl to perform an HTTP GET request on the user's items feed ($itemsFeedURL). It has the same basic structure as exchangeToken() above, with the exception of an added header (X-Google-Key) that specifies the application's developer key. After receiving the HTTP response, getItems() calls xml_parse to parse the Atom feed returned by the server.
function getItems($token) {
$ch = curl_init(); /* Create a CURL handle. */
global $developerKey, $itemsFeedURL;
curl_setopt($ch, CURLOPT_URL, $itemsFeedURL . "?");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/atom+xml',
'Authorization: AuthSub token="' . trim($token) . '"',
'X-Google-Key: key=' . $developerKey
));
$result = curl_exec($ch); /* Execute the HTTP command. */
curl_close($ch);
/* Parse the resulting XML. */
$xml_parser = xml_parser_create();
xml_set_element_handler($xml_parser, "startElement", "endElement");
xml_set_character_data_handler($xml_parser, "characterData");
xml_parse($xml_parser, $result);
xml_parser_free($xml_parser);
}
postItem()The postItem() function inserts a new recipe by performing an HTTP POST on the user's items feed. The two curl options of note are:
CURLOPT_POST, which specifies that libcurl should execute a POST instead of a GET; andCURLOPT_POSTFIELDS, which specifies the actual data to send (the item insertion request).
function postItem() {
$ch = curl_init(); /* Create a CURL handle. */
global $developerKey, $itemsFeedURL;
/* Set cURL options. */
curl_setopt($ch, CURLOPT_URL, $itemsFeedURL);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: AuthSub token="' . $_POST['token'] . '"',
'X-Google-Key: key=' . $developerKey,
'Content-Type: application/atom+xml'
));
curl_setopt($ch, CURLOPT_POSTFIELDS, buildInsertXML());
$result = curl_exec($ch); /* Execute the HTTP request. */
curl_close($ch); /* Close the cURL handle. */
return $result;
}
updateItem()The updateItem function updates an existing recipe by performing an HTTP PUT on that recipe's feed URI. Since libcurl requires that HTTP PUT data be provided as a file resource, updateItem:
tmpfile function.seeks back to the beginning.CURLOPT_PUT, CURLOPT_INFILE and CURLOPT_INFILESIZE to specify the PUT data and size.fclose.
function updateItem() {
$ch = curl_init(); /* Create a CURL handle. */
global $developerKey;
/* Prepare the data for HTTP PUT. */
$putString = buildInsertXML();
$putData = tmpfile();
fwrite($putData, $putString);
fseek($putData, 0);
/* Set cURL options. */
curl_setopt($ch, CURLOPT_URL, $_POST['link']);
curl_setopt($ch, CURLOPT_PUT, true);
curl_setopt($ch, CURLOPT_INFILE, $putData);
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($putString));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: AuthSub token="' . $_POST['token'] . '"',
'X-Google-Key: key=' . $developerKey,
'Content-Type: application/atom+xml'
));
$result = curl_exec($ch); /* Execute the HTTP request. */
fclose($putData); /* Close and delete the temp file. */
curl_close($ch); /* Close the cURL handle. */
return $result;
}
deleteItem()The deleteItem() function deletes a recipe the user has entered previously by performing an HTTP DELETE on its feed URI. The DELETE command is specified using the option CURLOPT_CUSTOMREQUEST below.
function deleteItem() {
$ch = curl_init();
global $developerKey;
/* Set cURL options. */
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_URL, $_POST['link']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: AuthSub token="' . $_POST['token'] . '"',
'X-Google-Key: key=' . $developerKey
));
$result = curl_exec($ch); /* Execute the HTTP request. */
curl_close($ch); /* Close the cURL handle. */
return $result;
}
batchDelete()The batchDelete() function deletes all of the user's recipes by issuing a batch request to the Google Base API. This is accomplished by performing an HTTP POST (specified by CURLOPT_POST) to the batch feed URL, specifying the batch XML document as POST data.
function batchDelete() {
$ch = curl_init(); /* Create a CURL handle. */
global $developerKey, $itemsFeedURL;
/* Set cURL options. */
curl_setopt($ch, CURLOPT_URL, $itemsFeedURL . "/batch");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: AuthSub token="' . $_POST['token'] . '"',
'X-Google-Key: key=' . $developerKey,
'Content-Type: application/atom+xml'
));
curl_setopt($ch, CURLOPT_POSTFIELDS, buildBatchXML());
$result = curl_exec($ch); /* Execute the HTTP request. */
curl_close($ch); /* Close the cURL handle. */
return $result;
}
Can't get the PHP sample code to work on your server? Here are a few suggestions:
curl_errno and curl_error functions to drill down and see exactly what error is occurring. Refer to the list of cURL error codes for more information about what each error code means.