13th March 2006

Posted in

Blog :: Improvements to Structures_Datagrid

One of the PEAR packages I have a love/hate relationship with is Structures_Datagrid. Structures_Datagrid actually provides a lot of functionality; you can bind a data source (an array, a DB_result or DB_DataObject or even an xml, csv, or RSS file) and then use a renderer to output the data. Current renderers include HTML Tables, CSV, XUL, and so forth.

I tend to use the simplest incarnation of datagrid: pass it an array or db_result and get an HTML table back. Even for such simple usage it frequently saves me LOC (and I'm a devout believer in the lesscode gospel). Sample code for such simple usage might be:


$dg =& new Structures_DataGrid();
$db_result = $db->query('SELECT * FROM username');
$dg->bind($db_result);
echo $dg->renderer->toHTML();

which produces

id uname email
1 joe joe@gmail.com
2 Sara sg@yahoo.com
2 leet8 coder@poseur.net

 

Boom! You've got a HTML table from an sql query in 4 lines of code. The table automatically supports sorting (if I wasn't cutting'n'pasting this in from the sample script a click on a the title of a column would reload the page with the appropriate sort applied) and with a small addition it will automatically handle paging the data as well. This sort of functionality is a recurring need and it's great that there is a package that provides it.

Unfortunately (here's where the hate part comes in) there are some very poor design choices in Structures_Datagrid that limit its utility. Consider doing something slightly more complicated than the default example. Frequently I want to change the column headings and format the contents of the columns slightly. By default the HTML renderer prints the name of the column as the column heading; perhaps in the example above I want to change the column named 'uname' in my 'users' table to 'Username'. Additionally I'd like to print a link to each user's home page rather than simply printing out the user name.

Fortunately Structures_Datagrid allows you to specify details about the columns. You can specify the Column name (as distinct from the field name) and specify additional details like a different sorting column. Finally you can use "formatters" to specify a callback function to generate the output for a column. The following code does what I want (line numbers added):


1 $dg =& new Structures_DataGrid();
2 $array = $db->query('SELECT * FROM users');
3 $dg->bind($array);
4 function userLink($r){return "<a href=\"/~{$r['record']['uname']}/\">{$r['record']['uname']}</a>";}
5 $dg->addColumn(new Structures_DataGrid_Column('id', 'id', 'id'));
6 $dg->addColumn(new Structures_DataGrid_Column('Username', 'uname', 'uname', null, null, 'userLink()'));
7 $dg->addColumn(new Structures_DataGrid_Column('Email', 'email', 'email'));
8 echo $dg->renderer->toHTML();

Line 4 defines a function I'm going to use as a formatter. It is passed an array with only one index ("record") which represents the row of data I'm working on, and it returns a string that will be used to fill the column it formats. On line 7 I add this to the username column, specifying seperate Column headings ("Username") and field name/sorting column ("uname") as desired and passing the formatter as the last argument. Structures_Datagrid requires you to either handle all your columns automatically or handle them all manually, so I add back in the email and id column on line 6 and 8.

My code is still pretty compact, but already two of the features of Structures_Datagrid that irritate me are on display. Having to manually add the email and id columns is a little annoying and only gets worse when I'm building a 10 or 20 column table. Ideally Structures_Datagrid would autmatically create all the column definitions for me and I could replace or delete them as required.

Another annoyance is the way in which the formatters are specified. I googled and discovered others have already commented on this issue, but basically the package developer has committed the cardinal sin of recreating a base type. PHP has callback as type by convention (there is no function "type" in PHP) as documented on the official site. Basically the rules are: functions are passed as the string name of a function or as an array of (object, "method") or ("staticClassname", "methodname") respectively. Structures_Datagrid, however, invents a new way of passing callbacks that requires you to embed parentheses in the string after the function name and allows passing arguments to the function by embedding them in the format string like "myfunc($arg=mydata, $arg2=5)" I think.

This is a not a good idea for a number of reasons. First, it is likely that this format string passing approach will be fragile since the format string will be parsed in user code. Even if it's robust enough, however, this is the only place such a pattern is used in any php library I have looked at. That means the cost of using Structures_Datagrid is remembering a little format pattern that isn't useful to me in any other place. This is too high a cost. The advantages of using a parsed format string instead of standard callbacks is that you can generate closures. There are,however, other ways of achieving the same effect.

Despite these flaws I appreciate and use Structures_Datagrid. To make things easier on myself, however, I finally decided to smooth out some of these rough edges. I've extended both the Structures_Datagrid and the Structures_Datagrid_Column classes to add the following features:

 

We can simplify our previous example now:


1 require_once 'Ext_Structures_DataGrid.php';
2 $dg =& new Ext_Structures_DataGrid();
3 $array = $db->query('SELECT * FROM users');
4 $dg->bind($array, array('allowhtml'=>true));
5 function userLink($r){return "<a href=\"/~{$r['record']['uname']}/\">{$r['record']['uname']}</a>";}
6 $dg->replaceColumn(new Ext_Structures_DataGrid_Column('Username', 'uname', 'uname', null, null, 'userLink'), 'Uname');
7 echo $dg->renderer->toHTML();

Here is a link to the source for Ext_Structures_Datagrid and Ext_Structures_Datagrid_Column. Opinions are welcome; perhaps I should try to get some of these features incorporated in Structures_Datagrid on PEAR (development looks to be stalled with no release since October 2005). I might mention as well that while I've harshed on the design decisions of Structure_Datagrid, it's a really large codebase that does a lot of useful stuff. Perhaps the author doesn't have a lot of experience with PHP; aside from not using PHP callbacks, the code contains idioms like $this->columnSet = array_merge($this->columnSet, array($column)); where I might have used $this->columnSet[] = $column;. Whatever the case, Structures_DataGrid has been useful to me and in that spirit I offer my improvements.

Update: Arno says that there is a refactoring of Structures_Datagrid in progress by the maintainers. This is good news (and I see in the CVS that there is some code activity going on.) Arno also thinks I'm a little rude. This is certainly a possibility and I've slightly edited the text above to state things in a less inflammatory way. I want to reiterate that I'm impressed by all the work that's gone into making Structures_DataGrid useful. My post is an attempt to educate future writers of libraries about the needs of their users (don't reinvent the wheel!) and possibly encourage other's use of this package by slightly extending it... Lastly Arno pointed to his Smarty/Structures_DataGrid integration work. I don't know French but it looks like good stuff...

Update: Arno now has an english translation of his Smarty/Structures_DataGrid work.

Posted on March 13th 2006, 12:40 PM