Skip to content


WordPress MVC Plugin Development Part 1

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!

Posted in Code, WordPress.

  • Ivan

    Interesante, no va a continuar con este artнculo?
    Gracias

    • jasonnathan

      Thanks. I have been too busy.., but do miss my blog… :)

  • Denis Brat

    I’d like to know where you instantiate the ‘my_crm_main’ in order to make it load and execute the constructor procedures.
    Do you set an instance of it in the same php file after or before you declare the class, like this?

    class my_crm_main {

    }
    $my_crm_main = new my_crm_main();

    I don’t see a way to instantiate a plugin class in WordPress if not by these means, so I’m very curious to know how you made it.

    BTW, this is an awesome post, and I’ll make use of your example if you permit me.

    Thanks a lot!

    • http://jasonnathan.com Jason Nathan

      Hey Denis, thanks for your comment. The class is ‘auto loaded’ through the

      $request_handler

      . When you create your plugin menus, you’d have to define a callback function. In this case the callback function is

      public function request_handler()

      method in the example above. That method decides which of your controllers (classes) to instantiate.
      And of course you can use it!

  • http://mdimran.net imran

    hi ,
    i just try to use your router but it not work properly .
    when i use the following url is not go to the specific action
    localhost/wp2/test5/wp-admin/admin.php?page=?page=my_crm_opportunities&action=edit&id=1

    can you give me , your any sample plugin with the router ? it will be helpfull to understand your router . Please send me it ASAP .

    • http://jasonnathan.com Jason Nathan

      Hey Imran , sorry I don’t have an example right now. What problems are you having? Did you create the WP Admin menus and specify a callback function like I did above? Also, all the class files must be included in your plugin loader file. IE,

      include_once('my_class_file.php'):
  • Pingback: WordPress MCV Plugin Development Part 1 « Jason's Juvenilia