Writing /home/k4ml/webapps/drupal5/wiki/data/cache/b/bb54cfa417a8faaa3c749613b4060ad0.i failed
Writing /home/k4ml/webapps/drupal5/wiki/data/cache/b/bb54cfa417a8faaa3c749613b4060ad0.i failed
Writing /home/k4ml/webapps/drupal5/wiki/data/cache/b/bb54cfa417a8faaa3c749613b4060ad0.xhtml failed
Table of Contents

web.py style request dispatcher, in PHP

The concept is simple, request such as GET http://localhost/customer/23 will call method GET of Customer object, passing any pattern match as arguments to that method. Clean urls were achieved using Drupal style mod_rewrite rules:-

  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

which rewrite url such as http://localhost/customer/23 to http://localhost/index.php?q=customer/23. The same applies to POST request and it would be nice if there’s http client that support DELETE and PUT method.

Reminder: examples below depends heavily on phphtmllib and adodb.

<?php
/**
 * web.py <http://webpy.org/> style request handler
 *
 * "The third principle is that web.py should, by default,
 * do the right thing by the Web. This means distinguishing
 * between GET and POST properly. It means simple, canonical
 * URLs which synonyms redirect to. It means readable HTML
 * with the proper HTTP headers." - Aaron Swartz, web.py author.
 *
 * This class just act as a namespace selector.
 * PHP currently do not support namespace, so this
 * is one way to live without it although it doesn't
 * solve the real problem since all class were loaded
 * to global namespace, rendering it useless other than
 * simple reminder this function coming from this class.
 * All methods were invoked statically.
 *
 * @author Mohd. Kamal Mustafa <kamal@k4ml.com>
 * @copyright Mohd. Kamal Mustafa <kamal@k4ml.com>
 */
class web {
    function web() {
        die("This is not a real class to be new()ed");
    }
 
    /**
     * Take an array of urls pattern to callback, call
     * the callback if matched and passed the arguments.
     */
    function run($urls) {
        // urls must be balanced, 'pattern => callback'
        if (count($urls) % 2 != 0) {
            die('not balanced');
        }
 
        $mapping = array_chunk($urls, 2);
 
        $request_url = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/index';
 
        $not_found = False;
 
        $http_methods = array(
            'GET' => '_run_GET',
            'POST' => '_run_POST',
            'PUT' => '_run_PUT',
            'DELETE' => '_run_DELETE',
        );
 
        // refactor to array mapping ??
        foreach ($mapping as $map) {
            if (preg_match($map[0], $request_url, $matches)) {
                $obj = web::_load_object($map[1]);
                $method = strtoupper($_SERVER['REQUEST_METHOD']);
                if (array_key_exists($method, $http_methods)) {
                    call_user_func(array('web', $http_methods[$method]), $obj, $matches);
                    $not_found = False;
                    break;
                } else {
                    header('HTTP/1.0 501 Method not supported');
                    die();
                    break;
                }
            } else {
                $not_found = True;
            }
        }
 
        if ($not_found) {
            print 'Not Found';
        }
    }
 
    function _run_GET($obj, $matches) {
        if (method_exists($obj, 'GET')) {
            call_user_func_array(array($obj, 'GET'), array_slice($matches, 1));
        } else {
            die('method not implemented');
        }
 
    }
 
    function _run_POST($obj, $matches) {
        if (method_exists($obj, 'POST')) {
            call_user_func_array(array($obj, 'POST'), array_slice($matches, 1));
        } else {
            die('method not implemented');
        }
    }
 
    function _run_DELETE($obj, $matches) {
        if (method_exists($obj, 'DELETE')) {
            call_user_func_array(array($obj, 'DELETE'), array_slice($matches, 1));
        } else {
            die('method not implemented');
        }
    }
 
    function _load_object($module) {
        //split $classname to get the filename
        list($fname, $classname) = explode('.', $module);
        if (defined('APP_ROOT')) {
            $filename = APP_ROOT.'lib/'.strtolower($fname).'.inc.php';
            if (file_exists($filename)) {
                require_once $filename;
                $obj = new $classname;
            }
 
            // also load the models, if exists
            $filename = APP_ROOT.'lib/models/'.strtolower($fname).'.inc.php';
            if (file_exists($filename)) {
                require_once $filename;
            }
 
            if ($obj) {
                return $obj;
            }
        }
    }
 
    function commit() {
        if (isset($GLOBALS['db'])) {
            $GLOBALS['db']->CompleteTrans();
        }
    }
}
?>

Directory Layout

Normally I would layout my php apps to something like this:-

 $ ls apps
 lib/ www/ app.inc.php config.ini web.inc.php
 $ ls lib
 models/ forms/ page/ customer.inc.php
 $ ls www
 index.php

index.php

<?php
require_once '../app.inc.php';
require_once APP_ROOT.'/web.inc.php';
require_once APP_ROOT.'/lib/page.inc.php';
 
//$GLOBALS['db']->debug = True;
 
$urls = array(
    '/customer\/(\d+)\/order\/(\d+)/', 'Customer',
    '/order/', 'Order',
    '/item/', 'Item',
    '/customer/', 'Customer',
);
 
web::run($urls);
?>

app.inc.php will bootstrap the application which include initializing database connection (riding on adodb) and set the APP_ROOT constant. Observe the patterns, actually that was ugly since we need to escape the slash, compared to web.py urls tuple:-

url = (
    '/customer/(\d+)/order/(\d+)', 'Customer',
    '/order', 'Order',
)

customer.inc.php

<?php
class Customer extends Page {
    function GET() {
        $container = new Container;
 
        $customers = $GLOBALS['db']->getall(
            'select * from customers'
        );
        $table = new InfoTable('Customers');
        $table->add_row(html_b('Name'), html_b('Address'), html_b('Action'));
        foreach ($customers as $row) {
            $table->add_row($row['name'], $row['address'], 'None');
        }
        $container->add($table);
 
        require_once APP_ROOT.'lib/forms/customer.inc.php';
        $form = new FormProcessor(new forms_customer('Customer'), 'customer', '/customer');
        $container->add($form);
        $this->page->add($container);
        print $this->render();
    }
 
    function POST() {
        $customer = dbo::get_or_create_row('models_customer_table');
        require_once APP_ROOT.'lib/forms/customer.inc.php';
        $form = new FormProcessor(new forms_customer('Customer'), 'customer', '/customer');
        $this->page->add($form);
        if ($form->is_action_successfull()) {
            web::commit();
            http_redirect('customer');
        } else {
            print $this->render();
        }
    }
}
?>

page.inc.php

<?php
class Page{
    var $page;
 
    function Page() {
        $this->page = new HTMLPageClass();
        $this->page->add(html_h1('Clinic Management System'));
        $this->page->add_css_link('/css/default.css');
    }
    function render() {
        print $this->page->render();
    }
}
?>