morningtrain/wp-view
A lightweight Laravel package that brings WordPress-style view handling to your PHP applications, making it easy to render templates, pass data to views, and keep presentation logic clean and reusable.
The package is a Composer library (not a plugin) that wires Laravel's illuminate/view Blade compiler into a WordPress theme, plus a set of custom directives that wrap common WP template functions.
Install into the theme, not the WP root:
composer require morningtrain/wp-view
If the theme has no composer.json yet, create one with "type": "wordpress-theme" and a PSR-4 autoload for your theme namespace.
Bootstrap it from functions.php, before any template renders:
require_once __DIR__ . '/vendor/autoload.php';
use MorningTrain\WP\View\View;
View::setup(
__DIR__ . '/views', // .blade.php source
__DIR__ . '/cache/views' // compiled PHP; must be writable
);
Create views/ and cache/views/ and chmod the cache dir so PHP can write. Add cache/ to .gitignore.
First use case — replace one template file. Keep the WP template hierarchy file as a thin controller and move markup into Blade:
// front-page.php
\MorningTrain\WP\View\View::render('front-page', [
'hero' => get_field('hero'),
]);
{{-- views/front-page.blade.php --}}
@extends('layouts.app')
@section('content')
<h1>{{ get_the_title() }}</h1>
{!! apply_filters('the_content', get_the_content()) !!}
@endsection
Where to look first: the README for exact View::setup/render signatures, src/View.php for the public API (share, composers, paths), and src/Directives/ for the authoritative list of @wp… directives. With 2 stars and 0 dependents, the source is the documentation — read it once and write your own cheat sheet.
Thin WP files, fat Blade views. Treat page.php, single.php, archive.php as routers: resolve data, then View::render(). This keeps WordPress's template hierarchy intact, so plugins that filter template selection still work.
Layouts mirror header.php/footer.php. If you go fully Blade, the layout must still call wp_head()/wp_footer() (usually via the package's @wpHead/@wpFooter directives) so enqueued assets and plugin hooks fire. If you're migrating incrementally, keep get_header()/get_footer() in the WP files and render only the middle section with Blade — mixing is fine.
WP globals as directives, not raw PHP. Typical set: @wpHead, @wpFooter, @wpBodyClass, @wpLanguageAttributes, loop helpers (@loop/@endloop or @while(have_posts()) + the_post()), @wpGetTemplatePart, @wpNavMenu, @shortcode, @doAction, @applyFilters. Confirm names in src/Directives. Prefer @wpNavMenu('primary') over @php wp_nav_menu([...]); @endphp — directives tend to handle escaping and defaults more cleanly.
Custom queries: always reset. If the package exposes a @query/@endquery pair around a WP_Query, use it and make sure wp_reset_postdata() runs. If not, do it explicitly with @php. A leaked global $post after a secondary loop is the #1 source of "why is my page title wrong" bugs.
Share global data with composers.
View::composer('*', function ($view) {
$view->with('options', function_exists('get_fields') ? get_fields('options') : []);
});
Guard ACF/plugin calls — composers run on every render, including error pages.
Escaping discipline. {{ }} escapes; {!! !!} does not. Post content needs {!! apply_filters('the_content', get_the_content()) !!}; titles, excerpts, and attribute values stay in {{ }}. Never {!! get_the_title() !!}.
Partials replace get_template_part(). @include('partials.card', ['post' => $post]) is clearer and testable, but it bypasses child-theme override semantics — if you ship a parent theme, document that partial overrides happen via the view path, not get_template_part.
Pass WP_Post objects explicitly rather than relying on the global inside partials. It makes views reusable for related-post sections, REST-rendered fragments, and AJAX responses.
cache/views throws on first render. Also block it from the web: deny from all in .htaccess or location ~ /cache/ { deny all; } in nginx. Blade's mtime check recompiles on source change, but containerised/rsync deploys with preserved timestamps can serve stale output — clear the cache as a deploy step.require vendor/autoload.php must precede View::setup, and both must run before any template file. Put them at the top of functions.php, not inside after_setup_theme, unless the package's own hooks require it.@ collisions. Emails, Twitter handles, and @media in inline CSS get parsed as directives. Escape with @@ or wrap the block in @verbatim … @endverbatim.<?php ?> in Blade. Use @php … @endphp. Raw tags confuse the compiler and break the compiled-file cache key.@wpHead and friends print directly; wrapping them in {{ }} produces empty output or double-escaping.render() return vs. echo. Check the source — some builds return a string. If yours does, echo View::render(...).View::setup accepts only one path, child-theme view overrides won't work out of the box. Check for a path/namespace registration method; otherwise build a small resolver that picks child-first.illuminate/view; running both setups leads to two Blade compilers and confusing directive registration. Don't mix.Blade::directive() calls. Keep names namespaced (@themeIcon) to avoid collisions with future package directives.@wpBodyClass or @bodyClass.WP_DEBUG log; "directive not found" → it isn't in your installed version; stale markup → delete cache/views/* and reload; wrong global data → dump $post at the top of the layout.How can I help you explore Laravel packages today?