alleyinteractive/mantle
Mantle brings Laravel-style development to WordPress. Build themes and plugins with Eloquent models, Blade views, routing, service providers, and testing tools, plus a familiar application structure that keeps WordPress code organized and maintainable.
Mantle is not a plugin you "activate" — it's a Composer library that turns WordPress into an application platform. Treat installation as a build-system decision, not a dashboard click.
Minimal setup
mu-plugin or a theme's inc/, not a normal plugin):
composer require alleyinteractive/mantle
plugins_loaded runs for anything that needs Mantle's container, config, or providers:
// wp-content/mu-plugins/00-mantle.php
require_once WPMU_PLUGIN_DIR . '/my-app/vendor/autoload.php';
Application, and register your service providers there. This is the equivalent of bootstrap/app.php in Laravel — it's the single most important file in your project.config/, routes/, views/, app/. Mantle's defaults follow Laravel's conventions, so if you keep the shape, you write almost no wiring code.wp on the command line and confirm Mantle's console kernel has registered its commands, and confirm the container boots without fatal errors.Where to look first
docs/ folder and the mantle docs site — read the Providers, Models, and Routing pages in that order; everything else is downstream of those three.tests/ directory. Mantle is unusual among WP frameworks in that its test suite doubles as the best usage documentation for models, factories, and the HTTP test client.config/ keys shipped by the framework — they tell you what's actually toggleable versus hardcoded.First use case
The highest-ROI first slice is a custom post type with a model, a route, and a Blade view. Register the post type, model it with a Mantle Post model, expose it at a URL via the router, and render with Blade. If that works end-to-end you've exercised the container, the query builder, the rewrite layer, and the view layer — which is 80% of what you'll touch daily. Don't start with the queue or the block editor.
Service providers as the seam between WordPress and your app
Everything non-trivial belongs in a provider. This is where you register hooks the old way and bind new things into the container:
class Content_Provider extends Service_Provider {
public function register() {
$this->app->singleton( Content_Repository::class );
}
public function boot() {
add_action( 'init', [ $this, 'register_post_types' ] );
}
}
The pattern that pays off: keep boot() for WordPress-facing registration (hooks, post types, taxonomies, REST fields) and keep business logic in plain injected classes. That's the whole point of the framework — your logic becomes testable without WP_UnitTestCase gymnastics.
Models: the daily workhorse
Mantle's Post, Term, and User models wrap WP_Query and the meta API in a query-builder interface. Day-to-day you'll:
class Event extends Post {
protected $object_type = 'event';
protected $meta = [
'start_date' => 'datetime',
'venue_id' => 'integer',
'is_virtual' => 'boolean',
];
}
WP_Query args, and use relationships for what would otherwise be a nest of meta_query clauses.find(), create(), save() for writes so meta and object caches stay consistent.Routing
Define routes in routes/web.php and routes/api.php. The router maps paths onto WordPress rewrite rules and dispatches to controller methods via container auto-wiring — so constructor injection just works:
Route::get( '/events/{event}', [ Event_Controller::class, 'show' ] );
Route::resource( '/venues', Venue_Controller::class );
This is the single biggest ergonomic win over vanilla WordPress: no more add_rewrite_rule() + template_redirect + manual get_query_var() plumbing. Remember to flush rewrites on deploy (see Gotchas).
Views with Blade
view( 'events.show', [ 'event' => $event ] ) inside a controller or a thin WordPress template. Keep PHP out of single-event.php — make the template file a two-liner that delegates to a Blade view. Blade's @extends/@include/components give you template inheritance WordPress simply doesn't have, and compiled views cache well in production.
Console commands
Anything you'd otherwise do with a one-off script — imports, backfills, cache warmers, data migrations — becomes a registered WP-CLI command. This is how you make deploys reproducible:
wp myapp import-events --dry-run
Guard command registration behind a WP_CLI check so the code doesn't fatal on web requests, and always implement --dry-run on anything destructive.
Testing
Extend Mantle's test case, use the model factories to build fixtures, and hit routes through the HTTP test client:
$event = EventFactory::create( [ 'is_virtual' => true ] );
$this->get( "/events/{$event->id}" )->assertOk();
The win here is being able to assert on behaviour (status codes, rendered output, query counts) rather than poking at globals. Budget time for wiring WP_TESTS_DIR and a dedicated test database — it's the usual WordPress tax and Mantle doesn't remove it.
Blocks, queues, HTTP
block.json as the source of truth for attributes.post_updated from timing out on imports.Load order will bite you first. Mantle must boot before anything that expects its container or config. A mu-plugin alphabetically early (00-mantle.php) is the safe pattern. If providers appear not to run, it's almost always that something loaded Mantle too late or a second copy got autoloaded.
Composer conflicts are the biggest operational risk. Mantle pulls in illuminate/* components. Any other plugin or theme that vendors its own Laravel components — at a different version — can produce duplicate class definitions or a container that resolves the wrong instance. Audit with composer why illuminate/support before you blame Mantle for a weird fatal. Also pin PHP: recent Mantle releases expect modern PHP (8.1+), and shared hosts that quietly run 7.4 will fail at the autoloader.
Rewrite rules don't flush themselves. Routes 404 until you run wp rewrite flush (or the REST equivalent). Add it to your deploy script. In local dev, flush after every route change — this is the #1 "the router is broken" false alarm.
Meta caching and stale models. Mantle's models read through WordPress's object cache. If some other plugin writes post meta directly via update_post_meta(), your model instance can be stale. Use a fresh query or the model's refresh path rather than reusing an object across a write boundary. In tests, remember to reset caches between cases or you'll get ghost failures.
Query builder is not SQL. where() on meta fields translates into meta_query, which means JOINs and ORDER BY meta_value can be genuinely slow on large datasets. Check with a query-log or SAVEQUERIES before shipping a filtered archive. Also watch posts_per_page/no_found_rows equivalents — the defaults are WP defaults, not Laravel's.
IDs and naming. WordPress objects are ID; Eloquent-ish models are id. Mixed conventions show up in views and in $post->ID vs $model->id. Be consistent in your own code and convert at the boundary.
Facades are convenient and opaque. They work, but if a facade resolves at the wrong time (before a provider registers), you get confusing nulls. When debugging, prefer app()->make( X::class ) to isolate whether it's the binding or the facade alias.
Don't fight the plugin ecosystem. Mantle replaces WordPress's structure, not its ecosystem. WooCommerce, ACF, SEO plugins, etc., still speak hooks. Keep an adapter provider that bridges their hooks into your services rather than trying to model their data with Mantle models.
Debugging toolbox. dd()-style helpers, the container's binding dump, and Xdebug with a breakpoint in the provider's boot() will solve most "why isn't this registered" questions faster than reading docs. For view issues, look at the compiled Blade directory — permission errors there produce blank pages with no PHP error.
Extension points worth knowing. Custom service providers, container singletons/bindings, and the console kernel are the supported seams. If you find yourself subclassing framework internals, you're probably on an unsupported path and a minor release will hurt.
Team cost. The framework is well-built but the documentation and community are thin relative to Laravel. A WordPress developer new to it needs a few days to internalise the container and model layers; a Laravel developer needs to unlearn assumptions about request lifecycle and Eloquent's full feature set. Budget for that, and keep your providers small and explicit so the next person can follow the wiring without reading the framework source.
How can I help you explore Laravel packages today?