Medryx Observations

October 4, 2008

CodeIgniter and Doctrine

Filed under: PHP — Tags: , , , — maulin @ 9:47 am

The most widely sited source for how to bootstrap Doctrine with CodeIgniter is available on the CI Wiki. While this approach is effective, I found it a bit intrusive into the CI code. I didn’t like the idea of having to edit index.php, and I didn’t like that the database.php, which should really only contain configuration code, was hijacked to actually do the bootstrapping. So I looked into it a bit, and I think there is a cleaner (if not easier) way.

The goal when bootstrapping Doctrine is to load up your database connection and point to your models folder. Thats pretty much it. Once you do that, all your models autoload, so you don’t need to do things like $this->load->mode('User'). Instead you can simply use Doctrine’s tools such as new User() or Doctrine::getTable('User').

So here is the process:

1. Install a “hook” that will bootstrap Doctrine. Edit application/config/hooks.php and add the following:

$hook['pre_controller'][] = array(
	'function' => 'bootstrap_doctrine',
	'filename' => 'doctrine.php',
	'filepath' => 'hooks'
);

2. Copy the following to application/hooks/doctrine.php

// YOU MUST EDIT THIS TO BE THE PATH TO YOUR DOCTRINE LIBRARY
// I put mine in a 'libs' folder at the same level as the 
// CI application folder, but it can be anywhere.
require_once APPPATH   	   . DIRECTORY_SEPARATOR . 
				'..' 	   . DIRECTORY_SEPARATOR . 
				'libs'     . DIRECTORY_SEPARATOR . 
				'Doctrine' . DIRECTORY_SEPARATOR . 
				'Doctrine.php';
 
function bootstrap_doctrine() {
 
	@include APPPATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database' . EXT;
 
	// Set the autoloader
	spl_autoload_register(array('Doctrine', 'autoload'));
 
	//optional, you can set this to whatever you want, or not set it at all
	Doctrine_Manager::getInstance()->setAttribute('model_loading', 'aggressive');
 
	// Load the Doctrine connection
	// (Notice the use of $active_group here, to make it easy to swap out
	//  you connection based on you database.php configs)
 
	if (!isset($db[$active_group]['dsn'])) {
		//try to create the dsn, if it has not been manually set
		//in your config. I personally would opt to set my
		//dsn manually, but it works either way
		$db[$active_group]['dsn'] = $db[$active_group]['dbdriver'] . 
                        '://' . $db[$active_group]['username'] . 
                        ':' . $db[$active_group]['password']. 
                        '@' . $db[$active_group]['hostname'] . 
                        '/' . $db[$active_group]['database'];
	}
 
	Doctrine_Manager::connection($db[$active_group]['dsn']);
 
	// Load the models for the autoloader
	// This assumes all of your models will exist in you
	// application/models folder
	Doctrine::loadModels(APPPATH . DIRECTORY_SEPARATOR . 'models');
}

3. Done. Doctrine and its models are now available in the usual Doctrine way in all of your controllers.

Let me know if you have questions!

October 3, 2008

CodeIgniter REST

Filed under: PHP — Tags: , , , , — maulin @ 1:42 am

I have been looking into various ways of building a simple PHP JSON REST server (to connect easily to dojo’s JsonRestStore on the clien). First off, I am a bit surprised that this does not exist as a project somewhere already.

CakePHP has a pretty simple way of mapping your REST requests to its controllers. I like their general approach here. And overall I like CakePHP’s architecture, separation of concerns, and design. I am not a huge fan of their ORM (I prefer to declare my model in my code and have it work back to my database, not the other way around), but it gets the job done for simple applications. It is also fairly easy to change your view depending on the incoming request accepted content-types, so you could route to a JSON or XML view depending on the type of REST request.

But, I also have some problems with Cake. I find it can be maddening to debug at first — trying to figure which magically called view or method or template or htaccess redirect is causing your problem. But once you get it all configured, its acceptable. Mind you, you will have to handle your own response codes, json handling, and raw POST data, but that’s not too hard either.

The biggest problem with Cake is that I can’t easily use it with the Doctrine ORM. And I already have a working project using Doctrine for its model and data access layers. So my options were figuring out how to map my existing model to Cake or how to make a Doctrine-friendly framework use REST. I chose the latter. Since this project was way to small to warrant diving into Symfony, I chose CodeIgniter as my framework.

CodeIgniter seems small and relatively lightweight, and in all honesty, it is very similar to Cake. I think it doesn’t try to do quite as much as cake in the background, and that made it easier for me to immediately grasp. While it took the better part of 3 days to get my hands around Cake, it only took about 3 hours to get my hands wround CI. Heck, in 3 hours, I felt good enough about it, that I was ready to hack it to get REST to work with it.

There is one CodeIgnite/REST solution on their wiki, it is relatively complicated and I think intrusive into your controller code. In my mind, the combination of REQUEST_METHOD, REQUEST_URI and perhaps HTTP_ACCEPT headers should route you to the right controller/method to get on with your business. Actually I think that METHOD/URI route you to the controller/method and the HTTP_ACCEPT can modify your choice of view (since the controller/method should be the same regardless of how the data gets rendered).

Both Cake and CodeIgniter have a method for creating custom routing of URI to controller/methods. However, Cake’s is much more robust. CI basically only allows routing from URI without any analysis of the REQUEST_METHOD, and furthermore, disallows query-strings when used in conjunction with their friendly URL system. That is to say this would not be a valid URI in CI:

http://medryx.org/friends/?city=Portland

The good news is that CI allows overriding of its code libraries quite easily (as does Cake). You simply create a prefixed version of the class in your local application/libraries folder. So we just need to override the custom routing behavior and we can get REST in CI fairly easily.

First, I created a subclass of Router called MY_Router and placed it in MY_Router.php in my application/libraries folder. Then I found the meat of the Routing occured in _parse_routes, so I copied it, put it in my subclass, and started to hack.

I think the simplest way to allow for the REQUEST_METHOD to influence the route is to make the routes.php config file allow for this. By default, routes.php in the config directory is very simple, essentially a uri is compared against the key of the $route[] config variable, and if it exists (with some Regex niceness), then the route is sent to the string that is the value of the key. Its all explained quite simply in the CI docs.

So my plan is to let the value of a route be an associative array — the keys will be ‘GET’, ‘POST’, ‘PUT’, and ‘DELETE’. Then the value is the actual route. I’ll preserve the other regex goodness that CI already provides. It turned out to only add 20 lines of code to the original _parse_routes method to do this.

//if the route is not a plain string, but an array
if (is_array($this->routes[$uri])) {
            //loop through the options and see if we find a match
            foreach ($this->routes[$uri] as $method => $route) {
                  foreach ($val as  $method => $route) {
                     if (strtolower($method) == strtolower($_SERVER['REQUEST_METHOD'])) {
                        $this->_set_request(explode('/', $route));   
                        return;
                     }
                  }
            } 
         }
}

Well that’s not too bad. That just gets added ~line 28, and a similar bit arond line 53, and you are done. Now its time to make our new custom routes. First lets look at a call to /friends:

$route['friends'] = array(
'GET' =>'friends/findAll',
'POST'=>'friends/add'
);

So, as per REST, a POST is a request to create new record, and in JSON-REST the RAW POST data is the content of the new record. A GET is a request for all records.

Now lets add PUT/DELETE methods, and allow for GET of a friend with a given id:

$route['friends/(:num)'] = array(
'GET' =>'friends/findById/$1',
'POST' =>'friends/update/$1',
'PUT' => 'friends/update/$1',
'DELETE' => 'friends/delete/$1'
);

That’s not too bad either. It uses the CI router regex stuff to get the id routed right to the right method as its parameter. In the case of POST/PUT, once again, the raw post data is the data.

The last bit turned out to be just a little trickier. It turns out that you can send parameters to the GET request, instead of an id, and that these params represent the query you want to execute. For example:

http://www.medryx.org/friends/?city=Portland

Well, this shouldn’t be too bad either — since we know that really any regex works as the pattern we can do:

$route['friends/\?(:any)'] = array(
'GET'=> 'friends/find/$1'
);

and that should do it right? Well, not quite. It turns out that when parsing the URI, CI by default uses an “AUTO” method, which, if there is a query-string, uses that as the URI. Woops, that’s not going to work for us. Luckily its a simple config to change it in config.php:

$config['uri_protocol']    = "REQUEST_URI"; //changed from "AUTO"

Ok, now we’re in business.

Update: Actually, all we need to do is make sure it doesn’t use the $_GET shortcut with the AUTO setting. You can see how I do this below.

Well still not quite. For security (I suppose), CI by default disallows ?, &, and = in the URI. So we need to allow that. Another simple config:

$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-\?\=\&'; //added ?, =, & to the end

Okay, now we really are in business. Fire this up, and it all “just works”. Next step is to create simple way to designate a controller as a REST controller, and if it is a REST controller, automatically handle all this custom routing, so you don’t have to repeat it all. But that is fun for another day.


Update

So giving this just a couple more minutes of thought, I think I’ve made it light years easier. Now you simple place MY_Router.php in your application/libraries folder, and rest.php in your application/config folder and you are DONE. (No need to change any configs, or edit any of the CI source).

Once setup, you get the following behavior:

GET:
http://www.medryx.org/Friends/100

This will call Friends controller in your controllers folder with the method findById(100). Better yet a POST or PUT to this same url will results in a call to your controller’s update method, with two parameters — the id (100), and the raw post data (which is what you need for REST). If, on the other hand this is a REST query in the form of:

GET:
http://www.medryx.org/Friends/?city=Portland

then your controller gets called with

$controller->find($query); //$query is assoc array or in this case array('city' => 'Portland')

:-)
The revised source for MY_Router.php, to be copied into your libraries folder is:

<?php
/**
 * Drop this class into you CodeIgniter application/libraries folder to automatically provide REST
 * routing to your application.
 * 
 * This looks for a config/rest.php file that contains a list of controllers that you want to make
 * available via REST. See that file for a full description of how it works.
 * 
 * As an aside, this class provides a couple other nuggets: it makes url's case insensitive, and
 * it provides a way for you to expose other web services only when the incoming request is
 * of a certain method. For example, GET methods should not have side-effects, POST can, so you
 * can make your web services more clear if you use those semantics. (even outside of REST)
 * 
 * Keep in mind that using this Router will AUTOMATICALLY CHANGE YOUR CONFIG FOR THE FOLLOWING
 * 
 * $config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-\?\=\&';
 * $config['uri_protocol']	= "REQUEST_URI";
 * 
 * 
 *
 */
class MY_Router extends CI_Router {
 
	function MY_Router() {
		$this->config =& load_class('Config');
		//fix the config
		$this->config->set_item('permitted_uri_chars', 'a-z 0-9~%.:_\-\?=&\*');
		if ($this->config->item('uri_protocol') == 'AUTO') {
			$_GET = array();
		}
		parent::CI_Router();
 
	}
 
	/**
	 * used to curry the parameters sent to your REST methods
	 *
	 * @param array $segments
	 * @param string $type (put, post, get, delete)
	 */
	function _set_rest_request($segments = array(), $type) {
		parent::_set_request($segments);
		$type = strtolower($type);
 
		//this is our chance to add arguments
		//for post/put lets provide the raw post
		//data as an extra parameter
 
		if ($type == 'post' || $type == 'put') {
			$this->uri->rsegments[] = file_get_contents("php://input");
		} else if ($type == 'get') {
			//if there is an '=' in the final segment, then this is a url-encoded query string
			//query, lets parse it
			$seg = $this->uri->rsegments[count($this->uri->rsegments)-1];
			if (preg_match('/=/', $seg) && preg_match('/\?/', $_SERVER['REQUEST_URI'])) {
				parse_str($seg, $params);
				$this->uri->rsegments[count($this->uri->rsegments)-1] = $params;
			}
		}
 
	}
 
	/**
	 * reads the rest.php file and sets up routing as appropriate
	 *
	 */
	function setupRestRouting() {
		define('DEFAULT_REST_METHODS', 'DEFAULT_REST_METHODS');
 
		$defaultRestMethods = array(
			'find' => 'find',
			'findById' => 'findById',
			'update' => 'update',
			'delete' => 'delete',
			'add' => 'add',
			'findAll' => 'findAll'
		);
 
 
		//load the rest config
		@include(APPPATH.'config' . DIRECTORY_SEPARATOR .'rest'. EXT);
		$rest = ( ! isset($rest) OR ! is_array($rest)) ? array() : $rest;
 
		foreach($rest as $controller => $methods) { 
			if ($methods == DEFAULT_REST_METHODS) {
				$methods = $defaultRestMethods;
			}
			$controller = strtolower($controller);
 
			if (isset($methods['find']) && $methods['find']) {
				$route[$controller . '/?\?(:any)'] = array(
					'GET'=> $controller . '/' . $methods['find'] . '/$1'
				 );
			}
			if (isset($methods['findById']) && $methods['findById']) {
				$route[$controller . '/(:num)']['GET'] = $controller . '/' . $methods['findById'] . '/$1';
			}
			if (isset($methods['update']) && $methods['update']) {
				$route[$controller . '/(:num)']['POST'] = $controller . '/' . $methods['update'] . '/$1';
				$route[$controller . '/(:num)']['PUT'] = $controller . '/' . $methods['update'] . '/$1';
			}
			if (isset($methods['delete']) && $methods['delete']) {
				$route[$controller . '/(:num)']['DELETE'] = $controller . '/' . $methods['delete'] . '/$1';
			}
			if (isset($methods['findAll']) && $methods['findAll']) {
				$route[$controller]['GET'] = $controller . '/' . $methods['findAll'];
			}
			if (isset($methods['add']) && $methods['add']) {
				$route[$controller]['POST'] = $controller . '/' . $methods['add'];
			}
			//default for extra methods
			$route[$controller . '/(:any)'] = $controller . '/$1';
		}
 
		$this->routes = array_merge($this->routes, $route);
 
	}
 
	/**
	 * override _set_routing to allow us to setup our REST routing
	 * before we parse the routs
	 *
	 */
	function _set_routing() {
		// Are query strings enabled in the config file?
		// If so, we're done since segment based URIs are not used with query strings.
		if ($this->config->item('enable_query_strings') === TRUE AND isset($_GET[$this->config->item('controller_trigger')]))
		{
			$this->set_class(trim($this->uri->_filter_uri($_GET[$this->config->item('controller_trigger')])));
 
			if (isset($_GET[$this->config->item('function_trigger')]))
			{
				$this->set_method(trim($this->uri->_filter_uri($_GET[$this->config->item('function_trigger')])));
			}
 
			return;
		}
 
		// Load the routes.php file.
		@include(APPPATH.'config/routes'.EXT);
		$this->routes = ( ! isset($route) OR ! is_array($route)) ? array() : $route;
		unset($route);
		$this->setupRestRouting();
		// Set the default controller so we can display it in the event
		// the URI doesn't correlated to a valid controller.
		$this->default_controller = ( ! isset($this->routes['default_controller']) OR $this->routes['default_controller'] == '') ? FALSE : strtolower($this->routes['default_controller']);	
 
		// Fetch the complete URI string
		$this->uri->_fetch_uri_string();
 
		// Is there a URI string? If not, the default controller specified in the "routes" file will be shown.
		if ($this->uri->uri_string == '')
		{
			if ($this->default_controller === FALSE)
			{
				show_error("Unable to determine what should be displayed. A default route has not been specified in the routing file.");
			}
 
			$this->set_class($this->default_controller);
			$this->set_method('index');
			$this->_set_request(array($this->default_controller, 'index'));
 
			// re-index the routed segments array so it starts with 1 rather than 0
			$this->uri->_reindex_segments();
 
			log_message('debug', "No URI present. Default controller set.");
			return;
		}
		unset($this->routes['default_controller']);
 
		// Do we need to remove the URL suffix?
		$this->uri->_remove_url_suffix();
 
		// Compile the segments into an array
		$this->uri->_explode_segments();
 
		// Parse any custom routing that may exist
		$this->_parse_routes();	
 
		// Re-index the segment array so that it starts with 1 rather than 0
		$this->uri->_reindex_segments();
 
	}
 
 
	/**
	 * the meat of REST (and all) routing. if regular routing, this is the same as _parse_requests
	 * but this adds the REST capable routing, calls _set_rest_request instead of _set_request
	 * when appropriate to allow currying of params, and, as a bonus, builds in case-insenitivity
	 * so that the url typed into the browser can be of any case without any problems
	 *
	 */
   function _parse_routes()
   {
 
      // Do we even have any custom routing to deal with?
      // There is a default scaffolding trigger, so we'll look just for 1
      if (count($this->routes) == 1)
      {
         $this->_set_request($this->uri->segments);
         return;
      }
 
      // Turn the segment array into a URI string
      $uri = strtolower(implode('/', $this->uri->segments));
 
 
      // Is there a literal match?  If so we're done
      if (isset($this->routes[$uri]))
      {
         if (is_array($this->routes[$uri])) {
            //loop through the options and see if we find a match
            foreach ($this->routes[$uri] as $method => $route) {
                  foreach ($this->routes[$uri] as  $method => $route) {
                     if (strtolower($method) == strtolower($_SERVER['REQUEST_METHOD'])) {
                        $this->_set_rest_request(explode('/', $route), strtolower($_SERVER['REQUEST_METHOD']));   
                        return;
                     }
                  }
            } 
         }
 
         $this->_set_request(explode('/', $this->routes[$uri]));
         return;
      }
 
      $restRequest = false;
      // Loop through the route array looking for wild-cards
      foreach ($this->routes as $key => $val)
      {                  
         // Convert wild-cards to RegEx
         $key = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $key));
         // Does the RegEx match?
         if (preg_match('#^'.$key.'$#i', $uri))
         {         
            if (is_array($val)) {
               //loop through the options and see if we find a match
               foreach ($val as  $method => $route) {
                  if (strtolower($method) == strtolower($_SERVER['REQUEST_METHOD'])) {
                     $val = $route;
                     $restRequest = strtolower($_SERVER['REQUEST_METHOD']);
                     break;
                  }
               }
            } 
            // Do we have a back-reference?
            if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
            {
               $val = preg_replace('#^'.$key.'$#', $val, $uri);
            }
 
            if ($restRequest) {
            	$this->_set_rest_request(explode('/', $val), $restRequest);
            } else {
	            $this->_set_request(explode('/', $val));      
            }
            return;
         }
      }
 
      // If we got this far it means we didn't encounter a
      // matching route so we'll set the site default route
      $this->_set_request($this->uri->segments);
   }
}

and the rest.php file to put into your application/config folder:

<?php
/**
 * Drop this file into you CodeIgniter application/config folder to enable REST handling for
 * your controllers. You also need to copy MY_Router.php into application/libraries.
 * 
 * In this file you will specify which controllers will use REST handling, and how they
 * will handle it. the DEFAULT_REST_METHODS are as follows:
 * 
 * $rest['MyController']= array(
 *		'find' => 'find',
 *		'findById' => 'findById',
 *		'update' => 'update',
 *		'delete' => 'delete',
 *		'add' => 'add',
 *		'findAll' => 'findAll'
 *	);
 * 
 * is the same as:
 * 
 * $rest['MyController'] = DEFAULT_REST_METHODS
 * 
 * the signature for these methods is as follows
 * 
 * function find($query):
 *		$query: 	associative array of params sent to your query
 * 		example: http://foo.com/restService/?category=true
 * 
 * function findAll():
 * 		example: http://foo.com/restService/
 * 
 * function findById($id):
 * 		$id: numeric id of your item (must be numeric)
 *		example: http://foo.com/restService/123
 * 
 * function update($id, $data)
 * 	    $id: numeric id of your item (must be numeric)
 * 		$data: the raw post data content that should be used to update (usually json or xml)
 * 
 *  function add($data)
 * 		$data: the raw post data content that should be used to add (usually json or xml)
 * 
 *  function delete($id)
 * 	    $id: numeric id of your item (must be numeric)
 * 
 *  If you would like to map to different methods from each of these, you
 *  can simply provide the name you prefer in the declaration of your rest service below.
 *  if there are some messages you would like to ignore (404), then you
 *  can simply omit that declaration, or set the declaration to empty string/null/false
 */
 
 
 
$rest['Friends'] = DEFAULT_REST_METHODS;

September 10, 2008

dijit.Search Part 2

Filed under: dojo, javascript — Tags: , — maulin @ 7:27 pm

Well its been a while since my first post about a search widget for dojo.data stores. There are a lot of reasons — the beautiful Portland, Oregon summers being the most significant. But now the kids are back in school, life is normalizing, and its time to show you how I finished what I think is a very nifty widget.

First a recap: My goal was to make a pretty widget that simply allowed you to search one or more dojo.data stores. You could use this in a lot of ways. I’ve posted a demo here that uses several dojox.data stores to allow you simultaneously search Google, Yahoo!, and Wikipedia from your site. While this is one use case, the use case I find most interesting is doing a “global” search on your own dojo.data stores that are driving your web application. But the sky is the limit! You can use this to build OSX Searchlight-style functionality into your app. You can “skin” the widget itself to make it match your site’s style. You an change the prompt from “Search” to anything you want, that may be more useful in your app. You can even include a dropdown menu that appears upon clicking the magnifying glass to the left of the search box that allows your users to choose which stores (or which methods) they would like to use to search. You can define a template for your result to make it display in just about any way you can imagine. (The default simply shows the “label” text from the resulting items.)

Before going any further, I want to make it clear that this works, but is clearly alpha-level code. And I think there are some simplifications that can be made in the usage of the widget in the future if this is useful to more people than me. But for now, think of it as the 0.9 dojox.grid — way too complicated, but really functional. And hopefully this widget will evolve into a simpler and more elegant widget just as the grid has. FYI, this widget is dependent on dojo 1.2 (primarily because it uses the 1.2 dojox DataGrid).

So lets dive into how to use the widget. The first concept we need to go over is how to create an interface into your stores to allow searching them in various ways. To solve this, you need to create a SearchStore. A SearchStore is really a composite of interfaces into other dojo.data stores. As is my usual practice, sometimes things are explained more easily with code, so here goes. Lets build the demo widget. We’ll start with just the WikipediaStore:

dojo.require("medryx.widget.Search");
dojo.require("dojox.data.WikipediaStore");
 
//create the WikipediaStore
var wikiStore = new dojox.data.WikipediaStore();
 
//create the SearchStore
var searchStore = new medryx.widget.SearchStore();
 
//add the wikiStore to the SearchStore
searchStore.addStore({
  name:"Wikipedia", // what will this search be called (useful when there is more than one store
                    // in your SearchStore (or more than one method or more than one method of
                    // searching the same store
  store:wikiStore, //the source store
 
  //this is the most important piece. given a search string (searchTerms) and
  //and a requested searchType, return the query that should be passed to the
  //the store (in this case to wikiStore.fetch())
  queryBuilder:function(searchTerms, searchType) {
    //if the user specifies "Wikipedia" search, or any search, then use this store
    if (!searchType || searchType == "Wikipedia" || searchType == "*") {
      return {query:{text:searchTerms, action:"query"}}; //see WikipediaStore for how to query Wikipedia
    }
  }
});

Okay, that does seem a little complicated. But conceptually its pretty simple. When a user types a string into the search box, the widget needs to know how to handle it with respect to your source stores — so you provide a “queryBuilder” method that converts generic search terms to your specific query. When there is just one member of you SearchStore, this seems like overkill. So lets add some more stores. I used the dojox.data.ServiceStore to create a Google store and Yahoo store that can search those services. The code for how to create those services is a bit beyond the scope of this article, but you can see the full code in the demo. For now, lets take it on faith that there is a yahooStore and a googleStore that I have created. So lets wire them up to our SearchStore:

searchStore.addStore({
  name:"Google",
  store:googleStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Google" || searchType == "*") {
      return {query:{q:searchTerms}}; //see google smd for format
    }
  }
}); 
 
searchStore.addStore({
  name:"Yahoo",
  store:yahooStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Yahoo" || searchType == "*") {
      return {query:{query:searchTerms}}; //see yahoo smd for format
    }
  }
});

So now you can see how we can create a very complicated method for searching multiple different stores with the same search string. There are several more features to the SearchStore, but I’ll come back to those. Right now, lets change gears and see how we can actually use this widget with this SearchStore. In the simplest case we simply declare the following in our markup:

<div dojoType="medryx.widget.Search" store="searchStore">
  <script type="dojo/connect" event="onHitSelected" args="item, store, hit">
    console.debug("Hit selected:", item, store, hit);
  </script>
  <script type="dojo/connect" event="showAllResults" args="terms">
    alert("search for more results for \"" + terms + "\" yourself, you lazy bum!");
  </script>
</div>

You can see that you can easily connect to the “onHitSelected” event to decide how you will handle the user selecting one of your search results. This event passes the original item, and its original source store as parameters. It also provides access to the “hit” itself, which is an item in the SearchStore that maps to the original item from your source store. This can be very useful, once you know about some more features of the SearchStore. We’ll get there, I promise.

Notice also that you can connect the the “showAllResults” event, which is really expected to behave as a link to your “complete search” page (or dijit.Dialog, or whatever). The “Complete search results” link is at the bottom of the search results in all cases.

Lets look at some ways to customize the widget a little more:

<div dojoType="medryx.widget.Search" store="searchStore">
  <div dojoType="dijit.Menu">
    <div dojoType="dijit.MenuItem" searchType="Google" iconClass="Google">Google</div>
    <div dojoType="dijit.MenuItem" searchType="Yahoo" iconClass="Yahoo">Yahoo</div>
    <div dojoType="dijit.MenuItem" searchType="Wikipedia" iconClass="Wikipedia">Wikipedia</div>
  </div>
  <script type="dojo/connect" event="onHitSelected" args="item, store, hit">
    console.debug("Hit selected:", item, store, hit);
  </script>
  <script type="dojo/connect" event="showAllResults" args="terms">
    alert("search for more results for \"" + terms + "\" yourself, you lazy bum!");
  </script>
</div>

If you include a dijit.Menu inside your declaration, then this menu will be displayed when you click the magnifying glass to the left of the search box. You can see there is a special (REQUIRED) attribute on each MenuItem called “searchType”. This is the searchType that is sent to your “queryBuilder”, and can be any string that your queryBuilder understands. If a user does not expand the menu, and simply presses “Enter”, then the searchType is by default set to “*” (though you can change this via the defaultSearchType attribute).

There are plenty of attributes to help you customize your search widget, and they are all documented in the source of medryx.widget.Search.

So now lets look at how we might do something useful with our “onHitSelected” event. Let say we want to open an iframe with the url of the hit (as in the demo). First add the iframe to your markup:

<div align="center">
  <iframe style="width:90%; height:600px; border:1px solid black; display:none;" id="preview"></iframe>
</div>

So one way to get to the “right” url regardless of which store your result came from would be to make a big, complicated event handler (can you tell this will not be the preffered method?)

var hitHandler = function(item, store, hit) {
  var src = "";
  if (store == yahooStore) {
   src = store.getValue("ClickUrl");
 } else if (store == googleStore) {
   src = store.getValue(item, "unescapedUrl", store.getValue(item, "url", defaultValue));
 } else if (store == wikiStore) {
   src = "http://en.wikipedia.org/w/index.php?title=" + store.getValue(item, "title");
 }
  dojo.byId("preview").src = src;
  dojo.byId("preview").style.display = "";
}

This is perfectly reasonable, but its quite “spaghetti-like”. And every time you add a store, you have to change you handler. What if we could define for each store how to obtain the “src” string, and then use a common way of accessing it? Much cleaner IMHO. So SearchStore has another attribute you can include when calling addStore — “map”. A map is a list of pseudo-fields that will be accessible for each hit from the SearchStore itself. Each pseudo-field begins with “search”. So lets upgrade our searchStore as follows, adding in the maps:

searchStore.addStore({
  name:"Google",
  store:googleStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Google" || searchType == "*") {
      return {query:{q:searchTerms}};
    }
  },
  map:{
    searchUrl:function(item,attribute,defaultValue) {
	return this.getValue(item, "unescapedUrl", this.getValue(item, "url", defaultValue));
    }
  }
}); 
 
searchStore.addStore({
  name:"Yahoo",
  store:yahooStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Yahoo" || searchType == "*") {
      return {query:{query:searchTerms}};
    }
  },
  map:{
    searchUrl:"ClickUrl" //simple! so no "function" needed, just map from "searchUrl" to yahoo's "ClickUrl"
  }
}); 
 
searchStore.addStore({
  name:"Wikipedia",
  store:wikiStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Wikipedia" || searchType == "*") {
      return {query:{text:searchTerms, action:"query"}};
    }
  },
  map:{
    searchUrl:function(item, attribute, defaultValue) {
      return "http://en.wikipedia.org/w/index.php?title=" + this.getValue(item, "title");
    }
  }
});

Now the event handler can be simplified, using the “hit” parameter:

var hitHandler = function(item, store, hit) {
    dojo.byId("preview").src = searchStore.getValue(hit, "searchUrl");
    dojo.byId("preview").style.display = "";
}

Much nicer, don’t you think? And you could add more stores to this widget or more ways to search the same store, and never have to touch the event handler.

Last but not least, you can customize how you “hits” appear on the dropdown list, as well as how the “category headers” appear. Simple include a child of your widget with an attribute of template=”category” or template=”hit”. You can then access any of  the attributes (or pseudo-attributes) of your item with the familiar ${attributeName} notation.

Building this widget taught me a LOT about dojo widgets. Unfortunately it wasn’t until I was almost done with the widget, that I read about the dojo 1.2 attributeMap. So obviously, to really take this widget to the next level, some work would need to be done. But right now it is quite functional, and can quickly and easily add a lot of robust functionality to your applications with very little additional work.

For now, you’ll need to grab the files you need from my dev server at http://dev.medryx.org/lib/js/medryx/. I’ll try to submit this to dojo as a dojox.widget sometime soon.

Let me know what you think, and if I can answer any questions!

August 15, 2008

Transparent PNG’s on IE with dojo

Filed under: dojo, javascript — Tags: , , , — maulin @ 5:55 am

Transparent PNG’s are fabulous. They are the key to making your app look really professional and slick. And they allow you to make easily themed web pages without having to redesign all your images, since you can just rely on the background colors shining through the transparency. Transparent PNG support is available on Firefox and Safari, and IE7. But IE6 does not support it.

There are, however, workarounds. And when it comes to making all browsers behave the same in the background, equalizing browser differences so you can just focus on writing great web applications — dojo is the best tool around. So it seems only natural to me that we should try to create a solution to this problem with dojo.

Fortunately, there has already been great work done by Angus Turnbull (which by the way is a GREAT name!) He has created a nifty solution to the problem. It turns out that IE6 does support transparent PNG’s when a special “filter” is used to load the image. Well angus has created a script that uses IE6’s “behaviors” via *.htc javascript files to implement a simple solution that will replace all your images with this filter-loaded version. This includes css background images and IMG tag images. And in his latest version he handles background-repeat and background-position, so this really becomes a simple drop-in solution, and your transparent PNG’s suddenly work perfectly on all the major browsers.

There is just a *little* configration needed to get Angus’s IEPNGFix to work. First you drop the iepngfix.htc, iepngfix_tilebg.js, and blank.gif files somewhere on your server. You then add the following to your CSS file:

img, div { behavior: url(/path/to/iepngfix.htc) }

You also need to open and change the path to blank.gif in iepngfix.htc to the absolute path to the blank.gif file. (Or a relative path, but relative to *any* page that will actually use the file, which is inconvenient). Finally you use a typical script tag to include iepngfix_tilebg.js in your page if you want background-repeat and background-position to work.

Its really not too bad, but its still more work that I want to do. And I would love to be able to use it any dojo web app without all this configuration. Well, dojo to the rescue! With a few modifications, you can make this available with a simple:

dojo.require("medryx.util.png");

(Or you can use whatever namespace you want!) Ideally, this could some day be included in dojo natively, so you wouldn’t have to do any work to get this support. In this technique, you don’t worry about absolute or relative paths — because you replace all that stuff with calls to dojo.moduleUrl
and you can eliminate the change to your CSS with a nifty CSS trick that Angus provides. Finally, you can eliminate the script tag to load iepngfix_tilebg.js by just adding a dojo.provide() call at the top of that file and using a dojo.require() to get it.

Sounds like a lot of work, but really it wasn’t, and now I can use it in any of my web applications without any real work. So here is the step-by-step process:

  1. download the IEPNGFix.zip file
  2. create the following directory structure and copy the files from the zip as follows — lets assume you will put this in a “medryx” module, though obviously yours would be named differently:
    dojotoolkit/
    -- dojo/
    --dojox/
    --dijit/
    --medryx/
    ----util/
    ------png.js --> see below
    ------png/
    --------iepngfix.htc
    --------iepngfix.php --> optional, see below
    --------iepngfix_tilebg.js
    --------blank.gif
  3. Create a file called “png.js” whose contents are quite simple. It simply checks your browser, and if needed, adds a rule to your first stylesheet to handle png’s with the behavior fix (this is a slight modification right from Angus’s demo page). The content of the file is as follows:

    dojo.provide("medryx.util.png");
     
    (function() {
    //a fix for transparent png in ie6 (only works with php, or change this to the iepngfix.htc file if you can get the server to set content-type correctly!
     
    if (dojo.isIE && dojo.isIE < 7 &&  document.styleSheets && document.styleSheets[0] && document.styleSheets[0].addRule){
    	//dojo.require("medryx.util.png.iepngfix_tilebg"); needed to support repeat, but doesn't work for me.
    	var fixUrl = dojo.moduleUrl("medryx.util.png", "iepngfix.php"); //use *.php if your server doesn't server the *.htc file correctly
    	document.styleSheets[0].addRule('*', 'behavior: url("' + fixUrl.path + '")');
    }
     
    })();

    One small thing to notice — the iepngfix.php file is really only needed to set the content-type header of the *.htc file correctly so IE6 will know what to do with it. Obviously there are other (better) ways of doing this on each type of server. But the php file is a simple workaround if you don’t have access to changing how *.htc content-type is set on your server.

  4. Open the iepngfix.htc file and search for “blank.gif”. Replace that line with:

    IEPNGFix.blankImg = dojo.moduleUrl("medryx.util.png", 'blank.gif').path;

    You might also want to search for “alert” and change it to console.warn instead, but that’s not mandatory!

  5. Open the iepngfix_tilebg.js file and add the following to the top line:

    dojo.provide("medryx.util.png.iepngfix_tilebg");
  6. Now on any app that you want to use transparent PNG, just add to your set of dojo.require statements:

    dojo.require("medryx.util.png");

A couple more side notes — I can’t seem to get background-repeat or background-position to work right with the *.js file. I haven’t had time to really troubleshoot it, since I don’t need it right now. If anyone has the solution, please let me know! A second note is that you should consider NOT using 1px images that are repeated, but instead use a more reasonable size, to limit the number of repeats. So if you know the smallest size of your display is 100px wide, then make your PNG 100px wide, and then repeat. Each repeat takes browser processing, and these days the download size of a 1px image and 100px image is not significant enough to make much of a difference.

Hope this helps someone! It has certainly helped me. Thanks Angus!

August 14, 2008

A Dojo Search Widget: dijit.Search

Filed under: dojo, javascript — Tags: , , , — maulin @ 6:33 am

Search has become ubiquitous on web sites and applications. Its long since replaced categorization or filing. On my new mac, I can’t remember if I have ever dug through my Applications folder looking for an app. I just type a few letters in Spotlight, and voila. On the dojo website its the same