WordPress MVC Plugin Development

Posted

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 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 website with Codeigniter, 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 because I was using it already; WordPress.

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) && ($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) && ($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;
    }
  }
}

This is a very basic class. Can probably enhance with error and 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
    // note the page names - used to create controllers later 
    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, '?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 something 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.