====== 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.
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
* @copyright Mohd. Kamal Mustafa
*/
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 =====
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 =====
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 =====
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();
}
}
?>