I spent a few days last week working on a new plugin, trying to approach it from different angles and test a few development flows in a way that would possibly change the way I work. One of my experiments was related to building an MVC plugin for WordPress – and I failed to do that in a way that satisfies me.
tl;dr – there are ways to build MVC plugins on the top of WordPress, some people do that, it didn’t seem natural to me even if I like MVC a lot.
Even if some developers, architects and consultants claim for MVC to be an overhead or a bad pattern, I like it a lot as a way to structure a given code base. Separation of responsibilities, easy decoupling of data or frontend layers etc.
WordPress and the Front Controller pattern
WordPress relies on the Front Controller pattern to manage all blogs and sites, together with their extensions (themes and plugins). It’s a centralized way to control all incoming request and redirect them accordingly. Normally, a router is set in place in a way that different URL groups are redirected to their mapped locations – controlling specific CPTs or taxonomies, loading certain archive pages etc.
Practically index.php is the file being loaded for every single request. Then, based on paths and everything, it loads other files – the URL is parsed against the rules from the router and the content/template is loaded based on these rules.
Few of the most popular PHP frameworks such as: Zend Framework, CodeIgniter, CakePHP, Yii and Symfony are MVC-driven, but they also rely on the Front Controller pattern – the only difference is that by default paths are well-formatted and predictable, and given classes and methods are loaded based on those URL arguments. It’s not mandatory and you can override that, but an example from a CodeIgniter/CakePHP-driven application could be loading in your browser:
Once the URL is parsed, it would initialize the Product controller. The product model would be loaded, reading (through some ORM) the product with ID 10. The “view” method of that controller object would be called with the product data that we just read.
Now, you can see how convenient this could be. It could be an overhead at times, but if you plan a project with all MVC drawbacks in mind, you could make use of that approach too.
The MVC Plugin
When I started the test project here, the first interaction was a small skeleton with controllers, models and views (scaffolded), a config folder for some setup details, lib and vendor folders for external entries. JS and CSS directories for assets.
Playing a bit with several use cases, it seemed quite hard to decide what should be loaded/read/used where. So, I dug into code reviewing all MVC-based WordPress plugin (and theme) frameworks and snippets I was able to find:
- Chester WordPress MVC Theme Framework
- PHP MVC Router
- Tina MVC
- Vacuum MVC
- other snippets from WordPress Gear
The two “code collections” (I’m using general terms since some of those are labeled as frameworks, libraries, applications, drop-ins etc) that I particularly enjoyed were the PHP MVC Router and WP MVC. The former specifically claims that “THIS IS NOT AN MVC FRAMEWORK FOR PHP!” and I admit that the router part is the main component here, but the folder structure and code samples are in fact a great example of structuring your MVC application. WP MVC on the other hand is pretty solid and massive – quite complicated and extensible, shell included, even the important routing details such as their MvcInflector class are in place.
So, why did I fail to properly implement an MVC-based plugin on WordPress?
The entire WordPress concept of loading data, providing content, displaying views seemed incompatible for my understanding. We have the admin and the frontend, they work in a different way. On the frontend part we have an entire template hierarchy, with various template types (including custom page templates). We also have shortcodes and widgets for displaying snippets here and there, and access to the WordPress queries. In the admin there are post type/taxonomy controls existing in WordPress itself, everything is represented as posts or terms, access to settings/users/tools etc. pages is built in a different way, but not as distinctive URL-wise.
Building predictable paths is not as straight-forward as it is in the MVC frameworks mentioned above (see the product example). URLs are not easy to guess and map to files, the patterns even differ in the admin and the frontend. Therefore, a specific router should be implemented, but doesn’t WordPress have one already? The Rewrite API was created for routing control, but is it flexible enough to support custom product/plugin paths the way we want them with MVC?
Long story short, we could use the Rewrite API for what we need, or build a custom router. Then, given the fact that we need plenty of custom route types, we’ll have a massive config (think yml, xml, long PHP arrays and everything) which would get messy for larger plugins. Also, we don’t have a direct mapping to files in this router and all refactoring should happen both in the actual files AND the router paths (easy to miss, skip, break).
I wasn’t able to figure out an easy way to not load everything when I don’t need it. Loading all plugin files on every request when 90% of them are needed in one or two screens only isn’t that slow, but it doesn’t make sense to me. Therefore, I started working one some sample autoloaders, working in different directions. Most of the issues I had with the routers (paths, too many use cases, too many components and controls to think about) applied for the autoloaders too.
PHP 5.2 support
Another breaking point was the PHP 5.2 support. The lack of namespaces for managing the code packages properly, lacking functions for reflection and low-level code control, class information needed in inheritance, were few of the issues that could lock you with repetitive code against the KISS principle, hardly maintainable and a larger percent of backwards compatibility snippets due to a 6+ years old PHP version.
Getting back to the whole MVC idea, we could assume that WordPress is taking care of the controller and view aspects of the MVC pattern. The custom post types and taxonomies are also being present, but I wouldn’t label them as a model myself.
Either way, if we stick to a “Model” as per MVC, then each unique entity would have a model class that would represent a single entry type (allowing for an array, map etc. of many entry objects). So we could have a class Post for posts, Page for pages, Product for a Product CPT, Order for an Order CPT. Additionally we could create models for custom tables that we need (which aren’t a good fit for the posts table).
Okay, where would the register_post_type or register_taxonomy functions live? In each model class – in the class or outside, but still in the file – or somewhere else? This isn’t a problem that frameworks usually take care of, because a model is just mapped to a data structure through ORM or any other way and reads/writes data on demand. It doesn’t have to be initialized all the time in order to be available with its UI, or within WP_Query objects. So it gets messy once again.
Not to mention that we might have to unnecessarily load all models, or initialize them even just to register them. And fetch data from several tables, convert in both directions, search for ways to implement post type relationships (with Posts 2 Posts or anything else), not to mention users here, or extending taxonomies.
The views would have to override the default template hierarchy in WordPress. Controllers would lay on the routing controller foundation of WordPress, and here we go again…
I’m not saying that using MVC in WordPress plugins is impossible, or should be avoided – apparently there are several boilerplates or libraries already that would do the work. I was tempted to start using/fork any of them, or build a base for my plugins (a new project similar to DX Plugin Base) that would intelligently communicate with the WordPress core, provide a maintainable code structure for a larger plugin, implementing standard and decent best practices and design patterns. I consider this attempt a failure – not that it was impossible, but because I wasn’t satisfied with the end results – overriding core WordPress APIs, inconsistent router entries, the necessity to comply with older PHP versions, and generally structure the content, control and display layers in the best possible way.
I’d be happy to revise that in a year or so (maybe too optimistic) when 5.2 is no longer required, and see if we could manage to work on that again with the state of the WordPress core in that near future.