Table Helper in PHP

Custom built frameworks are always a zero-sum game. You get the freedom to build the framework completely suited to your business needs, but then you will more or less have to retread the bug ridden path of other frameworks. For our company currently, as an architect I have immense freedom to look at application from completely ‘techie’ viewpoint and keep building better and simpler solutions to make the lives of ouri developers a bit simpler.

PHP is an amazingly simple language to build webapps in. Though personally I am in favor of Python or even Ruby to build webapplications, the amount of workforce in PHP available in the market makes it an immensely favored language.

Anyways, back in my work, we have this interesting problem where we have to render hundreds of analytics reports. Pull from the DB and display the results in table format. To deal with this tedium of having to write innumerable presentation layer files to deal with table rendering we use a small function to render the tables. Its a pretty simple function that formats the data in neat table cells using

and encloses each of the resultset in the table rows.

. However, because of this limitation we had the presentation layer tainting the data with the presentation tags.

Hence the genesis of this unit test code.

require 'table_helper.lib.php';

################# FOR UNIT TESTING THE FUNCTION #######################
function url_logos($profile_id) {
  switch($profile_id) {
    case 1: return "abcde"; break;
    case 2: return "vwxyz"; break;
    default: return "NONE"; break;
  }
}

$Data = array(
  array('john', 'john carmack', 1, 75, '2008-12-1 00:0:00'),
  array('galt', 'john galt', 2, 2, '2008-06-06 06:06:06'),
  array('dagny', 'dagny taggart', 1, 180, '2008-11-00 00:0:00'),
  array('hank', 'hank rearden', 2, 200, '2008-06-02 06:06:06'),
  array('roark', 'howard roark', 1, 0, '2008-12-1 00:0:00'),
  array('john', 'john carmack', 1, 75, '2008-12-1 00:0:00'),
);
$Headers = array('Alias', 'Name', 'Profile', 'Games', 'Joining Date', 'Square');
$Templates = array(
  '"{$data[0]}"',
  '$data[1]',
  '"http://www.someurl.com/profile.php?id={$data[2]}&name={$data[0]}"',
  '"{$data[3]}"',
  '$data[4]',
  'url_logos($data[2]);'
);

TableHelper::Render($Headers, $Templates, $Data);

This led to the first version of my code for the TableHelper class which would simply use the PHP ‘eval()’ code the replace the Templates and fill it with data.

class TableHelper {
        /**
         * @func   Render($Headers, $Templates, $Data, $Prefix = 'data')
         * @desc   renders a table for the given data and the cell templates given.
         * @param1 $Headers a string array of table column headers
         * @param2 $Templates string of cell templates. The data bucket should be named 
         *         as data in the default case. If you think of any other name be sure
         *         to pass the correct prefix to the function.
         * @param3 $Data Data to fill the templates with.
         * @param4 $Prefix Default bucket for row data is 'data'. Can be anything else.
         * @note   This function now builds a template file instead of using eval.
         */
        static function Render($Headers, $Templates, $Data, $Prefix = 'data') {
                $NumCols = count($Headers);

                echo '< table>';
                echo '< tr>';
                        foreach ($Headers as $header)
                                echo '< th>'.$header.'< /th>';
                echo '< /tr>';

                foreach ($Data as $$Prefix) {
                        echo '< tr>';
                        for ($i = 0; $i < $NumCols; $i++) {
                                        eval("$_cell="$Templates[$i]";");
                                        echo '< td>',$_cell,'';
                        }
                        echo '< /tr>';   
                }
                echo '';
        }
}

As any seasoned programmer will tell you looking at the code, I was sacrificing speed (lots!) for want of better code separation by using eval(). After days of rumination I decided to use aggressive template caching in pphplace of eval(). And here’s the class with the caching mechanism built in…

# BEGIN Section Table Template Caching Section
define(CACHED_TEMPLATES_FOLDER, './cache/');

class TableHelper {
        /**
         * @func CachedFilePath($Id)
         * @desc The table template cached file is just named as unique id.tpl.php
         *       in the CACHED_TEMPLATES_FOLDER folder.
         */
        static function CachedFilePath($Id) {
                return CACHED_TEMPLATES_FOLDER.$Id.'.html.php';
        }

        /**
         * @func BuildTemplate($Id, $Headers, $Templates)
         * @desc Generates the php code for rendering the table and writes the
         *       table generation view code template to the cached folder.
         */
        static function BuildTemplate($Id, $Headers, $Templates) {
                assert(is_writeable(CACHED_TEMPLATES_FOLDER)) or die('Error THL001: Cache Folder has no Write Permissions.');

                $str = '';

                /* Build the Table Rendering PHP Code String */
                $str .= "< table>n";
                $str .= "< tr>n";
                        foreach ($Headers as $header)
                                $str .= "< th>$header< /th>n";
                $str .= "< /tr>n";
                
                $str .= '< ? php foreach ($Data as $$Prefix) { ?>';
                        $str .= "< tr>n";
                        foreach ($Templates as $template)
                                $str .= "< td>< ?= $template ?>< /td>n";
                        $str .= "< /tr>n";
                $str .= "< ? php } ?>n"; 
                $str .= "n";

                $handle = fopen(TableHelper::CachedFilePath($Id), 'w'); 
                        fwrite($handle, $str);
                fclose($handle);

                return true;
        }
        # END Section Table Template Caching Section

        /**
         * @func   Render($Id, $Headers, $Templates, $Data, $Prefix = 'data')
         * @ver    1.2
         * @desc   renders a table for the given data and the cell templates given.
         * @param1 $Headers a string array of table column headers
         * @param2 $Templates string of cell templates. The data bucket should be named 
         *         as data in the default case. If you think of any other name be sure
         *         to pass the correct prefix to the function.
         * @param3 $Data Data to fill the templates with.
         * @param4 $Prefix Default bucket for row data is 'data'. Can be anything else.
         * @note   This function now builds a template file instead of using eval.
         */
        static function Render($Id, $Headers, $Templates, $Data, $Prefix = 'data') {
                $NumCols = count($Headers);

                /* Attempt to just include the cached template file for this table */
                $cached_file_path = TableHelper::CachedFilePath($Id);
                if(!file_exists($cached_file_path)) 
                        TableHelper::BuildTemplate($Id, $Headers,$Templates);
                
                if(file_exists($cached_file_path)) {
                        include_once $cached_file_path; return;
                }

                /* If Cached File cannot be created, just do the EVAL way! */
                echo '< table>';
                echo '< tr>';
                        foreach ($Headers as $header)
                                echo '< th>'.$header.'< /th>';
                echo '< /tr>';

                foreach ($Data as $$Prefix) {
                        echo '< tr>';
                        for ($i = 0; $i < $NumCols; $i++) {
                                        eval("$_cell="$Templates[$i]";");
                                        echo '< td>',$_cell,'< /td>';
                        }
                        echo '< /tr>';   

                }
                echo '< /table>';
        }
}

Catches
The only dependency with this code is that you have to create a Apache writeable folder referenced by the CACHED_TEMPLATES_FOLDER. After that its just fun all the way! Also note that the call to the Render in the unit testing class will now have to be supplied a unique Template Id.

TableHelper::Render($Id, $Headers, $Templates, $Data);

Tagged: Tags

Leave a Reply

Your email address will not be published. Required fields are marked *