Streetwise Media WordPress MVC
Rails inspired framework for MVC WordPress development
by Brian Zeligson · github.com/streetwise-media/streetwise-wordpress-mvc · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/streetwise-media/streetwise-wordpress-mvc/archive/refs/heads/master.zipReadme
swpMVC
swpMVC is a lightweight MVC framework built to bring some of the experience of other rapid application development frameworks to WordPress. Inspired largely by Rails, Express and FuelPHP, it aims to make routing, modeling, and rendering easy, giving you more control over your code structure than WordPress gives out of the box, without adding too much extra work.
Features
- Full MVC framework within WordPress environment
- Sinatra/Express style routing
- Data modeling courtesy of PHP ActiveRecord
- Runtime model extensions via Roles, Renderers, Control Renderers, and Validators
- JS library to ease ajax development using Model generated forms
- Query builder based on Model generated forms for seamless lookups
Tutorial
The quickest way to get familiar with the swpMVC framework is with the TodoApp tutorial found here
Starter Plugin
In place of code generation, you can get a jump start on plugin development by examining (and using) the starter plugin found in the starter_plugin directory.
Why a singleton?
Singletons are little more than object oriented global variable, but your plugins need to add WordPress filters and actions. Using a singleton gives you easy access to the plugin class, and also makes sure you don't end up running the methods you hang on your filters and actions more than once each.
require_dependencies
This is called in the constructor, and this is where you should include your models and controllers. By creating the plugin instance on the swp_mvc_init action, we ensure that the swpMVC core is loaded, and the base classes which your models and controllers must extend will be available.
Alternatively, models which need to be extended by other plugins can be preloaded by requiring them on the swp_mvc_preload action.
add_actions
Also called in the constructor, this is used to hook the add_routes method to the swp_mvc_routes filter, where you can add your plugin routes using the syntax described in the router section. By placing this after the require_dependencies call in the constructor, you can be sure your controllers are loaded when you add the routes to the system.
add_routes
swpMVC follows a routing structure that more closely resembles Sinatra or Express than WordPress' rewrite rules. See the next section for syntax.
Routes
Adding routes
swpMVC routes are stored as an array of arrays, with each array stored representing one route using the following structure
<?php
$route = array('controller' => 'ControllerClass',
'method' => 'ControllerMethod',
'route' => '/url/of/route/:p/:p'
);
There is no "automagic" routing, everything must be declared. This is done so that your routing structure is exactly as you want, with no additional steps required to turn off magic routes.
Routing parameters
Parameters in your route are represented with the token ":p"
They will be passed to your controller method in the same order they appear in the route. Skipping named parameters allows the framework to use only one additional querystring variable
Auto-flush rewrite rules
The core framework will monitor whether swpMVC routes have been added, modified or removed, and flush the rewrite rules as needed, so there is no need to do this manually.
Example
Here's a full example adding routes from your swpMVC plugin based off the example plugin included in the example directory
<?php
public function add_routes($routes)
{
$r[] = array('controller' => 'swpMVC_Example_Controller',
'method' => 'wp_style',
'route' => '/recent_thumbs/wp_style');
$r[] = array('controller' => 'swpMVC_Example_Controller',
'method' => 'swpmvc_style',
'route' => '/recent_thumbs/swpmvc_style');
$r[] = array('controller' => 'swpMVC_Example_Controller',
'method' => 'render_post_form',
'route' => '/post_form/:p');
$s = array_merge($routes, $r);
return $s;
}
Overriding the router
Sometimes you may want to override a default route provided by WordPress. In this case, you can use the swpmvc_request_override action hook to manually set the necessary query vars that will redirect to your desired controller method. The following example will call the PostController::single_post method passing in the slug when a single post is viewed
//plugin.php
add_action('swpmvc_request_override', 'override_request');
function override_request()
{
if (!is_single()) return;
global $wp_query, $post;
$wp_query->query_vars['swpmvc_controller'] = 'PostController';
$wp_query->query_vars['swpmvc_method'] = 'single_post';
$wp_query->query_vars['swpmvc_params'] = array($post->post_name);
}
//PostController.php
class PostController.php extends swpMVCBaseController
{
public function single_post($slug)
{
//retreive post model by slug and do something with it.
}
}
Models
Models must extend the swpMVCBaseModel class. This class itself extends ActiveRecord\Model, from the PHP ActiveRecord library. For query syntax, CRUD operations, basic model definitions, and overloading, refer to the ActiveRecord docs. The copy included in swpMVC includes several modifications to make it more WordPress friendly, (the diff is backwards, sorry).
public static function tablename
Instead of declaring the model table with a static variable, we use a static method. This allows us to do the following
<?php
public static function tablename()
{
global $wpdb;
return $wpdb->prefix.'posts';
}
This model would now be multisite aware. The advantage to using a method over a variable is that we can now dynamically define the table property for our model.
public static function conditions
This defines any conditions that should apply to every finder query that is generated by the model. For example if I wanted to model draft posts only:
<?php
public static function conditions()
{
return array("post_status = ?", "draft");
}
public static function joins
This defines any joins that should apply to every finder query that is generated by the model. In general for related eager loading, I favor include, using the joins method only when my conditions method relies on data in another table. An example of how this can be used to model categories:
<?php
class Category extends swpMVCBaseModel
{
public static function tablename()
{
global $wpdb;
return $wpdb->prefix.'terms';
}
public static function conditions()
{
global $wpdb;
$tt = $wpdb->prefix.'term_taxonomy';
return array("$tt.taxonomy = ?", 'category');
}
public static function joins()
{
global $wpdb;
$t = self::tablename();
$tt = $wpdb->prefix.'term_taxonomy';
return "LEFT JOIN $tt ON $t.term_id = $tt.term_id";
}
}
Now any finder queries generated by the Category class will include a left join on the term_taxonomy table, and filter results to include only those where the term_taxonomy.taxonomy field has a value of "category." Filtering subsets with models is particularly relevant in WordPress, where different "types" of data are frequently lumped together in single tables.
One catch to using the joins method, is that calls to the models finder methods will need to use table prefixes for any columns that are present in both the main and joined tables. For this I recommend your Model::tablename() methods.
Model::build_find(array $args)
This method allows you to build a conditions array suitable for PHP ActiveRecords finder methods with a simplified syntax, that can easily be passed directly from a form generated by a Control Renderer.
The array passed should contain only keys that correspond to properties of the model being queried. Each key can either be a single value (will generate a 'key' = 'value' query) or an array of values (will generate a 'key' IN ('array', 'of', 'values') query)
The following example illustrates basic use:
$conditions = Post::build_find(array('id' => array(2, 3, 4, 5), 'post_author' => 4));
$conditions === array('id IN (?) AND post_author = ?', array(2, 3, 4, 5), 4);
//above is true, and formatted for use with ActiveRecord finder method
In addition to basic use, the following modifiers can be prepended to a key to alter the type of query generated:
- $lte: - generates a <= comparison
- $gte: - generates a >= comparison
- $rxor: - for use with array of values. Generates a (REGEXP val1 OR REGEXP val2 etc) comparison
- $rxand: - same as $rxor, joining multiple values with AND instead of OR
- $neq: - generates a <> comparison
- $ni: - for use with array of values. Generates a NOT IN () comparison
The below examples illustrate use of each of the modifier prefixes
$conditions = Post::build_find(array('$lte:id' => 10));
$conditions === array('id <= ?', 10);
$conditions = Post::build_find(array('$gte:id' => 10));
$conditions === array('id >= ?', 10);
$conditions = Post::build_find(array('$rxor:post_title' => array('mvc', 'php', 'wordpress')));
$conditions === array('(post_title REGEXP ? OR post_title REGEXP ? OR post_title REGEXP ?)', 'mvc', 'php', 'wordpress');
$conditions = Post::build_find(array('$rxand:post_title' => array('mvc', 'php', 'wordpress')));
$conditions === array('(post_title REGEXP ? AND post_title REGEXP ? AND post_title REGEXP ?),
'mvc', 'php', 'wordpress');
$conditions = Post::build_find(array('$neq:post_author' => 4));
$conditions === array('post_author <> ?', 4);
$conditions = Post::build_find(array('$ni:post_author' => array(4, 5)));
$conditions === array('post_author NOT IN (?)', array(4, 5));
Last, a second argument can be passed to override the bind operator, which default to AND and joins the keys of your query array together. The following example illustrates this:
$conditions = Post::build_find(array('id' => 4, 'post_author' => array(10, 11)));
$conditions === array('id = ? AND post_author IN (?)', 4, array(10, 11));
$conditions Post::build_find(array('id' => 4, 'post_author' => array(10, 11)), 'OR');
$conditions === array('id = ? OR post_author IN (?)', 4, array(10, 11));
Automatic stripslashes
Model properties in string format are automatically run through stripslashes when accessed directly. To override this, call the properties method on an object, and access the properties from the resulting array.
$model->render()
swpMVCBaseModel comes with an instance method 'render,' which accepts as an argument a Stamp template object (see Templates section for details,) and autopopulates the template using the model properties.
public function sanitize_render()
This method accepts two parameters, a model property value and the property name, and gives you a chance to sanitize it before it is returned. This will be applied any time you directly access a model property. It can be bypassed in the same was described under the automatic stripslashes section.
Here's an example of a sanitize_render definition that will run all model properties except title through strip_tags when accessed directly:
<?php
public function sanitize_render($value, $name)
{
if ($name === 'title') return $value;
return strip_tags($value);
}
//calling the following on an instance of this model
//would strip all tags from the property value:
echo $model->property;
//the following would bypass
$properties = $model->properties();
echo $properties['property'];
public function render_{{property_name}}
These methods act as overrides for your properties when called by the render method. For example, if a Stamp view object passed to the render method contains a tag called post_name, and your Post model has a method called render_post_name, the return value of that method will be used to populate the Stamp object in favor of the value of the instance property post_name.
Model::renderForm()
This method can be called statically on a model class to render an empty form for the class properties.
The method accepts three parameters. The first parameter is required and must be a valid Stamp template object to be populated, the second is an optional form prefix (defaults to the class name,), and last is an optional ControlRenderer which defines the form controls that correspond to your model properties.