Make your own WordPress MVC Plugin

One issue I have with WordPress is the way it stores all of its pages in the database as posts; this can make it very slow and cumbersome to use more "modern" workflow techniques. It also forces you to build more complicated pages in less desireable ways, via hooks or piling a great deal of unrelated code together in a single functions.php file. Fortunately, WP is also very flexible in what it allows you to do - such as make your own MVC framework as a plugin. Let's take a look at how.

Do a standard WP setup (we just want to demo this idea for now) and install the plugin "WP Router"

TERRIBLY IMPORTANT STEP: Set permalinks to "Post name" and save.

WP Router comes with a working sample, so take a moment to check http://mysite.com/wp_router/my_sample_path and make sure a smaple page displays. Once everything is in working order, we are ready to get into our real code.

The first thing we want to do is have a look at what is rendering this page. In your WP Router plugin directory, open up the WP_Router_Sample.class.php file and let's have a look.

The init hook is simply used to set up the working sample - we'll do this a bit differently. The generate_routes() is what we want to look at more closely - namely, the path and page_callback keys (sorry, I'm going to make you read the README to understand everything about this). Our path is the route we will use to access our page - so '^wp_router/(.*?)$' translates to "/wp_router" plus a value. When that url is called, the sample_callback() method of this class is called and its output is added as the $wp_content. This is nice in that your theme template still functions normally. If you have used a more traditional MVC framework, wp_router would be your controller and sample_callback the function.

This is how the router works, but this setup really isn't very nice. Let's work with this to create our own internal MVC framework that works within Wordpress, letting us leverage all the WP theme and admin tools but giving us complete control over our app.

First of all, we want to work within our own plugin. Since this is our application itself and not a reusable plugin, we'd like to it load early so we'll put it in mu-plugins directory. Someday we'll want to buld this out much more fully, but for today we are staying very simple. Sol let's add two things - a loader file and our main application bootstrap file.

// mu-plugin/loader.php

require_once( 'app/bootstrap.php' );

// mu-plugin/app/boostrap.php

// ...leave empty for the moment

Our loader will be for nothing more than to be picked up automatically by Wordpress and send further include instructions about our app. Our bootstrap will actually load all of our classes. This allows us to keep our app files together as a single unit, which can be a startin gpoint for other sites.

Let us set up our new route. We are going to potentially have many of these, so let us make routes its own class where we can manage them all. In our apps directory, create file:

//mu-plugins/app/routes.php

Class Routes {

	static function load_routes()
	{
		// list all the routes here
        add_action( 'wp_router_generate_routes', array( get_class(), 'show_test_page' ), 20 );

    }
    
    static function show_test_page(WP_Router $router ) {
        $args = array(
            'path' => '^test',           
            'page_callback' => array( get_class(),'show_test' ),
            'access_callback' => true,
            'title' => 'Test Page',
            'template' => array('page.php', dirname(__FILE__).DIRECTORY_SEPARATOR.'page.php')
        );

        $router->add_route( 'show-test_page-route', $args );
    } 

    static function show_test()
    {

    	echo 'We are here!';
    }
}       

You'll see we've essentially moved all our sample page code over here. Now access at /test and ...Nothing? We haven't actually called this class, so none of the routes are registered. Now is time for our bootstrap.

//mu-plugins/app/bootstrap.php

require_once 'routes.php';

Routes::load_routes();	

We aren't using any type of autoloader here, so we'll just call it the old fashioned way. Now we have a class that handles all our routing that we can add to and improve over time. But we still have a bit of ugliness - all of our controller and view logic is stuck in this same file. So let's move that out.

//mu-plugins/app/controllers/test.php

Class Test
{
	static function show_test()
    {
    	echo 'We are here!';
    }
}	

Now when we call our route, it is gonig to break - we are still looking for show_test() on the Route class. We need to tell it to look in our new class:

//mu-plugins/app/routes.php	

...
'page_callback' => array( 'Test','show_test' ),

// and we'll need to include this file, so

//mu-plugins/app/bootstrap.php	

require_once 'routes.php';
require_once 'controllers/test.php';

Routes::load_routes();

Much better! We load a route class with finds the appropriate controller, which handles our output. Separating out our concerns, making it all cleanand easy to work with.

However.

The one thing I really hate that I see in WordPress all the time is mixing the html in the same files as the business logic. Since we've managed things so cleanly up till now, it should be easy to create separate view files for the actual html. Especially since we don't even have to write it ourselves!

If you've never used Composer, it is time. Whether or not it becomes your tool of choice, it is no longer something you can simply ignore. If you are not completely comfortable with it, pop over to Juan Treminio's excellent introduction which will explain everything you need to know for day to day use in about 5 minutes.

We are going to use the Plates PHP template system as it is framework agnostic and very simple to get started with. So inside your /app directory, download the composer.phar and add the composer.json file:

/app
   /controllers
   boostrap.php
   composer.json
   composer.phar
   routes.php

// composer.json

{
  "require": {
    "php": ">=5.3.0",
    "league/plates": "1.0.*"
  }
}

Once you've run "composer.phar install", you'll see your vendor directory. This is fantastic! This means any php component library out there can easily be added to our plugin - not just WP code. We can bring in form builders, validation, payment gateways...whatever we need.

First, however, we'll need to take once step to include it all:

//mu-plugins/app/bootstrap.php

require_once 'routes.php';
require_once 'vendor/autoload.php';
require_once 'controllers/test.php';

Routes::load_routes();	

Home stretch - hang in there! We are as a last setof steps going to create a directory for our view and add a file, add a generic function to call the template engine, and then change our controller function to use the template to render. Here we go:

//app/mu-plugins/controlelrs/test.php

Class Test
{
	static function show_test()
    {
    	$user = 'Jeff Madsen';

        $data = 
            array(
                'user'   =>$user,
        );

        $template = self::make_template();

        $template->data($data);

        return $template->render('views/test_view');
    }

	// this can be moved to a parent controller for reusability
    static function make_template()
    {

        $views_dir = dirname(dirname(dirname(__FILE__))) . '/app'; 

        $engine = new \League\Plates\Engine($views_dir);

        // Create a new template
        $template = new \League\Plates\Template($engine);

        return $template;
    }

}

//app/mu-plugins/views/test_view.php

echo $this->user;	

Visit /test and you should see "Jeff Madsen" print to the content area.

Some final words: This was done in a very quick, demo way. In my company we have actually built out a whole mini-framework around this concept, and are using it with several projects combined with Custom Post Types. You should play with this, and then start to think about better ways to group your files and functions to suit your needs.

Hope that helped!

Contact me