• 13 min read
If you've been working in web long enough, you will most likely have run into WordPress in some shape or form. For a seasoned developer, this experience has most likely involved throwing your hands up in the air in frustration from its archaic architecture, badly written plugins or some theme's spaghetti code.
I've recently had the pleasure of working on a redesign of a WordPress site for one of Hive's clients. For a typical application following good, modern practices, this wouldn't be so difficult, but upon reviewing the codebase it became clear that this would be anything but.
The code had already passed through several agencies in the past and was consequently riddled with the type of problems you would expect from this kind of churn; badly written and buggy features, inconsistent coding practices and over-reliance on third-party and internal plugins (that we had no control over).
Fortunately, with some buy-in from the client and the rest of the team, we had the go-ahead to rebuild the site (still in WordPress). Thankfully this posed a great opportunity to try a WordPress framework that had recently piqued my interest.
Themosis is a framework that attempts to modernise the WordPress development experience by wrapping the WordPress core with with an MVC layer that enables us to write clean object-oriented code instead of the typical procedural mess. It's actually been around since 2014, and its initial implementation revolved around defining all application code in the theme itself. This code would then be glued together by a loose set of Laravel packages.
Recently, Themosis reached its 2.0 release and has seen some major re-work. It has now moved away from defining everything inside the theme and instead fully embraces most of the Laravel feature set, including its project structure and general application architecture. Laravel developers will really feel at home here, with some exceptions (more on this later).
Out of the box, we get many of Laravel's standard features, like routing, Blade templates, Eloquent and validation. These tools allow us to take control of application-level code that WordPress would normally handle and inject some sanity back into the workflow.
Themosis is activated via a theme that has been built to work with WordPress (there is a default starter theme that you are encouraged to use). The theme acts as an entrypoint that delegates any further request handling to the bootstrapped Themosis application instance. From that point, you're basically working with a Laravel application rather than WordPress!
The project we used Themosis on was a fairly simple WordPress site with standard requirements. This included the usual assortment of custom post types that required custom meta fields, contact forms for getting in touch and a shop for selling products.
In the next few sections we'll go through the general experience of working with Themosis on this project.
One of the areas that WordPress arguably has a deficiency is dependency management. Administrators can easily break things as they normally have free reign to update and install plugins via the admin UI. To counteract this, we can use Composer to install WordPress plugins instead (via WPackagist) to control package versions. Themosis ships with this setup already and even uses Composer to install WordPress itself.
This is a good start and WordPress projects in general should adopt this approach to take advantage of packages in the Composer ecosystem and version control their plugin dependencies!
Additionally, as a benefit of having most Laravel features available to us, we have also been able to keep the number of WordPress plugins down to the bare minimum. Implementing custom functionality in Laravel is generally a breeze compared to WordPress and really reduces the need for most plugins.
I'm also not a big fan of WordPress plugins as they can have a large variance in quality and can introduce security vulnerabilities where higher quality Composer packages generally don't.
Because we're using Composer and Themosis is basically a Laravel application, we can bring in most packages that specifically target Laravel. For example, I was able to get the very useful Laravel Debugbar and Laravel IDE Helper packages working with fairly minimal fuss. These packages are pretty much development essentials if you're working with Laravel and use PHPStorm, by the way!
Debugbar was a little trickier to get working, but just required the pushMiddleware
method to be
added to the Kernel
class:
class Kernel extends HttpKernel
{
public function pushMiddleware($middleware)
{
if (array_search($middleware, $this->middleware) === false) {
$this->middleware[] = $middleware;
}
return $this;
}
}
PHPStorm's Laravel plugin also works and successfully identifies the project as a Laravel one, offering all the usual auto-completion and quick navigations in routes, config, etc. The development experience has consequently been pretty seamless and is pretty much no different than working on a Laravel project!
Whilst the project structure very closely resembles Laravel's, it is not a complete one-for-one
mapping. WordPress and its associated files live in a separate directory called htdocs
. This
convention should probably be a bit familiar for people that have deployed WordPress on Apache
servers. The project structure looks like the following:
/project-root
/app
/config
/htdocs
/cms <-- WordPress core lives here
/content
/plugins
/themes
/your-theme <-- Your Themosis theme lives here
/views
/uploads
index.php
wp-config.php
/resources
/views
/routes
One thing that is a little confusing with this project structure is where your views should go. The
project root has a resources/views
directory (like Laravel) and the theme directory has a views
directory. Having asked Julien Lambé (the Themosis lead) what he thought, he encourages using the
theme's views
, whilst the project root's resources/views
directory should be used for
common/admin pages instead.
Unfortunately, I think for a relatively simple site without any common/admin pages, you don't get
much benefit from this organisation of views. It feels like it would be simpler to place everything
in the project root's resources/views
directory to be more consistent with Laravel. It would also
save having to jump around the project to different nested directories.
The default theme comes with Laravel Mix installed and lets you immediately get started with a
modern asset pipeline e.g. using Sass and ES6+ JavaScript. Assets are built and will be placed in
the theme's dist
directory.
It should be noted that when you move into a production environment, direct access to the entire
theme directory should be blocked, with the exception of the dist
directory.
For convenience, I ended up adding Lerna so we could run scripts
from the top-level (rather than have to cd
all the way to the theme directory). This might be a
little overkill, but makes a bit more sense if you need to create some additional admin assets, as
they would most likely go in the project root's resources
directory instead.
Whilst Themosis ships with Laravel's Eloquent ORM, it doesn't provide any WordPress models out of the box. Fortunately, Eloquent is flexible enough to map to WordPress quite easily. Query scopes are particularly useful here as they simplify the queries you'll need in various places, such as in relationships like posts and post meta.
Whilst writing these models wasn't hard, there was a bit of work to make some of these function like
their WordPress equivalents. It's a bit of a shame that Themosis doesn't ship with any models and is
perhaps something they should have in the future. It seems like Julien Lambé prefers
using WordPress' default WP_Post
abstraction instead of Eloquent for simple use-cases, so he may
opt to keep Themosis neutral on this matter.
Something that could potentially fill that gap right now is Corcel, which should work with Themosis (I cannot confirm this however). Unfortunately, we didn't end up using it on this particular project (as I forgot about it), but I would definitely give it a look for my next Themosis project.
Themosis provides wrapper API that assist in creating custom post types, meta boxes and taxonomies. These are mostly just builders that are pre-configured with some sane defaults and allow us to call the underlying WordPress functions in a more OOP style.
Unfortunately, in the case of the PostType
API, this still mostly works by providing a
configuration array to the builder i.e.
PostType::make('team', 'Team members', 'Team member')
->setArguments([
'menu_position' => 21,
'supports' => [
'editor',
'excerpt',
'thumbnail',
'title',
],
'has_archive' => false,
'rewrite' => [
'with_front' => false,
],
])
->set();
I feel like Themosis could go a little further here and provide additional methods that encapsulate the configuration more so we can write it out more easily (with fewer potential mistakes) e.g.
PostType::make('team', 'Team members', 'Team member')
->setMenuPosition(21)
->setSupports('editor', 'excerpt', 'thumbnail', 'title')
->setHasArchive(false)
...
One area that does seem to work quite nicely is the Metabox
and Field
APIs. Custom fields for
saving post meta can be created easily and are added to the WordPress admin as React components.
Out of the box, a fairly large number of field types are available and should cover most use-cases.
If you're wondering where to put all these declarations, I personally opted to put it in a
Hookable
class called PostTypes
. Hookable
classes are automatically loaded and are a great place to declare any code where WordPress hooks
are concerned (as the name suggests).
For the most part, Themosis' routing works exactly like Laravel's and is declared in a
routes/web.php
file using the Route
API. There are some notable differences that allow Themosis'
routing to integrate with WordPress' routing model.
Themosis has a concept of WordPress routes
and these include things such as home, page and post routes. These are also declared using the
Route
API:
Route::get('home', function () {});
Route::get('page', function () {});
Route::get('single', function () {});
You inevitably end up needing at least some of these if you want to load pages with dynamic content
from WordPress. For example, we used this for declaring a default page route using
Route::get('page')
, which would render a matching WordPress page and its content in a default
template whenever a more specific route could not be found earlier in the stack.
I'm personally not a massive fan of declaring WordPress routes like this, and would have preferred
if they were declared via another API e.g. WPRoute
. Piggybacking on-top of Route
feels a bit
hacky and needs some trial and error before you figure out how these route definitions interact
within WordPress and Laravel.
How you define these routes also depends on how you decide to utilise the controller layer. If you want to use Eloquent instead of WordPress loops, it will probably be better to use standard Laravel routes. If you want to use loops, opt for Wordpress routes instead.
Themosis gives us the option to define views as either Blade or Twig templates. For this project, I just opted to use Blade as it had the least effort involved to get started. These templates work exactly like Laravel's, but with some additional directives to help work with WordPress.
One such example is the @loop
directive. This is essentially an alias for the standard WordPress
loop and looks like this:
@loop
<h1>{{ Loop::title() }}</h1>
<div>
{{ Loop::content() }}
</div>
@endloop
You typically need this whenever you're working with dynamic content that is being pulled in for a WordPress route.
There is also the @query
directive that can be used for creating custom loops (we avoided using
these):
@query(['post_type' => 'post', 'posts_per_page' => 3])
<h1>{{ Loop::title() }}</h1>
<div>
{{ Loop::content() }}
</div>
@endquery
Whilst this could be useful in simpler use-cases, I opted to leverage Eloquent and place any query
logic in the controller layer (following a standard MVC pattern). This also allowed us to add
doc-block type hints for our view models using @php
blocks:
@php
/**
* @var \App\Post[]|\Illuminate\Database\Eloquent\Collection $posts
*/
@endphp
This is great for providing a manifest of what variables are in scope, and with an IDE like PHPStorm, this also gives you auto-completion within the template. I would also recommend doing this for standard Laravel projects as well!
There are two options for form handling in Themosis. You can either do it the standard Laravel way or using Themosis' new Form API. The new Form API looks interesting and feels quite similar to Symfony Forms. For most use-cases it probably works well enough for scaffolding up forms quickly.
For this project, I opted to build our forms using standard Laravel APIs such as Request
and
Validator
. I'm personally not a big fan of form builders in controllers as it feels like it starts
blurring the separation of concerns between the controller and view too much. I think a better
approach is to define form components/partials for re-use instead.
Unfortunately, the standard Laravel approach needs some tweaking to our HTTP middleware as Themosis
doesn't include all of the necessary middlewares to make errors and old input persist in the
session. We just needed to add the StartSession
and ShareErrorsFromSession
middlewares like so:
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'web' => [
'wp.bindings',
'bindings',
StartSession::class,
ShareErrorsFromSession::class
],
];
}
Additionally, we also needed to manually add the old
helper method to display old form inputs in
our form. Unfortunately Themosis doesn't come with this out of the box, but it's not difficult to
add in some file like bootstrap/helpers.php
(required in through bootstrap/app.php
):
if (!function_exists('old')) {
function old(string $key, $default = null)
{
return Session::getOldInput($key, $default);
}
}
Other useful helpers such as dd
are also not included with Themosis unfortunately.
One of the final project requirements was a simple shop for selling products. As we were using WordPress already, it seemed pretty obvious to use WooCommerce to fulfil this requirement.
Themosis' documentation has an entire section
on setting this up, as you do need to make some additional tweaks to make everything work. One thing
we noticed the documentation lacked was how to enable featured images for products. You need to
remember to add the following to your theme's config/support.php
:
return [
'post-thumbnails' => [
'product'
],
]
Whilst the default WooCommerce seems to work fine for most use-cases, it doesn't seem to be very
flexible if you need to override some of the more nested templates. I did some investigation and it
looks like you need to create a woocommerce
directory in your theme and create vanilla PHP
templates that match the corresponding template.
This is how you would normally override WooCommerce templates in WordPress, but it feels a little disappointing that Themosis doesn't provide a way to use Blade templates instead. Unfortunately, it might also be impossible to change this (in a non-hacky way) as long as WooCommerce implements its own template loader.
Both Laravel and WordPress have email functionality that presents some challenges for interop. WordPress uses PHPMailer internally (assumes you'll use SMTP), and can cause friction if you want to use a HTTP API service instead like SendGrid.
I didn't spend too long investigating the options here, but SendGrid does offer a plugin that should theoretically work, however the reviews/ratings aren't particularly encouraging. If you use another email provider, they may offer a better integration plugin.
We ended up just opting to configure WordPress to use SendGrid's SMTP service, and Laravel with SendGrid's HTTP API (using a custom mail driver). Whilst this isn't entirely consistent, at least our emails are still being offloaded to SendGrid.
This was pretty easy to implement as Themosis provides a hookable Mail
class that configures
WordPress' PHPMailer with SMTP credentials using Laravel's mail configuration variables.
On the whole, the last few months using Themosis has generally made it enjoyable to work with WordPress again. Being able to structure our application in a standard MVC way feels much cleaner and is more in-line with standard web development. Consequently, I don't think I would ever want to go back to vanilla WordPress for a greenfield project!
That said, there were still some rough edges to the entire experience and there are improvements
that can be made to streamline things further. I would particularly like to see all views and assets
moved into the project root's resources
directory and more of an opinionated approach to modelling
the WordPress database with Eloquent.
Things have been great overall and I wouldn't hesitate to encourage other developers to pick up Themosis for their next WordPress project. If not, it's certainly a project to keep your eye on!