Charlie Harvey

UK Postcode Lookup — My First Drupal7 Module

Update 2012-02-26 I updated this module in a few ways. It now uses the Drupal presave hook rather than the form_alter hook -- meaning that if there is a postcode and no lat/long it will always look up your postcode. That is a much better way to do it in my opinion. I’ve also changed the default lookup code to use Google’s geocoding rather than openstreetmap’s. This is mostly because we needed to look up non-uk addresses. I had had great hope for the lovely open, non-corporate Geonames service, but unfortunately it doesn’t do lookups of addresses in Israel. I am keeping an eye though. I’ve not changed the writeup below, so it will be out of sync with the actual code.

I’m moonlighting on a project for an extremely cool client at the moment, it involves mapping and will be made out of Drupal. One of the things that it will need to do is to transform UK postcodes into latitude and longitude data, so my first cract at writing a Drupal module is to implement that. I’’ve put up a tar.gz of the ukpostcodelookup module code, but it is far from tested as yet. You have been warned.

The module will use StreetMap.co.uk's postcode lookup abilities, screen scraping the data from there and using that to populate the latitude and longitude fields for a particular content type. If you know of a non-corporate lookup service I’d love to hear about it (storing the lookup data locally may be an option, since the state has released the data now(finally)).

In this writeup, I’ll just walk you through the code that I’ve written so far. Bear in mind that I’m familiar with php, but I’ve never written a Drupal module before. And that I’ve spent two hours on it. So if you're a hardcore Drupal geek, please be gentle with me. I’m sure I’ve missed important bits. Also, I don't know if this would work in production. YMMV and all that.

The setup

You'll have a Drupal 7 (D7) site set up already. In my D7 site, I have a content type called premises. I’ve added a few extra fields, including latitude, longitude and postcode. In the long run, the idea is that by entering the postcode, Drupal'll be able to look up the longitude and latitude and fill in the fields. I was editing locally with Vim, but you can do that how you prefer. It'll probably work

Code walkthrough

The first bit that you need to do is to create a directory for your module, so $ mkdir /path/to/drupal/sites/mysite/modules/ukpostcodelookupwill do the job. Next you need to create an info file, to tell Drupal the basic shizzle about yer new module. Let's edit mysite/modules/ukpostcodelookup/ukpostcodelookup.info thus. name = "UK Postcode Lookup" description = "Looks up UK postcodes and inserts them into Long/Lat fields of a content type" core = 7.x package = "MyClient" php = 5.2 ; Core files files[] = ukpostcodelookup.module This is all straightforward. You can use it as a template for your module, replacing the bits you expect to replace. The package="blah" is worthy of note. It groups packages in the modules page of the admin interface. Which may be useful. It was useful for me, because it meant I could locate my module!

We're about done with the info file. Save and start editing mysite/modules/ukpostcodelookup/ukpostcodelookup.module . This is where we start cooking with gas as it were. I’ll go through the functions one by one in what seems like a logical-ish order.

ukpostcodelookup_menu(): Add our module config to the config menu

/** * Implements hook_menu(). * cut and pasted from http://drupal.org/node/1111212 */ function ukpostcodelookup_menu() { $items = array(); $items['admin/config/content/ukpostcodelookup'] = array( 'title' => 'UK Postcode Lookup', 'description' => 'Configuration for UK Postcode lookup module', 'page callback' => 'drupal_get_form', 'page arguments' => array('ukpostcodelookup_form'), 'access arguments' => array('access administration pages'), 'type' => MENU_NORMAL_ITEM, ); return $items; } This adds our config form to the configuration menu. Its overriding the Drupal "hook_menu" — hooks are Drupal's way of letting modules step in and take control of the program flow, overriding them just means creating a function called mymodule_hookname. The 'page arguments' => array('ukpostcodelookup_form') refers to the next function we'll look at. The other thing to note is that "$items['admin/config/content/" puts this into the content section of the menus, like in the screenshot. admin menu with uk postcode lookup drupal module in it

ukpostcodelookup_form(): The config form

Man this is a pretty dull function to write. It defines the fields that you want in your config form and is the one that was called as a page_argument in ukpostcodelookup_menu(). The thing to note are the call to variable_get() which makes what you set here persistent in D7's database. /** Module configuration form */ function ukpostcodelookup_form($form, &$form_state) { $form['content_type'] = array( '#type' => 'textfield', '#title' => t('Content type'), '#default_value' => variable_get('content_type', 'company_premises'), '#size' =>> 28, '#maxlength' =>> 28, '#description' => t('Content type to add postcode lookup to.'), '#required' => TRUE, ); $form['postcode_field'] = array( '#type' => 'textfield', '#title' => t('Postcode field'), '#default_value' => variable_get('postcode_field', 'field_postcode'), '#size' => 28, '#maxlength' => 28, '#description' => t('Field that postcode is stored in.'), '#required' => TRUE, ); $form['lat_field'] = array( '#type' => 'textfield', '#title' => t('Latitude field'), '#default_value' => variable_get('lat_field', 'field_lat'), '#size' => 28, '#maxlength' => 28, '#description' => t('Latitude field which will be updated with looked up postcode.'), '#required' => TRUE, ); $form['long_field'] = array( '#type' => 'textfield', '#title' => t('Longitude field'), '#default_value' => variable_get('long_field', 'field_long'), '#size' => 28, '#maxlength' => 28, '#description' => t('Longitude field which will be updated with looked up postcode.'), '#required' => TRUE, ); return system_settings_form($form); } The form that that little lot makes looks like this. uk postcode drupal module admin form screenshot

_http_post(): An utility function to get content from the interwebs

This is really a function that I ought to put in a library file somewhere, cos I’ll no doubt need to use it again. PHPers will suggest using curl. But I’m not sure whether the environment in which I’ll be deploying will have it. So I write the code to run a http POST request from scratch. /* Might not get to use curl for POST requests, this method sends a POST and retrieves the content * that gets sent back. */ function _http_post($uri, $data) { $context = stream_context_create( array ( 'http'=>array( 'method'=>'POST', 'content'=>$data ) ) ); $file = @fopen($uri, 'r', false, $context); if (!$file) { throw new Exception("Problem with $uri"); } $content = @stream_get_contents($file); if ($content === false) { throw new Exception("Problem reading data from $uri"); } return $content; }

_lookup(): Actually find your postcode

This is the fun part. We call streetmap.co.uk's converting interface, and return an associative array of our latitude and longitude. I’m using the throw Exception again, like in _lookup(). Not sure if this the idiomatic Drupal way to handle errors, but it does seem to work, so that's all good. /** Lookup the postcode on streetmap.co.uk, screen scrape the long and lat out * Note: wasn't sure if I’d have CURL, so I’ve build a page posting tool below */ function _lookup($postcode) { $postcode = strtolower(str_replace(' ','',$postcode)); $uri = "http://www.streetmap.co.uk/streetmap.dll?"; $post_data = "MfcISAPICommand=GridConvert&name=".$postcode."&type=Postcode"; $output=_http_post($uri,$post_data); if(!$output) throw new Exception("Nothing retrieved from http://streetmap.co.uk/ Perhaps its down?"); preg_match('#long</strong> \(wgs84\)\s*?\<\/td\<\s*?\<td width="50%" align="center" valign="middle"\>\S{2,3}:\S{2}:\S{2} \( (.*?) \)#i',$output,$long_capture); preg_match('#lat</strong> \(wgs84\)\s*?\<\/td\>\s*?\<td width="50%" align="center" valign="middle"\>\S{2,3}:\S{2}:\S{2} \( (.*?) \)#i',$output,$lat_capture); $long = $long_capture[1]; $lat = $lat_capture[1]; return array('lat'=>$lat,'long'=>$long); }

ukpostcodelookup_form_alter(): Lets rock!

So, we now have the bits we need to implement another hook. This time I shall override the edit form in the Drupal admin interface. If the form is a node of the right type (set in the admin interface, default company_premises), and it has a postcode but no longitude or latitude set, then we will call _lookup with our postcode as an argument. That will call _http_post and retrieve the latitude and longitude from streetmap.co.uk. We'll then update the latitude and longitude fields with the data from that call, and display a friendly message asking the user to save again if the latitude and longitude are correct. Easy.

Notice here the calls to variable_get to retrieve the variables we set in the config interface. /** Alters the form for content type company_premises * We only alter the form if: * - The form is from a node of the right type (company premises) * - The node has a postcode set * - The node has no longitude or latitude fields set */ function ukpostcodelookup_form_alter(&$form, $form_state, $form_id) { $content_type = variable_get('content_type', 'company_premises'); $postcode_field = variable_get('postcode_field', 'field_postcode'); $lat_field = variable_get('lat_field', 'field_lat'); $long_field = variable_get('long_field', 'field_long'); if (isset($form['#node']) && $form_id == $content_type .'_node_form' && isset($form[$postcode_field]['und'][0]['value']['#default_value']) && !isset($form[$lat_field]['und'][0]['value']['#default_value']) && !isset($form[$long_field]['und'][0]['value']['#default_value']) ) { $postcode = $form[$postcode_field]['und'][0]['value']['#default_value']; $ref = _lookup($postcode); $form[$lat_field]['und'][0]['value']['#default_value'] = $ref['lat']; $form[$long_field]['und'][0]['value']['#default_value'] = $ref['long']; drupal_set_message("I just set latitude: " . $ref['lat'] . ", longitude: " . $ref['long'] . ", based on postcode: $postcode <br />Hit save if that is correct." ); } } Latitude and longitude setting message, screenshot

What did I learn?

  • Writing Drupal modules is not at all scary. I might write more.
  • I need to find a better source of Drupal tutorials. A single page run through of how to write a Drupal module would have been a huge help. What are your favoutite resources? Chuck them in the comments!
  • Postcode lookups can vary a little between mapping systems.
  • Drupal’s hooks are rather nicely implemented and work as expected for simple tasks like the one I was trying to achieve.


Comments

  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.