Better Getter

I sometimes like to boast that I’m an award-winning web developer. Though I mean it to be tongue-in-cheek in this industry where developers aspire to be ninjas and rockstars and Chuck Norrises, there’s a grain of truth to it; at DrupalCon 2010 in San Francisco, I participated in the module development contest with a silly module which turned Drupal into a Gopher) client, and won first prize - a first-gen Apple iPad that still gets a lot of use, plus some free services from Tropo which I unfortunately have yet to find a use for.

A Gopher client in Drupal is a ridiculous idea, but actually, I had been thinking of ways to improve communication with a server from within PHP for a while, and the contest provided the impetus to open the editor and get some code written. (I chose Gopher both for the novelty and because it’s pretty simple as far as internet protocols go.) That code eventually turned into Better Getter, or Bget. I actually haven’t worked on the project directly in a while, but I still find it useful to call upon most of the time when I need to do something like connect to a remote server in my code. Fortunately, Better Getter no longer only supports Gopher - or requires Drupal.

Photo of woman at a switchboard
Better Getter helps you make connections. Photo: US Department of Agriculture @ Flickr

Accessing a server before Better Getter

First, let’s look into the options currently available for communicating with a server so we can get an idea of the hole that Better Getter was built to fill.

drupal_http_request() - easy, but simple

From within Drupal, there’s the drupal_http_request() function. (Non-Drupal folks, please indulge me for a moment.) The function is easy to use and actually works pretty well in many cases, if HTTP is all you need to do, but there are limitations that you can hit which are impossible to work around. For example, drupal_http_request() will work for you if the server you want to connect to requires basic authentication, but if you need to use digest authentication, you’re just out of luck.

cURL - ugly power

Another option is the cURL PHP library, which, although it is not a core part of PHP, is quite commonly installed on shared hosts and included by default by various operating system package managers when PHP is installed. The cURL library supports many different protocols, with many different connection options, authentication methods, and the like; it’s enough to make your head spin. However, since it’s designed to work at a very low level, a major down side is that code that uses the cURL library is not a lot of fun to write, and even less fun to look at. A basic script to get the contents of a web page would look like this:

<?php
// Initialize the cURL handle
$ch = curl_init('http://example.com/');
// Tell cURL we want the response to be returned
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// Execute the handle and get the response
$response = curl_exec($ch);
?>

Yeah, you’re having to pass handlers functionally like that. Ew. And if you want to do more complex things, get used to looking at that huge table on the curl_setopt() documentation page explaining all the available options that can be set, and how. However, the power of cURL means that no matter how you want to connect to a server, you can probably find a way to do it with cURL.

So what I found myself wanting was something which leveraged the power of cURL library for the things I needed it for, but was simpler to use, while also being more extensible to the approach that drupal_http_reqest() was using. Out of this thinking came Better Getter. If I had to describe Better Getter in one line to someone savvy, I’d call it an extensible object-oriented wrapper to the cURL library. Conceptually, it is still using the same cURL library at its core that you can normally use directly, but it adds some sugar on top which makes using it more pleasant for developers.

Others

Of course, as it turns out, I’m not the only person to think along these lines; there are other projects which give you the power of cURL with a more pleasant wrapper. If all you need is HTTP, the HTTP extension may be of interest, though it needs to be installed as a PECL extension. Another powerful project that’s getting a lot of momentum recently is Guzzle, which looks quite promising, but requires that you import big chunks of the Symfony framework into your project if you’re not using it already. This makes it rather impractical for non-Symfony projects such as Drupal 7, but there has been some discussion about adding Guzzle to the Symfony-based Drupal 8, though it’s postponed at the moment.

Why Better Getter is, well, better

When you download Better Getter, you’ll see that it comes with two class files; one for the class Bget, and one for BgetHttp. The Bget class is the basic class, and it has some methods and properties for setting cURL options, opening and closing a cURL handler, and dealing with its response. The BgetHttp class extends Bget with a bunch of methods for dealing with stuff relevant to HTTP, such as request and response headers and POST request fields - messy stuff cURL doesn’t do itself and BgetHttp greatly simplifies. Let’s consider a hypothetical example where we want to get the “Date” header from a HTTP response. Here’s how to do it using just the cURL library:

<?php
// Initialize the cURL handle
$ch = curl_init('http://example.com/');
// Tell cURL we want the response to be returned
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// …And to include HTTP headers with the response
curl_setopt($ch, CURLOPT_HEADER, TRUE);
// Execute the handle and get the response
$response = curl_exec($ch);
// Split the headers away from the body of the response so we don't
// accidentally find stuff that's in the body. Also, this is a rather
// brittle way of going about this since cURL sometimes returns several sets
// of headers when doing authentication, redirection, etc. But for
// simplicity's sake:
list($headers, $body) = explode("/r/n/r/n", $response, 2);
// Now see if the header we're looking for is in the headers string
if (preg_match('/^Date: (.*)/', $headers, $matches)) {
 
$date_header = $matches[1];
}
?>

Now let’s do the same with BgetHttp:

<?php
// Initialize a Better Getter HTTP instance
$bget = new BgetHttp('http://example.com/');
// Execute it
$bget->execute();
// Get the value of the "Date" HTTP header
$date_header = $bget->getResponseHeader('Date');
?>

If you really like to reduce your lines of code in your project, you can go one step further. Bget typically lets you chain method calls together in a manner that should be familiar if you’ve used Drupal 7’s database API. The below code snippet is functionally equivalent to the above:

<?php
$bget
= new BgetHttp('http://example.com/');
$date_header = $bget->execute()->getResponseHeader('Date');
?>

Intended to be extended

At any rate, though you may find these classes useful on their own, they’re not intended to be used directly; instead, it’s intended that you subclass the classes and extend them as needed to get your job done. The Bget class file itself has very little code in it, and it’s all pretty well documented; give it a look and see what code is available for you to build off of. The BgetHttp class has quite a bit more code since it has to parse headers and such, but it should still be quite browsable.

For an example of an extension of BgetHttp in the wild, check out the GMO Payment Gateway module I wrote for a previous employer. It allows Ubercart to process payments via a particular payment gateway. Rather unglamorous, but you can see that it has a class file for a BgetGmo class which extends the BgetHttp class and adds methods for switching a connection between GMO’s sandbox and live processing servers, and overrides one of the BgetHttp’s methods to fix an issue with a non-standard way that GMO’s servers wanted data to be posted. All the other code from the core Bget and BgetHttp classes are simply reused - only what is necessary to be different is altered.

I recently used Better Getter in a more complex case, though I can’t share that code at the moment, unfortunately. It involved connecting to an enterprisey SaaS company’s API server. The particular server required requests to be, and formed its responses in, some rather… creative XML structures. Putting those structures together and taking them apart makes for some rather messy code, but I can keep all that code in my subclass’s class file and not in the various places in the module code where I need to make calls against the API site. I just call some methods on my object to add the data to the request, then call some other methods to get the parsed data out of the response after execution..

As an example, let’s go back to our hypothetical question of how we’d get the “Date” header from an HTTP server’s response using Better Getter. I showed you how it can be done above using an instantiation of the BgetHttp class, but if that’s something you need to do often in your code, you can subclass BgetHttp and override its execute() method to return the value of that header directly.

<?php
class BgetDate extends BgetHttp {
 
 
/**
   * Override the execute() method to return the value of the "Date" header.
   */
 
function execute() {
    return
parent::execute()->getResponseHeader('Date');
  }
}
?>

Now, our code gets even simpler:

<?php
$date
= new BgetDate('http://example.com/');
$date_header = $bget->execute();
?>

Of course, this is an almost uselessly simple example, but hopefully you get the idea of how Better Getter can help you save lines of code in the normal flow of your code and create reusable classes for doing routine communication with API servers and such for your current and future projects. Does Better Getter sound useful to you? I’d love to hear about the sort of project you might use Better Getter with, and maybe even entertain your Github pull requests.

Bonus: Tricking Drupal’s class registry

Okay, you’re sold on Better Getter. You’ve downloaded it and put it into your site’s “libraries” directory (perhaps you’re even using the Libraries module to make getting at those library code files easier) and now you’ve created your own subclass and added it to your module. You may already know about Drupal’s class file registry and how you can use the files[] array in your module’s .info file to list files in your module which contain classes, and Drupal will ensure that those files are only loaded if an instance of those classes is created. But what about the Better Getter class files? They’re not in your module, so you can’t add them to the files[] array… Do you just have to use include() in your .module file so that Better Getter’s include files are loaded even you don’t instantiate a Bget (or subclassed) object? Nope, it turns out you can use hook_registry_files_alter() to add files to the class registry even if they’re somewhere far away in the file system. Here’s an anonymized example straight from the most recent project I used Better Getter with.

<?php
/**
 * Implements hook_registry_files_alter().
 *
 * We add the Bget library files to the registry so we don't have to manually
 * include them when we want to create a MyModuleCxn object, because we're lazy
 * and also because the files[] parameters in the .info file only work for files
 * in "this" module directory.
*/
function my_module_registry_files_alter(&$files, $modules) {
  if (
$bget_path = libraries_get_path('bget')) {
   
$files[$bget_path . '/includes/Bget.class.inc'] = array(
     
'module' => 'my_module',
     
'weight' => 0,
    );
   
$files[$bget_path . '/includes/BgetHttp.class.inc'] = array(
     
'module' => 'my_module',
     
// Bump up the weight because this file needs to be included after
      // Bget.class.inc.
     
'weight' => 10,
    );
   
// And speaking of which, our own class file needs to load even later.
   
$my_path = drupal_get_path('module', 'my_module');
   
$files[$my_path . '/MyModuleCxn.class.inc']['weight'] = 20;
  }
}
?>

After doing this, all I have to do in my code is a $cxn = new MyModuleCxn() in my code, and Drupal will still take care of loading all the necessary class files automatically.

We want to work with you!