zen of coding

Build a URL-shortener for your app

In order to avoid using some external service, you might want to add a simple feature to your application to provide a URL-shortener.
The benefits are really simple… you’ll use your own domain name and while your app is around so will be your short-URL links.

Let’s define some goals:

  • We’ll provide an admin interface to take a long URL and create a short version for it
  • The URL’s should be pretty simple (i.e. http://www.example.com/s/d8YS)
  • We’ll have a simple counter of how many times a short URL has been clicked

First, we’ll create the table to hold our short URL data:

CREATE TABLE `short_urls` (
  `id` VARCHAR(36) NOT NULL COLLATE utf8_unicode_ci,
  `url_id` VARCHAR(50) NULL DEFAULT NULL COLLATE utf8_unicode_ci,
  `original_url` VARCHAR(50) NULL DEFAULT NULL COLLATE utf8_unicode_ci,
  `count` INT(10) NULL DEFAULT NULL,
  `created` DATETIME NULL DEFAULT NULL,
  `modified` DATETIME NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
)

Next, let’s take a look at the ShortUrl model:

<?php
class ShortUrl extends AppModel {

  public function beforeSave() {
    $tries = 0;
    while ($tries <= 10) {
      $urlId = $this->buildShortUrl();

      if(!$this->checkExisting($urlId)) {
        $this->data[$this->alias]['url_id'] = $urlId;
        break;
      }
    }

    return TRUE;
  }

  //this function generates short ID's
  //I stole from some place online
  private function buildShortUrl() {
    $codeset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $base = strlen($codeset);
    $n = mt_rand(299, 9999999);
    $converted = NULL;
    while ($n > 0) {
      $converted = substr($codeset, ($n % $base), 1) . $converted;
      $n = floor($n / $base);
    }
    return $converted;
  }

  private function checkExisting($urlId) {
    if($this->hasAny(array($this->alias . '.' . 'url_id' => $urlId))) {
      return TRUE;
    }
    return FALSE;
  }
}
?>

The model handles the short URL creation.
In beforeSave() we attempt to generate the short ID, by using the buildShortUrl(). This function works well for my app, because I do not anticipate a ton of URL’s will be required and the ID’s produced are pretty simple, as seen in the example.
Also, because the ID is quite simple there is a chance of collision, so I attempt to check for uniqueness 10 times (as seen in the while loop). Again, this works well for my needs, but you might want to adjust the tries for your app… or perhaps replace the buildShortUrl() method to return a slightly more unique ID to begin with.
At any rate, the approach should remain the same.

Now, let’s take a look at the controller:

<?php
class ShortUrlsController extends AppController {

  public function admin_index() {
    $this->ShortUrl->recursive = 0;
    $this->set('shortUrls', $this->paginate());
  }

  public function forward() {
    $this->autoRender = FALSE;
    $redirectTo = $this->ShortUrl->field('original_url', array('ShortUrl.url_id' => $this->params['id']));

    $this->ShortUrl->updateAll(array('ShortUrl.count' => 'ShortUrl.count + 1', $this->params['id']));
    $this->redirect($this->processUrl($redirectTo));
  }

  public function admin_add() {
    if (!empty($this->data)) {
      if ($this->ShortUrl->save($this->data)) {
        $this->Session->setFlash(__('The Short Url has been saved', true));
        $this->redirect(array('action' => 'show_url', $this->ShortUrl->id, 'admin' => true));
      } else {
        $this->Session->setFlash(__('The Short Url could not be saved. Please, try again.', true));
      }
    }
  }

  public function admin_show_url($id = NULL) {
    if($id) {
      $this->set('url', $this->ShortUrl->field('url_id', array('ShortUrl.id' => $id)));
    }
  }

protected function processUrl($url = NULL) {
    if($url) {
      if(!stristr($url, 'http://')) {
        return 'http://' . $url;
      }
      else {
        return $url;
      }
    }
  }

}
?>

The admin_add() function is very generic, since all the logic for actually building the URL is handled by the model.

The forward() takes the short URL ID, looks up the actual (long) URL in the table, updates the counter and handles the redirect. You’ll also notice a simple method processUrl(), which handles tacking on ‘http://’ to a URL, just in case a user has entered something like: www.yahoo.com instead of http://www.yahoo.com. Of course, the ‘http://’ is required for the redirect to work properly.

Last, but not least, we require a simple route in routes.php to tie all of this together:
Router::connect(‘/s/:id’, array(‘controller’=>’short_urls’, ‘action’=>’forward’), array(‘id’=>'[0-9a-zA-Z]+’));

And there you have a simple, but effective URL-shortener that works well for any app.

P.S. Just FYI, here are the views for the “admin” actions:

admin_add()
Allows the admin to enter an original (or long) URL.

<?php
  echo $form->create();
  echo $form->input('ShortUrl.original_url');
  echo $form->end('Build short URL');
?>

admin_show_url()
Shows the produced, short, URL back to the admin.

<?php
  echo FULL_BASE_URL . '/s/' . $url;
?>
  • Pingback: Vladimir Savić (firusvg) 's status on Sunday, 20-Sep-09 10:08:20 UTC - Identi.ca()

  • Pingback: Twitter Trackbacks for Build a URL-shortener for your app « nuts and bolts of cakephp [teknoid.wordpress.com] on Topsy.com()

  • Instead of relying on a random string, you could also use your article/page ID to generate a short string. advantages: no collision possible, and there’s a way to calculate the url so you may not even a lookup table.
    http://kevin.vanzonneveld.net/techblog/article/create_short_ids_with_php_like_youtube_or_tinyurl/

  • @Kevin van Zonneveld

    Thanks for sharing, this is a nice approach as well…

    I required a look-up table, because of the counter. Also, since I am not providing a real URL-shortener service, I’m not too worried about collisions (the ID’s have plenty of variations for my needs), but rather the simplicity of the URL.

    I guess the main goal here is not to provide a copy/paste solution, but rather to show a simple blueprint to build a URL-shortener by using a route and some model logic. (i.e. One can easily replace the ID creation method with something more robust, like the one you propose).

  • Good work. But what I don’t get is why you loop 10 times to check if it is unique? 1 time should be enough right? If not unique generate a new one, until it is unique.

  • @Akif

    10 times is the worst case scenario (in my app, I can hardly imagine that it would actually not find a unique ID within 10 tries)… however as soon as non-unique ID is found, I break out of the loop and save it.

    Like I mentioned, it works for my needs, but for other cases it might need to be adjusted. Or a “more unique” ID needs to be produced in the first place.

    • Tekh

      function encode($url) {
      $hash = sha1($url);
      return base_convert(hexdec($hash), 10, 32);
      }

    • Appreciation for this infromiaton is over 9000-thank you!

  • @Tekh

    Nice and simple. However, this is pretty long ID for my need.
    Nevertheless it should be be useful to some.

  • good work. should test it asap

  • This code would work well for a url shortener.

  • teknoid

    @Mark

    Well, I hope it serves you well ;)

%d bloggers like this: