====== 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(); } } ?>