Skip to content
This repository

MTV is a plugin for WordPress that provides a new API for developing plugins and themes. Born out of frustration with the undocumented, inconsistent WordPress API, MTV provides a simple, familiar, consistent way to develop heavily customized WordPress sites.

MTV borrows a lot from existing MVC-style frameworks, namely Django and Backbone.js. If you're familiar with those frameworks, you should feel at home using MTV. If you're not familiar, go try some tutorials. It'll make you a better programmer.

This plugin hijacks and takes over how WordPress handles URLs and templates, and gives you new ORM-style tools for handling posts, users and blogs. This plugin does nothing by itself, and (hopefully) will not break stuff that you already have. It's just a set of tools for developers to use.

Why?

The way that WordPress handles requests and serves responses is not clear to anyone who is not well acquainted with the internals. This is fine for theme development and for simple plugin development. Great, actually. Create template files in the correct spot with the right names, use the magical template tags in the right places in the right files, and you can quickly build a WordPress theme with the structure and style you want.

The problem with this, and the genesis of this project, is that once you try to step outside the lines of the standard theme and plugin development, the simplicity of WordPress unravels frighteningly quickly.

WordPress is magic. When you want to setup a quick blog, make it your own and start writing, WordPress is magic. But when you want to heavily customize it, adding post types, new URLs and rivers for those post types, new widgets — the list goes on and on — WordPress' magic goes awry.

We love WordPress, but we need it to be more serviceable. We need to be able to look under the hood and figure things out quickly. We need a code base that is simple, explicit, legible and self-documenting. That's what we're trying to do with MTV.

Contents

Getting started

Check out Heather Billings' tutorial on putting together a theme with MTV.

Basics

MTV is built around the concept of apps, an idea that comes from Django. An app is package of models, views, templates and other code that can mostly stand alone.

This is what a MTV app looks like:

myapp/
    urls.php
    views.php
    models.php
    templates/
        base.html
        index.html
    templatetags/
        functions.php
        tags.php
        filters.php

These are all the things MTV will look for. You don't need to add the files you're not going to use.

register_app \mtv\register_app( $name, $path_to_app )

This makes MTV aware of an app. This function doesn't load the app. It simply names the app for later reference.

run \mtv\run( $args )

run makes MTV go. It takes an array containing three things: the current URL, an array of url patterns and their associated views, and an ordered array of apps to load.

The MTV WordPress plugin uses run internally. Here's how it works:

  • Checks to see if the currently active theme has a urls.php file. If the theme does not have a urls.php, but it has a parent theme, MTV looks for a urls.php file in the parent theme.
  • Loads the $url_patterns variable from the urls.php file.
  • Looks for a global variable named $apps.
  • Calls run with $url_patterns, $apps, and $_REQUEST['path'] as the URL.
  • run loads all the code for the selected apps and configures the Twig template library.
  • run matches the url to one of the url patterns and executes the associated view function.
  • Internets!

If you're using the MTV plugin for WordPress development, you should never need to use this function directly. The more you know...

load \mtv\load( $apps )

If you ever need to use your models views or templates, but don't want to run MTV, use load. You pass it an array of apps to load, and it includes the code and configures the Twig template library but doesn't execute any views.

The model layer

The model layer is a collection of classes which handle the retrieval and manipulation of data. Models are objects which represent a single item or row from a database.

In MTV, two types of classes comprise the model layer: Model and Collection. A Model represents a single item from the database; a post, user, category, etc., and contains all the logic for accessing data, saving to the database, validating changes, and retrieving related models or data. A Collection represents an array of a single type of Model and contains all the logic needed to retrieve and filter one or more models from the database, as well as logic to work on multiple models at once.

Model classes are stored in the models.php file of the MTV plugin.

\mtv\models\Model

Constructor new Model( [$attributes] )

The constructor creates a real live model for you to use. It takes one optional parameter, an associative array of attributes to attach to the model.

$data = array(
    "title" => "My blog post",
    "published_on" => now(),
    "author" => "Bobby Tables",
    "content" => "I can blog!!"
);
$model = new Model($data);

print "<h1>" . $model->title . "</h1>";
print "by " . $model->author;
print "<p>" . $model->content . "<p>";

save $model->save()

Save this model to the database.

$model = new Model();
$model->title = "My blog post";
$model->save();

The base Model class does not implement save. It's up to you to extend the Model class and write this function.

validate $model->validate()

Validate the attributes in this model. Call this before saving the model to verify that all of the attributes are correct.

$model = new Model();
$model->email_address = "bobby";
$model->validate();
// ERROR! "bobby" is not a valid email address

The base Model class does not implement validate. It's up to you to extend the Model class and write this function.

initialize $model->initialize( $attributes )

Process the attributes when creating a new model. If you need to do something when a model object is created, you can implement this method instead of overriding the constructor. The base Model class does not implement this.

clear $model->clear()

Delete all of the attributes on the model.

set $model->set( $attributes )

Takes an associative array of attributes and sets them on the model.

$data = array(
    "title" => "My blog post",
    "published_on" => now(),
    "author" => "Bobby Tables",
    "content" => "I can blog!!"
);
$model = new Model($data);

$model->set(array(
    "title"=>"Better title",
    "status"=>"draft"
))

print $model->status; // draft
print $model->author; // Bobby Tables
print $model->title; // Better title

fetch $model->fetch()

Populates the model's attributes from the database.

The base Model class does not implement fetch. It's up to you to extend the Model class and write this function.

parse $model->parse( $data )

Parses the raw data from the database and returns an associative array of attributes for the model.

reload $model->reload( $data )

Takes a variable containing raw data from the database and loads it into the model's attributes. reload calls parse then set.

to_json $model->to_json()

Returns an associative array containing a subset of the model's attributes that are appropriate to send over the wire as json. Does not return an actual JSON string.

to_json uses an variable called json_fields to determine which attributes it should return.

$data = array(
    "name" => "Bobby Tables",
    "birthday" => "02/01/00",
    "secret_desire" => "space man"
);
$model = new Model($data);

$model->json_fields = array('name', 'birthday');

print json_encode($model->to_json()) // only shows name and birthday

from_json Model::from_json( $data )

set_from_json $model->set_from_json( $data )

The reverse of to_json, takes an associative array of attributes, filters and sets them on the model. Does not accept an actual JSON string.

set_from_json uses an variable called editable_json_fields to determine which attributes it should set.

$json = '{
    "name": "Bobby Tables",
    "birthday": "02/01/00",
    "secret_desire": "space man"
}'
$model = new Model();

$model->editable_json_fields = array("name", "birthday");

$model->set_from_json(json_decode($json));

print $model->name; // Bobby Tables
print $model->secret_desire; // Null

from_json is a static function that you can use to create a new model object from an array

$json = '{
    "name": "Bobby Tables",
    "birthday": "02/01/00",
}'

$model = Model::from_json(json_decode($json));

print $model->name; // Bobby Tables

Extending Model

You can extend a Model like you would any PHP class. When extending a model there are a few variables you might be interested in.

class MyModel extends Model {
    // Default model data used when a new model is created
    public $defaults             = array();
    // Attributes that are OK to send over the wire
    public $json_fields          = array();
    // Attributes that can be updated over the wire
    public $editable_json_fields = array();
}

\mtv\models\Collection

A Collection represents a bunch of models. It has only a few functions but it acts as the gateway to retrieving models. You can treat a Collection object as an array.

Constructor $collection = new Collection( [$attributes] )

add $collection->add( $model )

clear $collection->clear()

to_json $collection->to_json()

get $model = Collection::get( $attributes )

get_by $model = Collection::get_by( $attributes )

filter $collection = Collection::filter( $attributes )

Extending Collection

You can extend a Collection like you would any PHP class. There is only one thing you absolutely must put in your new class.

class MyModelCollection extends Collection {
    public static $model = 'myapp\models\MyModel';
}

The static variable $model must be the globally accessible name of the model class that this collection will be handling.

Included WordPress Models

WordPress models are stored in the wp/models.php file of the MTV plugin.

\mtv\wp\models\Post

The Post model represents a WordPress post in the database. It inherits and implements all of the features of the Model class.

Here are the typical WordPress post fields you'll find on a Post object:

id             // ID of the post
post_author    // ID of the post author
post_date      // timestamp in local time
post_date_gmt  // timestamp in gmt time
post_content   // Full body of the post
post_title     // title of the post
post_excerpt   // excerpt field of the post, caption if attachment
post_status    // post status: publish, new, pending, draft, auto-draft, future, private, inherit, trash
comment_status // comment status: open, closed
ping_status    // ping/trackback status
post_password  // password of the post
post_name      // post slug, string to use in the URL
to_ping        // to ping ??
pinged         // pinged ??
post_modified  // timestamp in local time
post_modified_gmt // timestatmp in gmt tim
post_content_filtered // filtered content ??
post_parent    // id of the parent post. If attachment, id of the post that uses this image
guid           // global unique id of the post
menu_order     // menu order
post_type      // type of post: post, page, attachment, or custom string
post_mime_type // mime type for attachment posts
comment_count  // number of comments
filter         // filter ??

The Post model also adds these fields, not typical to WordPress:

post_meta      // an array containing all of the meta for this post
post_format    // the post_format of this post
blogid         // id number of the blog this post lives on
url            // attachments only, url of the original uploaded image or whatever
thumb_url      // attachments only, url of the thumbnail image, if thumbnails are enabled

With the exception of the last three fields above (blogid, url, thumb_url), you can read, update and save any field using the Post model:

$post = new Post(array('id'=>1));
$post->fetch();

$post->post_status = 'publish';
$post->post_title  = 'Better headline';
$post->post_meta['my_special_meta'] = 'this post is special';
$post->post_meta['my_related_items'] = array(2,5,7);

$post->save();

The Post model also has these extra special functions:

password_required $post->password_required()

is_sticky $post->is_sticky()

post_class $post->post_class( [$extra_classes] )

permalink $post->permalink()

categories $post->categories()

tags $post->tags()

the_time $post->the_time( [$format] )

the_date $post->the_date( [$format] )

make_excerpt $post->make_excerpt( [$more_text] )

\mtv\wp\models\PostCollection

Your gateway to the wonderful world of WordPress posts. PostCollection only implements the basic features set out in the Collection above. With a couple twists, PostCollection really is just a thin wrapper around WordPress's WP_Query object.

$query = array(
    'post_type' => 'post',
    'posts_per_page' => 10,
    'order' => 'DESC'
);
$posts = PostCollection::filter( $query );

foreach ( $posts as $post )
    print $post->post_title;

The PostCollection object ($posts in this example), holds on to the WP_Query object for later reference. So if you need to see how many pages of posts you have:

$max_pages = $posts->wp_query->max_num_pages;

Not too shabby.

\mtv\wp\models\User

The User model represents a WordPress user in the database. It inherits and implements all of the features of the Model class.

\mtv\wp\models\UserCollection

\mtv\wp\models\Site

The Site model represents a WordPress network blog in the database. It inherits and implements all of the features of the Model class. This will only work if you've install MTV on a network install.

\mtv\wp\models\SiteCollection

The view layer

The view layer is the switching yard for all the requests your crazy WordPress/MTV website will receive. It's made up of two files: urls.php and views.php. Every MTV app can have a views.php and a urls.php

urls.php

This file contains a single variable. Not a global variable, mind you, we're trying to keep those to a minimum. MTV includes your urls.php file only when it needs to read the url patterns.

$url_patterns = array(
    "URL_REGEX" => "FUNCTION_CALLBACK"
)

That's all there is to it. $url_patterns is an associative array where the key is a regular expression and the value is the name of the function to execute.

MTV's url resolver (the function that matches the current url to a url pattern) reads the $url_patterns in order and uses the first view that matches.

The url resolver will also pass interesting bits of the matched url into your view function.

$url_patterns = array(
    '/(?P<slug>[^\/]+)\/?$/' => 'myapp\views\page'
)

In the regex above, we capture part of the url using parentheses and name that captured part slug: (?P<slug>[^\/]+). When our url resolver executes our view function, it gives the view an array of the captured bits.

function page( $request ) {
    print "I'm on page " . $request['slug'] . "!";
}

Fun fun.

Including urls

If you have multiple apps, you will probably want to have a urls.php in each app and separate stuff out. Maybe not, but we've found a need for it.

The MTV WordPress plugin will load the urls from your active theme, so what if you want to use urls from another app?

include_urls_for \mtv\http\include_urls_for( $app )

Include url patterns from another registered app.

$url_patterns = array(
    \mtv\http\include_urls_for('myotherapp'),
    '/(?P<slug>[^\/]+)\/?$/' => 'myapp\views\page'
)

Again, order is everything.

views.php

This is where we put our view functions. Functions that process users' requests, prepare data, and generate a response.

View functions are dead simple:

<?php // views.php
namespace myapp\views;

function page( $request ) {
    print "Hello page!";
}

View functions do not return anything. They print output and exit. ASIDE: We should probably return the response and output it somewhere else like other fancy frameworks, but this is simple and works.

Couple important things: $request is an associative array containing data captured by the url resolver see above. And you really should use a namespace. You don't have to, but you really, really should.

Shortcut functions for your views

display_template \mtv\shortcuts\display_template( $template_name, $context )

display_json \mtv\shortcuts\display_json( $data )

require_capability \mtv\shortcuts\require_capability( $cap, $kwargs )

set_query_flags \mtv\shortcuts\set_query_flags( $views )

Errors in your views

Problems are going to happen, your views are not always going to work, you'll have to handle errors and missing data. Luckily MTV has some neat tricks for relaying the bad news to your users: Exceptions!

MTV will catch any exceptions that get generated by your misbehaving view and display a an error page. If you want to force an error page, throw a Http500 exception. If some data was not found, you can make MTV generate a 404 page by throwing a Http404 exception. If you're writing an API you can tell MTV to generate a json formatted error response, throw a AjaxHttp404 or a AjaxHttp500.

/mtv/http/HttpException

Base class for all the HTTP exceptions.

constructor throw new HttpException( [$message], [$error_data] )

display_header $exception->display_header()

Set the http header depending on value of $this->code.

display_message $exception->display_message()

Prints out the value of $this->message. This should be overridden to display something nicer.

display $exception->display()

Calls $this->display_header(), then $this->display_message(). Futuristic.

/mtv/http/Http404

Extends HttpException. Throwing this will cause MTV to generate a 404 error page.

When displayed, tries to load the template 404.html. You should have one in your templates or else you'll have errors on top of errors.

/mtv/http/Http500

Extends HttpException. Throwing this will cause MTV to generate a 500 error page.

When displayed, tries to load the template 500.html. The wp app that in the MTV plugin has a nice one setup for you already. But if you're gonna run a production site, you'll want to make your own. With a funny cat picture or something so your users don't get angry and smash something because your site isn't working.

/mtv/http/AjaxHttp404

Extends HttpException. Throwing this will cause MTV to generate a 404 response and a JSON payload.

When displayed, generates a JSON response that looks like...

{
    'error' => 'Callback not found'
}

It should probably say something funnier, but it's a 404 error. Does it need more description?

/mtv/http/AjaxHttp500

Extends HttpException. Throwing this will cause MTV to generate a 500 response and a JSON payload.

When displayed, generates a JSON response that looks like...

{
    'error': 'Some explaination of what happened for normal people',
    'data': {
        'error_code': 42,
        'error_beeps': 'beep boop'
    }
}

AjaxHttp500 will use $this->message to populate the error field, and will JSON-ify whatever is in $this->error_data. You can pass these fields in when you throw the exception.

throw new AjaxHttp500(
    'Some explaination of what happened for normal people',
    array('error_code' => 42, 'error_beeps' => 'beep boop')
);

The template layer

We use the wonderful Twig template library. MTV mostly handles setting up Twig for you, but sometimes you need a little bit more.

MTV will look for Twig template-related stuff in your apps:

myapp/
    templates/ # Twig templates go here
        base.html
        index.html
    templatetags/
        functions.php # Declare Twig functions here
        tags.php      # Declare Twig tags here
        filters.php   # Declare Twig filters here

When MTV is loaded, either by the run or load functions, Twig is initialized and a new global variable $twig is created. The $twig variable is a Twig_Environment object. You shouldn't need to use this variable directly while writing views (that's what display_template is for), but it's there if you need to do something fancy that we didn't anticipate.

Template files

MTV will equip Twig with the template directories from all of your loaded apps. When trying to load a template file, either through display_template or by accessing $twig directly, your template directories will be searched in the same order as the loaded apps.

$apps = array('myapp', 'myotherapp');

If your apps array looked like this, Twig would look for templates in myapp/templates first. If the template wasn't there, it would then look in myotherapp/templates.

Learn how to write Twig templates.

Template tags, functions and filters

MTV will look for and load three files from your app's templatetags directory after initializing Twig. These files are just provided for your convenience.

Learn how to make tags, functions and filters for your Twig templates.

When adding things to the PHP files in your app's templatetag directory, you can use the $twig variable straight away. No need for global.

<?php
$twig->addFunction('foo', new Twig_Function_Function('foo'));

Included WordPress template functions

The wp app that is included with the MTV plugin registers a lot of WordPress functions for you to use in your templates. For more details on these functions, take a look in the WordPress Codex

Here's a list:

// Debug
print_r( $var )

// General, helpful WordPress stuff
apply_filters( $tag, $value, $var ... )
esc_attr( $string )
esc_url( $string )
get_option( $show, $default )
do_action( $tag, $arg )
__( $string )

// Request flags, conditionals
is_preview()
is_single()
is_page()
is_archive()
is_date()
is_year()
is_month()
is_day()
is_time()
is_author()
is_category()
is_tag()
is_tax()
is_search()
is_feed()
is_comment_feed()
is_trackback()
is_home()
is_404()
is_comments_popup()
is_paged()
is_admin()
is_attachment()
is_singular()
is_robots()
is_posts_page()
is_post_type_archive()

// Login/register
wp_login_url( $redirect )
is_user_logged_in()

// Author functions
get_avatar( $id_or_email, $size, $default, $alt )
get_the_author_meta( $field, $userID )

// Post functions
get_edit_post_link( $id, $context )
wpautop( $string )
get_comments( $args )
mysql2date($dateformatstring, $mysqlstring, $translate = true)

// Comment form
get_comment_reply_link( $args, $comment, $post )
comment_author( $comment_ID )
get_comment_excerpt( $comment_ID )

// Thumbnails
has_post_thumbnail( $post_id )
get_the_post_thumbnail( $post_id )

// Galleries
get_attachment_url( $attachment_id )
get_attachment_thumb_url( $attachment_id )

// Theme functions
// CSS
get_stylesheet()
get_stylesheet_directory()
get_stylesheet_directory_uri()
get_stylesheet_uri()
get_template_directory_uri()
get_theme_root()

// Blog functions
get_blog_details( $fields, $getall )
get_current_blog_id()
get_site_url()
get_home_url()
get_bloginfo( $show, $filter )

// Template functions
wp_head()
wp_footer()
body_class( $class )
language_attributes()
get_header()
get_footer()
is_active_sidebar( $index )
dynamic_sidebar( $index )
sidebar_is_populated( $index )
this_year()
wp_nav_menu( $args )

AJAX

The MTV WordPress plugin has some built-in niceties for handling AJAX requests using views.

Start by including MTV's javascript in your theme's functions.php.

wp_enqueue_script('mtv-all');

If you're using the wp_head() function in your templates, you will now have MTV's javascript loaded on the front-end. This javascript provides an object called MTV that contains a few convenience functions.

debugging MTV.debugging()

Will tell you if it's okay to use console.log(). Make sure you turn on script debugging.

do_ajax MTV.do_ajax(url, data, success, error)

Fire an AJAX request at MTV. It will use the url parameter to resolve the request to a view. The data parameter will be passed along as a post variable: $_POST['data']. The last two parameters are callbacks that get fired depending on if the server returns a 200 or something else.

Handling AJAX requests

In your theme's urls.php add a new variable.

$ajax_url_patterns = array(
    '/posts/' => 'myapp\views\ajax_posts'
)

The MTV plugin hooks into WordPress's AJAX handling and run when the 'mtv' action is used. In order to keep ajax urls and standard urls separate we add this new variable, although the request processing works exactly the same.

When we write a view, we'll be doing things slightly differently.

function ajax_posts( $request ) {
    $request_data = json_decode($_POST['data']);

    // do something with my data

    if ($success)
        \mtv\shortcuts\display_json($posts_data);
    else
        throw new \mtv\http\AjaxHttp500("Something bad happened.");
}

There we have it, easy AJAX.

Something went wrong with that request. Please try again.