potterywp/potter
Potter bridges Laravel and WordPress, offering a modern, structured workflow for building WordPress themes and plugins with cleaner code, reusable components, and less boilerplate. A handy toolkit for developers who want Laravel-style ergonomics in WordPress projects.
Potter is a Composer package that lives inside your theme, not in wp-content/plugins. Treat the theme directory as a small Laravel-ish app.
wp-content/themes/my-theme/ with the required style.css header, then:
cd wp-content/themes/my-theme
composer require potterywp/potter
functions.php a thin bootstrap — require the Composer autoloader and hand control to Potter:
require __DIR__ . '/vendor/autoload.php';
// boot the Potter theme app (exact class/function name per repo README)
functions.php — the boot entry point and where hooks are registered.views/ — your Blade templates; this replaces index.php, single.php, page.php, archive.php, etc.config/ (if present) — app-level settings: view paths, cache paths, container bindings.app/ — your theme classes: models, view composers, service providers, hook registrars.wp potter --help
wp potter list
The WP-CLI commands scaffold themes/views and manage assets, which is faster and more consistent than hand-creating files.index.php to views/index.blade.php, create views/layouts/app.blade.php with wp_head() / wp_footer() and @yield('content'), then @extends it from index.blade.php. That one conversion proves the template pipeline, the compiled-view cache path, and asset loading all work.Note the package's last release was April 2015. Pin the version, and verify PHP compatibility before committing — the underlying Illuminate/Blade components are from the PHP 5.x era.
Layouts and partials instead of get_header() / get_template_part().
{{-- views/layouts/app.blade.php --}}
<!doctype html>
<html @php(language_attributes()) @endphp>
<head>@php(wp_head()) @endphp</head>
<body @php(body_class()) @endphp>
@include('partials.nav')
@yield('content')
@php(wp_footer()) @endphp
</body>
</html>
Keep every template dumb: query in PHP, loop in Blade.
Eloquent-style models for queries. The value is replacing raw WP_Query argument arrays with readable chains:
$posts = Post::where('post_type', 'post')
->orderBy('date', 'desc')
->limit(3)
->get();
Pass the result into the view (View::make('home', compact('posts')), a view composer, or whatever the theme's renderer exposes) and @foreach it. Do this in app/ classes or a small controller-ish layer rather than inline in functions.php.
Service container for theme logic. Register bindings once, resolve everywhere:
// app/providers/ThemeServiceProvider.php
$this->app->bind('nav.walker', function () {
return new App\Support\NavWalker();
});
Then hook callbacks can accept container-resolved dependencies instead of reaching for globals. This is the main day-to-day win over vanilla WP: testable, injectable theme classes.
Hooks stay native. Register actions and filters the WordPress way — Potter doesn't replace them:
add_action('after_setup_theme', [App\Setup::class, 'themeSupports']);
add_filter('excerpt_length', fn() => 24);
Put these in a single app/hooks.php or a provider so the hook surface is greppable.
Facades for WP globals. Use the package's facades (Asset, Post, Theme, etc. — check the repo for the exact list) instead of global $wpdb-style access. In Blade, a facade call is cleaner than a pile of @php blocks.
Assets via the facade/config, not raw wp_enqueue_*. One place declares handles, versions, and paths; the CLI commands then handle copying/compiling. This is the piece that actually saves time on multi-template themes.
Scaffolding workflow. For each new template type: wp potter ... (make view / make model / make provider) → fill in the Blade body → verify in the browser. Consistent structure across a team matters more than any single feature here.
illuminate/* or a second Composer autoloader, you'll get "cannot redeclare class" or silently wrong Blade versions. Keep the theme's vendor/ isolated and audit composer.lock..blade.php. While debugging, keep compiled files around (disable cache/don't auto-purge) so you can open the compiled output and match it back.{{ }} HTML-escapes; {!! !!} does not. Never pipe the_content() through {!! !!} for untrusted authors, and remember the_title() echoes — it doesn't return. Use get_the_title() inside {{ }}.WP_Query, not real Eloquent. No lazy relations, no save(), no migrations, no model events. Pagination is especially divergent — a model "paginator" will not automatically cooperate with the_posts_pagination() / paginate_links(). Verify the query's paged handling manually.have_posts() won't set up $post. If you call template tags like the_permalink() on a model's post, call setup_postdata() (or the model's equivalent) first; otherwise you get the wrong post's data.front-page, home, 404, search, and custom post type singles — Blade view names must match its resolver, not WordPress's .php names.wp_body_open() (required by many modern plugins) and wp_head() ordering. Add them explicitly; Potter won't.WP_DEBUG / WP_DEBUG_LOG and enable Potter's debug flag if it has one (keeps compiled views, surfaces container resolution errors). If a facade returns null, the binding probably wasn't registered before the template rendered — check provider registration order in the bootstrap.Blade::directive('wphead', ...)) for noisy WP calls, view composers for shared data (menus, options) instead of repeating queries in templates, and container bindings for swapping implementations (e.g., a stub nav walker in tests).How can I help you explore Laravel packages today?