A simple Router for WordPress plugins
I was working on creating a simple CRM plugin for wordpress. My first plugin yet. I needed something simple and very specific to my work in real estate. After looking around, I didn’t find anything suitable – unsurprisingly.
So I set out to make this simple plugin. I divided the functionality in two parts. The first being basic CRM functions to view contacts, reminders & opportunities etc… The next part was to do with the more advanced portion of syncing, auto reminder templates. Auto emails and so on.
After creating my first single-handed website with Kohana 2.4, I realised the importance of an administrative backend. Not just any admin panel, something apt in both the management of the site AND adding new functionality.
Here’s where I learnt the point of CMSes, really only because I hate tiresome learning curves, and eventually – because I was using it already; WordPress.
Don’t get me wrong, I absolutely LOVE Kohana and am already trying to master Kohana 3 but that really is for another post. So before I digress from the intended short ( much shorter! ) intro, I’d better get started.
I have become too comfortable with the MCV approach where URL segments point to a method in your controller. After dissecting a few plugins, I realise WordPress doesn’t really have a straightforward router to decide which method or function runs so I decided to make a utility class that handles routing. It wouldn’t change how the URLs look but will at least automatically know what to run.
class Router { /** * Router setup routine. * * Construct takes a $route array which needs to have a controller name, * method (action) name * and possible arguments and tries to find the respective classes and * methods. * * If it can't find the controller, it returns the default controller and * default method - both defined * * If it finds the controller, it looks for the defined method, if not it * looks for the default method ( which also needs to be defined) and * supplies it with arguments if they exist. * * * @param array $route ( (str) controller, (str) method, (mixed) args ) * @param string $default_controller - the fallback class * @param string $default_method to use in all cases - the action * @return void */ public function __construct( $route, $default_controller, $default_method ) { /*try the route specified in $route, if not fallback to the $default_method*/ $try = $this->route_it( $route, $default_method ); /*if that didn't work out, then revert to the default controller*/ if( $try === false ) { return $this ->default_route_it( $default_controller, $default_method, $route[2] ); } } /** * The first step. Try the route specified in $route array * * * @param array $route( $controller, $method, $args) * @param string $default_method to use in all cases - the action * @return void */ public function route_it( $route, $default_method ) { $controller = $route[0]; // the array's first element $method = $route[1]; // the array's second element /*the extracted arguments. Probably other $_GET variables. we check if it is set and use it if it is*/ $parameters = ( isset( $data[2] ) ? $data[2] : NULL ); /*Checking if an instance of the controller supplied already exists*/ if( is_object( $controller ) and ( $controller instanceof $controller ) ) { /*we'll use the existing instance*/ $obj = $controller; } /*if not instanced, we have to check if it is a valid class*/ elseif( class_exists( $controller ) ) { /*and try to initialise it*/ $obj = new $controller(); } else { /*Nope! Class doesn't exist. Did we include the file? */ return false; } /*Now, for the method! Checking to see if it exists in the class*/ if( method_exists( $obj, $method ) ) { /*if it does, we return that method together with the arguments*/ return $obj->$method( $parameters ); } /*if not, we check if the default method exists*/ elseif( method_exists( $obj, $default_method ) ) { /*if it does, we return that method together with the arguments*/ return $obj->$default_method( $parameters ); } else { /*No luck! The supplied route didn't work*/ return false; } } /** * The second possibilty. Try the default controller. Functionality is * almost identical to the former. * Didn't combine it for readibility * * * @param string default controller name - the fallback class * @param string default method to use in all cases - the action * @param mixed arguments * @return void */ public function default_route_it( $controller, $method, $parameters = NULL ) { //echo "Falling back to defaults"; /*same thing, checking for existing instance*/ if( is_object( $controller ) and ( $controller instanceof $controller ) ) { // using existing instance $obj = $controller; } elseif( class_exists( $controller ) ) { /*using new instance*/ $obj = new $controller(); } else { return false; } /*since this is the default method, it really should exist! No sense in doing more checks...*/ if( method_exists( $obj, $method ) ) { // same thing again, returning the method with optional params return $obj->$method( $parameters ); } else { return false; } } }
Of course, this is a very basic class. Can probably enhance with error and / or exception handling. Could also autoload classes in the include path but I didn’t elaborate as I merely intended to share the basic idea behind the router.
How would you use this in the parent controller? I decided that the respective controllers could be defined in the
$_GET['page']
variable – since it is the way WordPress admin navigates through the individual plugin pages anyway.
And the method definition will be in the
$_GET['action']
variable.
Finally, the arguments can be the remaining $_GET variables.
To Illustrate:
class my_crm_main { public function __construct() { // the method that handles all requests $request_handler = array( $this, 'request_handler' ); // wordpress functions to add menus and submenus add_menu_page( __( 'MY CRM'), __( 'MY CRM'), 4, '?page=my_crm_main', $request_handler ); add_submenu_page( '?page=my_crm_main', __( 'Contacts' ), __( 'Contacts' ), 4, '?page=my_crm_main', $request_handler ); add_submenu_page( '?page=my_crm_main', __( 'Opportunities' ), __( 'Opportunities' ), 4, '?page=my_crm_opportunities', $request_handler ); add_submenu_page( '?page=my_crm_main', __( 'Reminders' ), __( 'Reminders' ), 4, // note the page names - used to create controllers later '?page=my_crm_reminders', $request_handler ); } /*The request handler function that declares the needed vars and calls the router*/ public function request_handler() { /*as mentioned, we use the page as the controller*/ $controller = $_GET['page']; /*and the action variable for the method*/ $action = $_GET['action']; // we add a small check to see if the page requested is this controller if( $controller == get_class( $this ) ) { // if it is, we can use the instance of this controller instead $controller = $this; } // now the params. All the other get variables $params = $_GET; // we can remove the page and action variables first unset( $params['page'] ); unset( $params['action'] ); // finally! let's set up data for the router $route = array( $controller, $action, $params ); // we are using the instance of this class as the default controller $default_controller = $this; // the default method - Kohana 2 style! $default_method = 'index'; $router = new Router( $route, $default_controller, $default_method ); } /* since this is the default controller, we should set up the default method here as well*/ public function index( $args = NULL ) { echo "Welcome to My CRM......."; } } // the opportunities controller class my_crm_opportunities { //action ''index'' public function index( $args = NULL ) { echo "The Opportunities Page"; if( ! is_null( $args ) ) { //do somthing with the data } } // action 'edit' public function edit( $args ) { if( $_POST and !empty( $args['id'] )) { // add the POST data the the db.. } } // action 'delete' public function delete( $args ) { // and so on .... } }
So, if wordpress admin points to mysite.com/wp-admin/admin.php?page=my_crm_main
The Controller my_crm_main() will be loaded and since no action is specified, it will run the default index() method.
If the url pointed to …admin.php?page=my_crm_opportunities&action=edit&id=1
The Controller my_crm_opportunities() will be loaded. The edit() method is run with the remaining $_GET args. In this case – the id…
Of course, you really ought to do the regular cleaning of post data and so on, but I hope this brief intro gives you an idea of how to implement a simple router for use with Controllers for your plugin development. We’ll discuss it in the comments!
Pingback: WordPress MCV Plugin Development Part 1 « Jason's Juvenilia