How To Write an MVC Wordpress Plugin
Sticking to MVC Makes Your Plugin Code Easy To Upgrade and Maintain
Wordpress plugins don't have to follow specific structure. That's why everyone writes them in their own style. Plenty of the existing plugins are a mess of files named without following any conventions, full of procedure code, mixed HTML with PHP, and just dropped into a single folder (I'm guilty for the last one too). This works for small plugins but once you start adding more complex functionality it quickly turns into maintenance nightmare.
MVC (Model-View-Controller) is a good solution to this. Maybe not the best, but it's probably the most popular and good enough. So from my latest plugins like Eventy PRO for example I am starting to follow the MVC approach I am going to share with you.
I am not saying that my approach is the best or the only possible one. There are several MVC frameworks for Wordpress that handle things in slightly different way. But I think what I share is easy and well structured, and will let you write good plugins that are easy to maintain.
Overall Structure
I like to keep only the index file of the plugin in the main plugin folder. This keeps everything more organized. And I call it simply index.php just like you would do in a web site. Really, what's the point to give name that's not index.php? By naming your main file index.php everyone who works with your plugin can immediately recognize the main file (even if you've dropped many others in the root foler).
The index then requires all the controllers, some models and all the helpers. I do not include all models, only these that are needed at start time. Controllers then will include what else has to be in a particular situation and this way will reduce the load.
The index then fires several actions, defines the plugin URL, and registers the activation hook. And that's ALL that the index does. Let's better see a code example. We'll call this plugin "Myplugin"
// define global path so we can use it everywhere define( 'MYPLUGIN_PATH', dirname( __FILE__ ) ); // require controllers, helpers and some models (currently just the basic model and the widget model) require(MYPLUGIN_PATH."/helpers/linkhelper.php"); require(MYPLUGIN_PATH."/helpers/htmlhelper.php"); require(MYPLUGIN_PATH."/models/myplugin.php"); require(MYPLUGIN_PATH."/models/widget.php"); require(MYPLUGIN_PATH."/controllers/my_controller.php"); register_activation_hook(__FILE__, array("MyPlugin", "install")); // call the menu and scripts add_action('admin_menu', array("MyPlugin", "menu")); add_action('admin_enqueue_scripts', array("MyPlugin", "scripts")); // show the things on the front-end add_action( 'wp_enqueue_scripts', array("MyPlugin", "scripts")); // widgets add_action( 'widgets_init', array("MyPlugin", "register_widgets") ); // other actions add_action('plugins_loaded', array("MyPlugin", "init"));
Dissection
Let's see what each of these calls does and how the actual work in the plugin can be done.
The helpers contain basic helper function like one that makes friendly links, another one that outputs a date drop-down selector, and so on. These are created as classes with static models so we can invoke them just like procedure functions, without creating an object. Why use objects at all then? Because we want to avoid possible name collisions with other plugins. So for example a call to display date drop-down would look something like this:
MyPluginHTMLHelper::date_dropdown();
It's few letters longer than calling a short procedure function but you can be sure there will be no naming conflict. Alternative to this would be using PHP namespaces.
Then the models work like models in any web application. They generally abstract each of the database tables that your plugin uses. I like to make one exception with the basic/main model here. It is a file named after the plugin name, and contains all the initialization functions. So for example for MyPlugin, your class would be called MyPlugin in models/myplugin.php and would look like this:
class MyPlugin { // runs the DB table queries static function install() { // ... } // creates the admin menu static function menu() { // ... } // CSS and JS static function scripts() { // here are the wp_enqueue_script, wp_enqueue_style etc calls } // initialization static function init() { // load_plugin_textdomain, creating constants (for table names for example) } // manage general options static function options() { // ... } // registering widgets static function register_widgets() { // register_widget('MyPluginWidget') etc } }
Of course you may prefer to keep this file outside of models if you wish.
Then in controllers I put specific classes for different logical operations. You are entirely free to follow your own ideas here. You can even load all controller classes in a single file.
The menus (called by add_menu_page, add_submenu_page etc) will call different controller functions. Then these controller functions require models only when they need them, and include the appropriate views. This part should be pretty straightforward for everyone who has worked with the MVC concept outside of Wordpress.
To get a better idea you can check the code of the free Eventy plugin. It doesn't follow the given structure exactly but is pretty close (I'll try to get closer with the next update).
Why Static Methods?
This may be seen as a personal preference and is not necessarily related to Wordpress. I think there is no point to create object instance if you simply want to enclose a library of functions in an class. Creating instances makes sense when each of these instances is supposed to hold different data, and probably you may have more than one instance at a time.
Other Things To Note
1. In the views I prefer to use the alternative PHP syntax. It makes the views cleaner and helps to further separate the logic of the design. Again, this is not a Wordpess-specific thing, I do it outside of Wordpress too.
2. Keep the javascript and CSS files in their own folders to keep the root folder clean. Ideally only index.php and readme.txt will be in the root directory.
3. Any suggestions to further improve this setup are most welcome. Also I wonder whether releasing yet another mini-MVC foundation framework will be useful for anyone?