alleyinteractive/mantle-framework
Mantle is a Laravel-inspired framework for WordPress that brings Eloquent models, routing, service providers, and Blade-like views to plugin and theme development, helping teams build large-scale WordPress applications with familiar, modern PHP patterns.
Mantle replaces the "functions.php full of add_action" model with an application bootstrap, service providers, routing, models, and Blade views that feel like Laravel but compile down to WordPress hooks.
Minimal setup:
composer require alleyinteractive/mantle # or: mantle install
bootstrap/app.php boots the framework; functions.php loads the application instead of hand-registering hooks.routes/ and resources/views/ (Blade). Business logic goes in app/, usually behind a service provider registered in config/app.php.mantle CLI is your first stop for discovery: it mirrors the Artisan commands you expect (make a model, controller, service provider, test).First use case — render a template off a route instead of a single-*.php file:
// routes/web.php
use Mantle\Database\Model\Post;
Route::get( '/news/{slug}', function ( string $slug ) {
return view( 'news.single', [
'post' => Post::where( 'name', $slug )->firstOrFail(),
] );
} );
The fastest way to understand the package's conventions is to read the bundled test suite and the Mantle\Contracts\* interfaces: contracts describe the seams you are expected to code against (assets, HTML helpers, models), and the test suite shows the sanctioned way to boot WordPress inside a test.
Providers as the default home for hooks. Keep functions.php thin; register every add_action/add_filter inside a service provider's boot(). This keeps hooks discoverable and lets tests boot only the providers they need.
Routes and controllers over template hierarchy. Prefer a route → controller → Blade view for new screens. Fall back to a page template only when you genuinely need the WP loop and conditional tags. Controllers can be constructor/method injected the same as Laravel.
Models for content reads. Wrap post/meta access in Mantle\Database\Model\Post (and term/user models) so queries return typed objects, relationships work, and factories can generate fixtures. Use raw WP_Query only where you need a loop shape that the models do not cover.
Assets go through the asset manager. Register every script/stylesheet with a stable handle in one place and let the manager enqueue it, rather than sprinkling wp_enqueue_script() around:
// e.g. in a provider's boot(), or the asset registration hooks
Asset::register( 'theme-main', get_stylesheet_directory_uri() . '/dist/main.css' )
->load_method( Load_Method::DEFER ); // confirm the exact builder signatures against the asset docs
This matters more than usual after v1.22.2 — see the gotchas below; the load method is now version-dependent.
Testing. Use the package's WordPress test case (Mantle\Testing\TestCase-style base) with factories and HTTP helpers so you can assert on rendered responses, queries, and registered hooks. Tests run against a real WP boot, which is why the _doing_it_wrong() notice behaviour below can turn into a failing assertion.
HTML helpers and view components are the right place for reusable markup fragments; HTML_Helpers is the string-conversion layer used when composing attributes, and it is worth reading before rolling your own escaping helper.
The 1.22.2 asset change is the one to act on. The alleyinteractive/wp-asset-manager constraint widened to ^1.4.0 || ^2.0. If you consume the asset manager only transitively through Mantle, a plain composer update can move you from 1.x to 2.0 — a major bump you did not ask for.
Read the 2.0 notes before updating. 2.0 removes the async-defer load method. On 2.x:
defer now loads as async and raises a _doing_it_wrong() notice.Mantle\Contracts\Assets\Load_Method::ASYNC_DEFER is deprecated: passed through unchanged on wp-asset-manager 1.x, silently coerced to async on 2.x. It will be removed in Mantle 2.0.async() and defer() is subject to the same coercion — you do not need to set both, and setting both is no longer a no-op.If you rely on defer semantics for CSS, pin the dependency. Add "alleyinteractive/wp-asset-manager": "^1.4.0" to your root composer.json until you have audited stylesheets; otherwise you inherit 2.x behaviour on the next update and get notices instead of deferred styles.
Track down notice sources. _doing_it_wrong() is only visible with WP_DEBUG on, and it can fail tests. Grep your asset registrations for defer, async, and ASYNC_DEFER, and audit anything that sets both explicitly. Inspect the registered queue (wp_scripts() / wp_styles()) to confirm the load method actually applied.
Deprecations are on a clock. Anything touching ASYNC_DEFER (or the async+defer combination) should be migrated to a single explicit async now — the constant disappears in Mantle 2.0, and the pass-through on 1.x is a compatibility shim, not a contract.
The PHPStan fix is cosmetic. The catch.neverThrown fix in HTML_Helpers::convertToHtmlEntities() only affects static analysis; no runtime change. If you had a baseline entry suppressing it, regenerate the baseline.
Extension points to lean on: contracts for anything you might swap in tests, service providers for hook registration, and factories for content fixtures. If you find yourself editing vendor behaviour, you are probably missing a provider-level hook.
How can I help you explore Laravel packages today?