Habtamable behavior

The basic idea behind this behavior is that you (well, once in a while) need to save two HABTM models at the same time or search across both models, which are involved in the HABTM relationship.

By default cake does a nice job of saving HABTM models and their relation, but you need to know the ID(s) of at least one of the models that’s involved.
Also, you cannot supply HABTM model conditions, because cake will not build a JOIN to apply the conditions to both models properly.

So, again, this behavior attempts to solve some common issues with HABTM associations.

To give an example we could have a Location Model, which hasAndBelongsToMany Address.
In the Location Model we can have some arbitrary info, such as location name, location value, location capacity, etc., etc.
In the Address Model, of course, we’ll store the relevant address.

The usage is quite simple…

In the Location Model add:

public $hasAndBelongsToMany = array('Address');
public $actsAs = array('Habtamable');

The behavior will do a look-up in the addresses table, to see if a given address already exists.
It will also take care of validating the models.
Once the models are saved (using this behavior or not), you have an established HABTM relationship in the database. Now, using this behavior you can do a search by supplying the conditions of both models. See the updates below for more info…

Silly me, I’ve overlooked an important issue…
As of today’s writing (9/26/2009), the behavior will validate the models one at a time, which obviously looks ugly (and super user-unfriendly) when it comes to UI.
I’m working on fixing this at the moment…
This feature is now working with the latest update (maybe a bit hackish, however).

Grab the behavior here:
http://github.com/teknoid/cakephp-habtamable-behavior

Update (10/8/2009):
You can now pass the following settings:
joinType
fieldsToSkip
habtmModel
See behavior’s README for more details

Update (10/8/2009):
Validates two HABTM models at once, based on their own validation rules.
Will only do the fake re-bind, if the HABTM model is present in the conditions.

Update (10/7/2009):
It also dawned on me that searching across HABTM models, has always been a pain. So the latest revision published on the above date, will take care of that for you.

$this->Location->find('all', array('conditions' => array('Location.is_active' => 1, 'Address.city' => 'Miami')));

The behavior will do a “fake” hasOne bind, as outlined in some other posts here, and build a JOIN to perform the search.
For now I’m only supporting INNER JOIN, but after I get done with validation the behavior will accept a few additional settings to improve the searching and joining.
(And ultimately make it work with pagination).

P.S. Any feedback is greatly appreciated.

Random thought + prediction post

I hope not have too many of these… maybe once every few months.

jQuery and JavaScript coding has become a helluva lot of fun.

By the end of 2010 (with advent of HTML5), JavaScript will finally make web developers content and happy — and cause massive mojito-drinking.

CakePHP’s CDN/CloudFront/asset host helper

This asset host helper (in my case made specifically to help with Amazon’s CloudFront service) can be used to improve page load speed, by using dedicated asset servers.
For now the considered assets are: images, JavaScript files, and style sheets.

The whole idea was inspired by RoR asset helper:
http://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html
(Please give this link a read to fully understand the purpose behind this whole thing).

If you are lazy, like me, I’ll give you a few bullet points to consider:

  • By default all assets are loaded from the server’s local filesystem
  • Using this helper you can direct CakePHP to link to assets from a dedicated asset server(s)
  • This helps to improve page load speeds and alleviate the server from dealing with static assets
  • Browsers typically open at most two simultaneous connections to a single host, which means your assets often have to wait for other assets to finish downloading
  • Setup more than one host to avoid the issue above
  • To do this, you can either setup actual hosts, or you can use wildcard DNS to CNAME the wildcard to a single asset host. You can read more about setting up your DNS CNAME records from your ISP
  • It is suggested to use host names like: assets0.example.com, assets1.example.com, assets2.example.com, etc. (Depending on how heavy your traffic is, 4 hosts should be OK… add more if needed)

Now onto some features…

Obviously, be sure to download and save the helper into app/views/helpers/cf.php
(And include it in your App Controller’s helpers array)

It works exactly the same (including all options) as core Html and JavaScript helpers.
When in production mode the files are loaded from dedicated asset hosts, when in development mode the files are loaded from the local file system.
Keep the paths on the remote and local file systems the same. It will make your life so much easier and won’t break the helper ;)

Examples:

//include image from sub directory
<?php echo $cf->image('icons/test.png'); ?>  

//include image with some options
<?php echo $cf->image('test.png', array('id' => 'some-image')); ?> 

//include multiple JS files at once
<?php echo $cf->jsLink(array('file_one.js', 'file_two.js')); ?>

//include single JS file
<?php echo $cf->jsLink('single_file.js'); ?>      

//include single CSS file
<?php echo $cf->css('test.css'); ?>

//include multiple CSS files
<?php echo $cf->css(array('test.css', 'test2.css')); ?>

//include files from the view with false param (i.e. not in-line, but in the head of the page)
//CSS and JavaScript
<?php $cf->jsLink('not_inline.js', FALSE); ?>
<?php $cf->css('not_inline.css', NULL, NULL, FALSE); ?>

What about settings?

You will need provide your own dedicated asset host(s). See the helper comments or the link above to RoR API for details on how it should be set.
You will need provide a dedicated SSL asset host. At least, it is highly recommended to have one.
Be sure to force time stamps in core.php to ensure proper caching.

Please do not hesitate to ask any questions, your input and comments are greatly appreciated!

The code is relatively well documented, the helper is here:

http://github.com/teknoid/cakephp-asset-host-helper

P.S. Take a look here for more info about CloudFront and how it can help improve your app:
http://developer.amazonwebservices.com/connect/entry.jspa?externalID=2331

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://&#8217; 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://&#8217; 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;
?>